Compare commits

...

9 Commits

Author SHA1 Message Date
Nate Butler
a3e04867ab Update git_ui.rs 2024-12-16 15:30:27 -05:00
Nate Butler
3abf165c1d Update git_panel.rs 2024-12-16 15:30:25 -05:00
Nate Butler
48eae645f2 Revert "Merge branch 'main' into git-panel-commit-editor"
This reverts commit 78b6cae754, reversing
changes made to 1cac4b6c00.
2024-12-16 15:29:44 -05:00
Nate Butler
78b6cae754 Merge branch 'main' into git-panel-commit-editor 2024-12-16 15:26:45 -05:00
Nate Butler
1cac4b6c00 Revert "wip"
This reverts commit aee641d79d.
2024-12-16 15:12:49 -05:00
Nate Butler
aee641d79d wip 2024-12-16 13:24:55 -05:00
Nate Butler
4f439ae35f Update git_panel.rs 2024-12-13 21:19:16 -05:00
Nate Butler
c2eea3a474 start on commit editor 2024-12-13 21:06:18 -05:00
Nate Butler
695f06c020 Checkpoint 2024-12-13 20:18:09 -05:00
97 changed files with 1908 additions and 4459 deletions

6
Cargo.lock generated
View File

@@ -2768,6 +2768,7 @@ dependencies = [
"language", "language",
"menu", "menu",
"notifications", "notifications",
"parking_lot",
"picker", "picker",
"pretty_assertions", "pretty_assertions",
"project", "project",
@@ -5181,14 +5182,17 @@ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"db", "db",
"editor",
"git", "git",
"gpui", "gpui",
"language",
"project", "project",
"schemars", "schemars",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"settings", "settings",
"theme",
"ui", "ui",
"util", "util",
"windows 0.58.0", "windows 0.58.0",
@@ -12634,7 +12638,6 @@ dependencies = [
"sha2", "sha2",
"shellexpand 2.1.2", "shellexpand 2.1.2",
"util", "util",
"zed_actions",
] ]
[[package]] [[package]]
@@ -16107,7 +16110,6 @@ name = "zed_actions"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"gpui", "gpui",
"schemars",
"serde", "serde",
] ]

View File

@@ -1,4 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-cw"><path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/></svg>
<path d="M12 6.5L9.99556 4.21778C9.27778 3.5 8.12 3 7 3C6.20888 3 5.43552 3.2346 4.77772 3.67412C4.11992 4.11365 3.60723 4.73836 3.30448 5.46927C3.00173 6.20017 2.92252 7.00444 3.07686 7.78036C3.2312 8.55628 3.61216 9.26902 4.17157 9.82842C4.73098 10.3878 5.44372 10.7688 6.21964 10.9231C6.99556 11.0775 7.79983 10.9983 8.53073 10.6955C8.88113 10.5504 9.20712 10.357 9.5 10.1225" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 4V6.5H9.5" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 673 B

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -17,7 +17,6 @@
"escape": "menu::Cancel", "escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel", "ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel", "ctrl-c": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }], "alt-enter": ["picker::ConfirmInput", { "secondary": false }],
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow", "ctrl-shift-w": "workspace::CloseWindow",
@@ -426,10 +425,7 @@
"ctrl-shift-r": "task::Rerun", "ctrl-shift-r": "task::Rerun",
"ctrl-alt-r": "task::Rerun", "ctrl-alt-r": "task::Rerun",
"alt-t": "task::Rerun", "alt-t": "task::Rerun",
"alt-shift-t": "task::Spawn", "alt-shift-t": "task::Spawn"
"alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
// also possible to spawn tasks by name:
// "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
} }
}, },
// Bindings from Sublime Text // Bindings from Sublime Text
@@ -470,25 +466,22 @@
"enter": "editor::ConfirmRename" "enter": "editor::ConfirmRename"
} }
}, },
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{ {
"context": "Editor && !inline_completion && showing_completions", "context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion" "tab": "editor::ComposeCompletion"
} }
}, },
{ {
"context": "Editor && inline_completion && showing_completions", "context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion",
"shift-tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"tab": "editor::AcceptInlineCompletion" "tab": "editor::AcceptInlineCompletion"

View File

@@ -24,7 +24,6 @@
"cmd-escape": "menu::Cancel", "cmd-escape": "menu::Cancel",
"ctrl-escape": "menu::Cancel", "ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel", "ctrl-c": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
"cmd-shift-w": "workspace::CloseWindow", "cmd-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom", "shift-escape": "workspace::ToggleZoom",
"cmd-o": "workspace::Open", "cmd-o": "workspace::Open",
@@ -495,9 +494,8 @@
"bindings": { "bindings": {
"cmd-shift-r": "task::Spawn", "cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun", "cmd-alt-r": "task::Rerun",
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }] "alt-t": "task::Spawn",
// also possible to spawn tasks by name: "alt-shift-t": "task::Spawn"
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
} }
}, },
// Bindings from Sublime Text // Bindings from Sublime Text
@@ -539,25 +537,22 @@
"enter": "editor::ConfirmRename" "enter": "editor::ConfirmRename"
} }
}, },
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{ {
"context": "Editor && !inline_completion && showing_completions", "context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion" "tab": "editor::ComposeCompletion"
} }
}, },
{ {
"context": "Editor && inline_completion && showing_completions", "context": "Editor && inline_completion",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion",
"shift-tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && inline_completion && !showing_completions",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"tab": "editor::AcceptInlineCompletion" "tab": "editor::AcceptInlineCompletion"

View File

@@ -101,8 +101,6 @@
// Whether to show the informational hover box when moving the mouse // Whether to show the informational hover box when moving the mouse
// over symbols in the editor. // over symbols in the editor.
"hover_popover_enabled": true, "hover_popover_enabled": true,
// Time to wait before showing the informational hover box
"hover_popover_delay": 350,
// Whether to confirm before quitting Zed. // Whether to confirm before quitting Zed.
"confirm_quit": false, "confirm_quit": false,
// Whether to restore last closed project when fresh Zed instance is opened. // Whether to restore last closed project when fresh Zed instance is opened.
@@ -554,8 +552,6 @@
// 4. Save when idle for a certain amount of time: // 4. Save when idle for a certain amount of time:
// "autosave": { "after_delay": {"milliseconds": 500} }, // "autosave": { "after_delay": {"milliseconds": 500} },
"autosave": "off", "autosave": "off",
// Maximum number of tabs per pane. Unset for unlimited.
"max_tabs": null,
// Settings related to the editor's tab bar. // Settings related to the editor's tab bar.
"tab_bar": { "tab_bar": {
// Whether or not to show the tab bar in the editor // Whether or not to show the tab bar in the editor

View File

@@ -15,14 +15,10 @@
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false, "allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started: // What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the task's pane, and focus the corresponding tab in it (default) // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always", "reveal": "always",
// Where to place the task's terminal item after starting the task:
// * `dock` — in the terminal dock, "regular" terminal items' place (default)
// * `center` — in the central pane group, "main" editor area
"reveal_target": "dock",
// What to do with the terminal pane and tab, after the command had finished: // What to do with the terminal pane and tab, after the command had finished:
// * `never` — Do nothing when the command finishes (default) // * `never` — Do nothing when the command finishes (default)
// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it // * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it

View File

@@ -716,7 +716,7 @@ impl ContextStore {
let candidates = metadata let candidates = metadata
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title)) .map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = fuzzy::match_strings( let matches = fuzzy::match_strings(
&candidates, &candidates,

View File

@@ -47,7 +47,6 @@ use std::{
iter, mem, iter, mem,
ops::{Range, RangeInclusive}, ops::{Range, RangeInclusive},
pin::Pin, pin::Pin,
rc::Rc,
sync::Arc, sync::Arc,
task::{self, Poll}, task::{self, Poll},
time::{Duration, Instant}, time::{Duration, Instant},
@@ -175,7 +174,7 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) { if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.push_code_action_provider( editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider { Arc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(), editor: cx.view().downgrade(),
workspace: workspace.downgrade(), workspace: workspace.downgrade(),
}), }),
@@ -1442,15 +1441,6 @@ impl Render for PromptEditor {
] ]
} }
CodegenStatus::Error(_) | CodegenStatus::Done => { CodegenStatus::Error(_) | CodegenStatus::Done => {
let must_rerun =
self.edited_since_done || matches!(status, CodegenStatus::Error(_));
// when accept button isn't visible, then restart maps to confirm
// when accept button is visible, then restart must be mapped to an alternate keyboard shortcut
let restart_key: &dyn gpui::Action = if must_rerun {
&menu::Confirm
} else {
&menu::Restart
};
vec![ vec![
IconButton::new("cancel", IconName::Close) IconButton::new("cancel", IconName::Close)
.icon_color(Color::Muted) .icon_color(Color::Muted)
@@ -1460,22 +1450,23 @@ impl Render for PromptEditor {
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)), cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
) )
.into_any_element(), .into_any_element(),
IconButton::new("restart", IconName::RotateCw) if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
.icon_color(Color::Muted) IconButton::new("restart", IconName::RotateCw)
.shape(IconButtonShape::Square) .icon_color(Color::Info)
.tooltip(|cx| { .shape(IconButtonShape::Square)
Tooltip::with_meta( .tooltip(|cx| {
"Regenerate Transformation", Tooltip::with_meta(
Some(restart_key), "Restart Transformation",
"Current change will be discarded", Some(&menu::Confirm),
cx, "Changes will be discarded",
) cx,
}) )
.on_click(cx.listener(|_, _, cx| { })
cx.emit(PromptEditorEvent::StartRequested); .on_click(cx.listener(|_, _, cx| {
})) cx.emit(PromptEditorEvent::StartRequested);
.into_any_element(), }))
if !must_rerun { .into_any_element()
} else {
IconButton::new("confirm", IconName::Check) IconButton::new("confirm", IconName::Check)
.icon_color(Color::Info) .icon_color(Color::Info)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
@@ -1484,8 +1475,6 @@ impl Render for PromptEditor {
cx.emit(PromptEditorEvent::ConfirmRequested); cx.emit(PromptEditorEvent::ConfirmRequested);
})) }))
.into_any_element() .into_any_element()
} else {
div().into_any_element()
}, },
] ]
} }
@@ -1502,7 +1491,6 @@ impl Render for PromptEditor {
.py(cx.line_height() / 2.5) .py(cx.line_height() / 2.5)
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::restart))
.on_action(cx.listener(Self::move_up)) .on_action(cx.listener(Self::move_up))
.on_action(cx.listener(Self::move_down)) .on_action(cx.listener(Self::move_down))
.capture_action(cx.listener(Self::cycle_prev)) .capture_action(cx.listener(Self::cycle_prev))
@@ -1849,10 +1837,6 @@ impl PromptEditor {
} }
} }
fn restart(&mut self, _: &menu::Restart, cx: &mut ViewContext<Self>) {
cx.emit(PromptEditorEvent::StartRequested);
}
fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
match self.codegen.read(cx).status(cx) { match self.codegen.read(cx).status(cx) {
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => { CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {

View File

@@ -1439,7 +1439,10 @@ impl PromptStore {
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(ix, metadata)| { .filter_map(|(ix, metadata)| {
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?)) Some(StringMatchCandidate::new(
ix,
metadata.title.as_ref()?.to_string(),
))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = fuzzy::match_strings( let matches = fuzzy::match_strings(

View File

@@ -7,13 +7,11 @@ use editor::{CompletionProvider, Editor};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext}; use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint}; use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
use parking_lot::Mutex; use parking_lot::{Mutex, RwLock};
use project::CompletionIntent; use project::CompletionIntent;
use rope::Point; use rope::Point;
use std::{ use std::{
cell::RefCell,
ops::Range, ops::Range,
rc::Rc,
sync::{ sync::{
atomic::{AtomicBool, Ordering::SeqCst}, atomic::{AtomicBool, Ordering::SeqCst},
Arc, Arc,
@@ -80,7 +78,11 @@ impl SlashCommandCompletionProvider {
.command_names(cx) .command_names(cx)
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(ix, def)| StringMatchCandidate::new(ix, &def)) .map(|(ix, def)| StringMatchCandidate {
id: ix,
string: def.to_string(),
char_bag: def.as_ref().into(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let command_name = command_name.to_string(); let command_name = command_name.to_string();
let editor = self.editor.clone(); let editor = self.editor.clone();
@@ -324,7 +326,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
&self, &self,
_: Model<Buffer>, _: Model<Buffer>,
_: Vec<usize>, _: Vec<usize>,
_: Rc<RefCell<Box<[project::Completion]>>>, _: Arc<RwLock<Box<[project::Completion]>>>,
_: &mut ViewContext<Editor>, _: &mut ViewContext<Editor>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
Task::ready(Ok(true)) Task::ready(Ok(true))

View File

@@ -218,7 +218,10 @@ impl Options {
} }
fn match_candidates_for_args() -> [StringMatchCandidate; 1] { fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)] [StringMatchCandidate::new(
0,
INCLUDE_WARNINGS_ARGUMENT.to_string(),
)]
} }
} }

View File

@@ -249,7 +249,11 @@ fn tab_items_for_queries(
.enumerate() .enumerate()
.filter_map(|(id, (full_path, ..))| { .filter_map(|(id, (full_path, ..))| {
let path_string = full_path.as_deref()?.to_string_lossy().to_string(); let path_string = full_path.as_deref()?.to_string_lossy().to_string();
Some(fuzzy::StringMatchCandidate::new(id, &path_string)) Some(fuzzy::StringMatchCandidate {
id,
char_bag: path_string.as_str().into(),
string: path_string,
})
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut processed_matches = HashSet::default(); let mut processed_matches = HashSet::default();

View File

@@ -3,7 +3,6 @@ mod assistant_panel;
mod assistant_settings; mod assistant_settings;
mod context; mod context;
mod context_picker; mod context_picker;
mod context_strip;
mod inline_assistant; mod inline_assistant;
mod message_editor; mod message_editor;
mod prompts; mod prompts;

View File

@@ -94,9 +94,7 @@ impl AssistantPanel {
cx, cx,
) )
}), }),
message_editor: cx.new_view(|cx| { message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
MessageEditor::new(workspace, thread_store.downgrade(), thread.clone(), cx)
}),
tools, tools,
local_timezone: UtcOffset::from_whole_seconds( local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(), chrono::Local::now().offset().local_minus_utc(),
@@ -125,14 +123,8 @@ impl AssistantPanel {
cx, cx,
) )
}); });
self.message_editor = cx.new_view(|cx| { self.message_editor =
MessageEditor::new( cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx); self.message_editor.focus_handle(cx).focus(cx);
} }
@@ -154,14 +146,8 @@ impl AssistantPanel {
cx, cx,
) )
}); });
self.message_editor = cx.new_view(|cx| { self.message_editor =
MessageEditor::new( cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.workspace.clone(),
self.thread_store.downgrade(),
thread,
cx,
)
});
self.message_editor.focus_handle(cx).focus(cx); self.message_editor.focus_handle(cx).focus(cx);
} }
@@ -323,11 +309,10 @@ impl AssistantPanel {
.when(!recent_threads.is_empty(), |parent| { .when(!recent_threads.is_empty(), |parent| {
parent parent
.child( .child(
h_flex().w_full().justify_center().child( h_flex()
Label::new("Recent Threads:") .w_full()
.size(LabelSize::Small) .justify_center()
.color(Color::Muted), .child(Label::new("Recent Threads:").size(LabelSize::Small)),
),
) )
.child( .child(
v_flex().gap_2().children( v_flex().gap_2().children(

View File

@@ -24,5 +24,4 @@ pub struct Context {
pub enum ContextKind { pub enum ContextKind {
File, File,
FetchedUrl, FetchedUrl,
Thread,
} }

View File

@@ -1,12 +1,11 @@
mod fetch_context_picker; mod fetch_context_picker;
mod file_context_picker; mod file_context_picker;
mod thread_context_picker;
use std::sync::Arc; use std::sync::Arc;
use gpui::{ use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakModel, WeakView, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip}; use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
@@ -15,16 +14,13 @@ use workspace::Workspace;
use crate::context_picker::fetch_context_picker::FetchContextPicker; use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker; use crate::context_picker::file_context_picker::FileContextPicker;
use crate::context_picker::thread_context_picker::ThreadContextPicker; use crate::message_editor::MessageEditor;
use crate::context_strip::ContextStrip;
use crate::thread_store::ThreadStore;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum ContextPickerMode { enum ContextPickerMode {
Default, Default,
File(View<FileContextPicker>), File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>), Fetch(View<FetchContextPicker>),
Thread(View<ThreadContextPicker>),
} }
pub(super) struct ContextPicker { pub(super) struct ContextPicker {
@@ -35,15 +31,13 @@ pub(super) struct ContextPicker {
impl ContextPicker { impl ContextPicker {
pub fn new( pub fn new(
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>, message_editor: WeakView<MessageEditor>,
context_strip: WeakView<ContextStrip>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let delegate = ContextPickerDelegate { let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(), context_picker: cx.view().downgrade(),
workspace, workspace: workspace.clone(),
thread_store, message_editor: message_editor.clone(),
context_strip,
entries: vec![ entries: vec![
ContextPickerEntry { ContextPickerEntry {
name: "directory".into(), name: "directory".into(),
@@ -60,11 +54,6 @@ impl ContextPicker {
description: "Fetch content from URL".into(), description: "Fetch content from URL".into(),
icon: IconName::Globe, icon: IconName::Globe,
}, },
ContextPickerEntry {
name: "thread".into(),
description: "Insert any thread".into(),
icon: IconName::MessageBubbles,
},
], ],
selected_ix: 0, selected_ix: 0,
}; };
@@ -92,7 +81,6 @@ impl FocusableView for ContextPicker {
ContextPickerMode::Default => self.picker.focus_handle(cx), ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx), ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx), ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
ContextPickerMode::Thread(thread_picker) => thread_picker.focus_handle(cx),
} }
} }
} }
@@ -106,7 +94,6 @@ impl Render for ContextPicker {
ContextPickerMode::Default => parent.child(self.picker.clone()), ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()), ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()), ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
ContextPickerMode::Thread(thread_picker) => parent.child(thread_picker.clone()),
}) })
} }
} }
@@ -121,8 +108,7 @@ struct ContextPickerEntry {
pub(crate) struct ContextPickerDelegate { pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>, message_editor: WeakView<MessageEditor>,
context_strip: WeakView<ContextStrip>,
entries: Vec<ContextPickerEntry>, entries: Vec<ContextPickerEntry>,
selected_ix: usize, selected_ix: usize,
} }
@@ -161,7 +147,7 @@ impl PickerDelegate for ContextPickerDelegate {
FileContextPicker::new( FileContextPicker::new(
self.context_picker.clone(), self.context_picker.clone(),
self.workspace.clone(), self.workspace.clone(),
self.context_strip.clone(), self.message_editor.clone(),
cx, cx,
) )
})); }));
@@ -171,17 +157,7 @@ impl PickerDelegate for ContextPickerDelegate {
FetchContextPicker::new( FetchContextPicker::new(
self.context_picker.clone(), self.context_picker.clone(),
self.workspace.clone(), self.workspace.clone(),
self.context_strip.clone(), self.message_editor.clone(),
cx,
)
}));
}
"thread" => {
this.mode = ContextPickerMode::Thread(cx.new_view(|cx| {
ThreadContextPicker::new(
self.thread_store.clone(),
self.context_picker.clone(),
self.context_strip.clone(),
cx, cx,
) )
})); }));
@@ -199,9 +175,7 @@ impl PickerDelegate for ContextPickerDelegate {
self.context_picker self.context_picker
.update(cx, |this, cx| match this.mode { .update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent), ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_) ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
| ContextPickerMode::Fetch(_)
| ContextPickerMode::Thread(_) => {}
}) })
.log_err(); .log_err();
} }

View File

@@ -8,12 +8,12 @@ use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, Wea
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler}; use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl}; use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ViewContext}; use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace; use workspace::Workspace;
use crate::context::ContextKind; use crate::context::ContextKind;
use crate::context_picker::ContextPicker; use crate::context_picker::ContextPicker;
use crate::context_strip::ContextStrip; use crate::message_editor::MessageEditor;
pub struct FetchContextPicker { pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>, picker: View<Picker<FetchContextPickerDelegate>>,
@@ -23,10 +23,10 @@ impl FetchContextPicker {
pub fn new( pub fn new(
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_strip); let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker } Self { picker }
@@ -55,7 +55,7 @@ enum ContentType {
pub struct FetchContextPickerDelegate { pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
url: String, url: String,
} }
@@ -63,12 +63,12 @@ impl FetchContextPickerDelegate {
pub fn new( pub fn new(
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
) -> Self { ) -> Self {
FetchContextPickerDelegate { FetchContextPickerDelegate {
context_picker, context_picker,
workspace, workspace,
context_strip, message_editor,
url: String::new(), url: String::new(),
} }
} }
@@ -150,15 +150,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem; type ListItem = ListItem;
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
if self.url.is_empty() { 1
0
} else {
1
}
}
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
"Enter the URL that you would like to fetch".into()
} }
fn selected_index(&self) -> usize { fn selected_index(&self) -> usize {
@@ -189,9 +181,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.delegate this.delegate
.context_strip .message_editor
.update(cx, |context_strip, _cx| { .update(cx, |message_editor, _cx| {
context_strip.insert_context(ContextKind::FetchedUrl, url, text); message_editor.insert_context(ContextKind::FetchedUrl, url, text);
}) })
})??; })??;
@@ -218,8 +210,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
Some( Some(
ListItem::new(ix) ListItem::new(ix)
.inset(true) .inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected) .toggle_state(selected)
.child(Label::new(self.url.clone())), .child(self.url.clone()),
) )
} }
} }

View File

@@ -8,13 +8,13 @@ use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView}; use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId}; use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem}; use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt as _; use util::ResultExt as _;
use workspace::Workspace; use workspace::Workspace;
use crate::context::ContextKind; use crate::context::ContextKind;
use crate::context_picker::ContextPicker; use crate::context_picker::ContextPicker;
use crate::context_strip::ContextStrip; use crate::message_editor::MessageEditor;
pub struct FileContextPicker { pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>, picker: View<Picker<FileContextPickerDelegate>>,
@@ -24,10 +24,10 @@ impl FileContextPicker {
pub fn new( pub fn new(
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_strip); let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx)); let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker } Self { picker }
@@ -49,7 +49,7 @@ impl Render for FileContextPicker {
pub struct FileContextPickerDelegate { pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
selected_index: usize, selected_index: usize,
} }
@@ -58,12 +58,12 @@ impl FileContextPickerDelegate {
pub fn new( pub fn new(
context_picker: WeakView<ContextPicker>, context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
context_strip: WeakView<ContextStrip>, message_editor: WeakView<MessageEditor>,
) -> Self { ) -> Self {
Self { Self {
context_picker, context_picker,
workspace, workspace,
context_strip, message_editor,
matches: Vec::new(), matches: Vec::new(),
selected_index: 0, selected_index: 0,
} }
@@ -214,22 +214,24 @@ impl PickerDelegate for FileContextPickerDelegate {
let buffer = open_buffer_task.await?; let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.delegate.context_strip.update(cx, |context_strip, cx| { this.delegate
let mut text = String::new(); .message_editor
text.push_str(&codeblock_fence_for_path(Some(&path), None)); .update(cx, |message_editor, cx| {
text.push_str(&buffer.read(cx).text()); let mut text = String::new();
if !text.ends_with('\n') { text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push('\n'); text.push_str(&buffer.read(cx).text());
} if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n"); text.push_str("```\n");
context_strip.insert_context( message_editor.insert_context(
ContextKind::File, ContextKind::File,
path.to_string_lossy().to_string(), path.to_string_lossy().to_string(),
text, text,
); );
}) })
})??; })??;
anyhow::Ok(()) anyhow::Ok(())
@@ -252,29 +254,14 @@ impl PickerDelegate for FileContextPickerDelegate {
selected: bool, selected: bool,
_cx: &mut ViewContext<Picker<Self>>, _cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let path_match = &self.matches[ix]; let mat = &self.matches[ix];
let file_name = path_match
.path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let directory = path_match
.path
.parent()
.map(|directory| format!("{}/", directory.to_string_lossy()));
Some( Some(
ListItem::new(ix).inset(true).toggle_state(selected).child( ListItem::new(ix)
h_flex() .inset(true)
.gap_2() .spacing(ListItemSpacing::Sparse)
.child(Label::new(file_name)) .toggle_state(selected)
.children(directory.map(|directory| { .child(mat.path.to_string_lossy().to_string()),
Label::new(directory)
.size(LabelSize::Small)
.color(Color::Muted)
})),
),
) )
} }
} }

View File

@@ -1,209 +0,0 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakModel, WeakView};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem};
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::context_strip::ContextStrip;
use crate::thread::ThreadId;
use crate::thread_store::ThreadStore;
pub struct ThreadContextPicker {
picker: View<Picker<ThreadContextPickerDelegate>>,
}
impl ThreadContextPicker {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_strip: WeakView<ContextStrip>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate =
ThreadContextPickerDelegate::new(thread_store, context_picker, context_strip);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
ThreadContextPicker { picker }
}
}
impl FocusableView for ThreadContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for ThreadContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, Clone)]
struct ThreadContextEntry {
id: ThreadId,
summary: SharedString,
}
pub struct ThreadContextPickerDelegate {
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_strip: WeakView<ContextStrip>,
matches: Vec<ThreadContextEntry>,
selected_index: usize,
}
impl ThreadContextPickerDelegate {
pub fn new(
thread_store: WeakModel<ThreadStore>,
context_picker: WeakView<ContextPicker>,
context_strip: WeakView<ContextStrip>,
) -> Self {
ThreadContextPickerDelegate {
thread_store,
context_picker,
context_strip,
matches: Vec::new(),
selected_index: 0,
}
}
}
impl PickerDelegate for ThreadContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search threads…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Ok(threads) = self.thread_store.update(cx, |this, cx| {
this.threads(cx)
.into_iter()
.map(|thread| {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let id = thread.read(cx).id().clone();
let summary = thread.read(cx).summary().unwrap_or(DEFAULT_SUMMARY);
ThreadContextEntry { id, summary }
})
.collect::<Vec<_>>()
}) else {
return Task::ready(());
};
let executor = cx.background_executor().clone();
let search_task = cx.background_executor().spawn(async move {
if query.is_empty() {
threads
} else {
let candidates = threads
.iter()
.enumerate()
.map(|(id, thread)| StringMatchCandidate::new(id, &thread.summary))
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
&query,
false,
100,
&Default::default(),
executor,
)
.await;
matches
.into_iter()
.map(|mat| threads[mat.candidate_id].clone())
.collect()
}
});
cx.spawn(|this, mut cx| async move {
let matches = search_task.await;
this.update(&mut cx, |this, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = 0;
cx.notify();
})
.ok();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let entry = &self.matches[self.selected_index];
let Some(thread_store) = self.thread_store.upgrade() else {
return;
};
let Some(thread) = thread_store.update(cx, |this, cx| this.open_thread(&entry.id, cx))
else {
return;
};
self.context_strip
.update(cx, |context_strip, cx| {
let text = thread.update(cx, |thread, _cx| {
let mut text = String::new();
for message in thread.messages() {
text.push_str(match message.role {
language_model::Role::User => "User:",
language_model::Role::Assistant => "Assistant:",
language_model::Role::System => "System:",
});
text.push('\n');
text.push_str(&message.text);
text.push('\n');
}
text
});
context_strip.insert_context(ContextKind::Thread, entry.summary.clone(), text);
})
.ok();
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let thread = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.toggle_state(selected)
.child(thread.summary.clone()),
)
}
}

View File

@@ -1,101 +0,0 @@
use std::rc::Rc;
use gpui::{View, WeakModel, WeakView};
use ui::{prelude::*, IconButtonShape, PopoverMenu, PopoverMenuHandle, Tooltip};
use workspace::Workspace;
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::thread_store::ThreadStore;
use crate::ui::ContextPill;
pub struct ContextStrip {
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
}
impl ContextStrip {
pub fn new(
workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
Self {
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| {
ContextPicker::new(workspace.clone(), thread_store.clone(), weak_self, cx)
}),
context_picker_handle: PopoverMenuHandle::default(),
}
}
pub fn drain(&mut self) -> Vec<Context> {
self.context.drain(..).collect()
}
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
}
impl Render for ContextStrip {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let context_picker = self.context_picker.clone();
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
})
}
}

View File

@@ -45,7 +45,6 @@ use std::{
iter, mem, iter, mem,
ops::{Range, RangeInclusive}, ops::{Range, RangeInclusive},
pin::Pin, pin::Pin,
rc::Rc,
sync::Arc, sync::Arc,
task::{self, Poll}, task::{self, Poll},
time::Instant, time::Instant,
@@ -179,7 +178,7 @@ impl InlineAssistant {
if let Some(editor) = item.act_as::<Editor>(cx) { if let Some(editor) = item.act_as::<Editor>(cx) {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.push_code_action_provider( editor.push_code_action_provider(
Rc::new(AssistantCodeActionProvider { Arc::new(AssistantCodeActionProvider {
editor: cx.view().downgrade(), editor: cx.view().downgrade(),
workspace: workspace.downgrade(), workspace: workspace.downgrade(),
}), }),
@@ -212,12 +211,12 @@ impl InlineAssistant {
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target { let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
InlineAssistTarget::Editor(active_editor) => { InlineAssistTarget::Editor(active_editor) => {
InlineAssistant::update_global(cx, |assistant, cx| { InlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_editor, cx.view().downgrade(), cx) assistant.assist(&active_editor, Some(cx.view().downgrade()), cx)
}) })
} }
InlineAssistTarget::Terminal(active_terminal) => { InlineAssistTarget::Terminal(active_terminal) => {
TerminalInlineAssistant::update_global(cx, |assistant, cx| { TerminalInlineAssistant::update_global(cx, |assistant, cx| {
assistant.assist(&active_terminal, cx.view().downgrade(), cx) assistant.assist(&active_terminal, Some(cx.view().downgrade()), cx)
}) })
} }
}; };
@@ -264,7 +263,7 @@ impl InlineAssistant {
pub fn assist( pub fn assist(
&mut self, &mut self,
editor: &View<Editor>, editor: &View<Editor>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| { let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
@@ -429,7 +428,7 @@ impl InlineAssistant {
initial_prompt: String, initial_prompt: String,
initial_transaction_id: Option<TransactionId>, initial_transaction_id: Option<TransactionId>,
focus: bool, focus: bool,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> InlineAssistId { ) -> InlineAssistId {
let assist_group_id = self.next_assist_group_id.post_inc(); let assist_group_id = self.next_assist_group_id.post_inc();
@@ -2166,7 +2165,7 @@ pub struct InlineAssist {
decorations: Option<InlineAssistDecorations>, decorations: Option<InlineAssistDecorations>,
codegen: Model<Codegen>, codegen: Model<Codegen>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
} }
impl InlineAssist { impl InlineAssist {
@@ -2180,7 +2179,7 @@ impl InlineAssist {
end_block_id: CustomBlockId, end_block_id: CustomBlockId,
range: Range<Anchor>, range: Range<Anchor>,
codegen: Model<Codegen>, codegen: Model<Codegen>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let prompt_editor_focus_handle = prompt_editor.focus_handle(cx); let prompt_editor_focus_handle = prompt_editor.focus_handle(cx);
@@ -2240,7 +2239,11 @@ impl InlineAssist {
if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) { if let CodegenStatus::Error(error) = codegen.read(cx).status(cx) {
if assist.decorations.is_none() { if assist.decorations.is_none() {
if let Some(workspace) = assist.workspace.upgrade() { if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
let error = format!("Inline assistant error: {}", error); let error = format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
struct InlineAssistantError; struct InlineAssistantError;
@@ -3383,7 +3386,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
"Fix Diagnostics".into(), "Fix Diagnostics".into(),
None, None,
true, true,
workspace, Some(workspace),
cx, cx,
); );
assistant.start_assist(assist_id, cx); assistant.start_assist(assist_id, cx);

View File

@@ -1,21 +1,30 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakModel, WeakView}; use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool}; use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu}; use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, Tooltip}; use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace; use workspace::Workspace;
use crate::context_strip::ContextStrip; use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::thread::{RequestKind, Thread}; use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore; use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector}; use crate::{Chat, ToggleModelSelector};
pub struct MessageEditor { pub struct MessageEditor {
thread: Model<Thread>, thread: Model<Thread>,
editor: View<Editor>, editor: View<Editor>,
context_strip: View<ContextStrip>, context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>, language_model_selector: View<LanguageModelSelector>,
use_tools: bool, use_tools: bool,
} }
@@ -23,10 +32,10 @@ pub struct MessageEditor {
impl MessageEditor { impl MessageEditor {
pub fn new( pub fn new(
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
thread_store: WeakModel<ThreadStore>,
thread: Model<Thread>, thread: Model<Thread>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let weak_self = cx.view().downgrade();
Self { Self {
thread, thread,
editor: cx.new_view(|cx| { editor: cx.new_view(|cx| {
@@ -35,8 +44,10 @@ impl MessageEditor {
editor editor
}), }),
context_strip: cx context: Vec::new(),
.new_view(|cx| ContextStrip::new(workspace.clone(), thread_store.clone(), cx)), next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
language_model_selector: cx.new_view(|cx| { language_model_selector: cx.new_view(|cx| {
LanguageModelSelector::new( LanguageModelSelector::new(
|model, _cx| { |model, _cx| {
@@ -49,6 +60,20 @@ impl MessageEditor {
} }
} }
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) { fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx); self.send_to_model(RequestKind::Chat, cx);
} }
@@ -75,7 +100,7 @@ impl MessageEditor {
editor.clear(cx); editor.clear(cx);
text text
}); });
let context = self.context_strip.update(cx, |this, _cx| this.drain()); let context = self.context.drain(..).collect::<Vec<_>>();
self.thread.update(cx, |thread, cx| { self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx); thread.insert_user_message(user_message, context, cx);
@@ -161,6 +186,7 @@ impl Render for MessageEditor {
let font_size = TextSize::Default.rems(cx); let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3; let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx); let focus_handle = self.editor.focus_handle(cx);
let context_picker = self.context_picker.clone();
v_flex() v_flex()
.key_context("MessageEditor") .key_context("MessageEditor")
@@ -169,7 +195,48 @@ impl Render for MessageEditor {
.gap_2() .gap_2()
.p_2() .p_2()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.child(self.context_strip.clone()) .child(
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({ .child({
let settings = ThemeSettings::get_global(cx); let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle { let text_style = TextStyle {

View File

@@ -82,7 +82,7 @@ impl TerminalInlineAssistant {
pub fn assist( pub fn assist(
&mut self, &mut self,
terminal_view: &View<TerminalView>, terminal_view: &View<TerminalView>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
let terminal = terminal_view.read(cx).terminal().clone(); let terminal = terminal_view.read(cx).terminal().clone();
@@ -361,7 +361,7 @@ struct TerminalInlineAssist {
terminal: WeakView<TerminalView>, terminal: WeakView<TerminalView>,
prompt_editor: Option<View<PromptEditor>>, prompt_editor: Option<View<PromptEditor>>,
codegen: Model<Codegen>, codegen: Model<Codegen>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@@ -370,7 +370,7 @@ impl TerminalInlineAssist {
assist_id: TerminalInlineAssistId, assist_id: TerminalInlineAssistId,
terminal: &View<TerminalView>, terminal: &View<TerminalView>,
prompt_editor: View<PromptEditor>, prompt_editor: View<PromptEditor>,
workspace: WeakView<Workspace>, workspace: Option<WeakView<Workspace>>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let codegen = prompt_editor.read(cx).codegen.clone(); let codegen = prompt_editor.read(cx).codegen.clone();
@@ -396,7 +396,11 @@ impl TerminalInlineAssist {
if let CodegenStatus::Error(error) = &codegen.read(cx).status { if let CodegenStatus::Error(error) = &codegen.read(cx).status {
if assist.prompt_editor.is_none() { if assist.prompt_editor.is_none() {
if let Some(workspace) = assist.workspace.upgrade() { if let Some(workspace) = assist
.workspace
.as_ref()
.and_then(|workspace| workspace.upgrade())
{
let error = let error =
format!("Terminal inline assistant error: {}", error); format!("Terminal inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {

View File

@@ -194,7 +194,6 @@ impl Thread {
if let Some(context) = self.context_for_message(message.id) { if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new(); let mut file_context = String::new();
let mut fetch_context = String::new(); let mut fetch_context = String::new();
let mut thread_context = String::new();
for context in context.iter() { for context in context.iter() {
match context.kind { match context.kind {
@@ -208,12 +207,6 @@ impl Thread {
fetch_context.push_str(&context.text); fetch_context.push_str(&context.text);
fetch_context.push('\n'); fetch_context.push('\n');
} }
ContextKind::Thread => {
thread_context.push_str(&context.name);
thread_context.push('\n');
thread_context.push_str(&context.text);
thread_context.push('\n');
}
} }
} }
@@ -228,12 +221,6 @@ impl Thread {
context_text.push_str(&fetch_context); context_text.push_str(&fetch_context);
} }
if !thread_context.is_empty() {
context_text
.push_str("The following previous conversation threads are available\n");
context_text.push_str(&thread_context);
}
request_message request_message
.content .content
.push(MessageContent::Text(context_text)) .push(MessageContent::Text(context_text))

View File

@@ -2,7 +2,7 @@ use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView, uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
}; };
use time::{OffsetDateTime, UtcOffset}; use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip}; use ui::{prelude::*, IconButtonShape, ListItem};
use crate::thread::Thread; use crate::thread::Thread;
use crate::thread_store::ThreadStore; use crate::thread_store::ThreadStore;
@@ -117,25 +117,17 @@ impl RenderOnce for PastThread {
.unwrap_or(UtcOffset::UTC), .unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute, time_format::TimestampFormat::EnhancedAbsolute,
); );
ListItem::new(("past-thread", self.thread.entity_id())) ListItem::new(("past-thread", self.thread.entity_id()))
.outlined()
.start_slot(Icon::new(IconName::MessageBubbles)) .start_slot(Icon::new(IconName::MessageBubbles))
.spacing(ListItemSpacing::Sparse) .child(Label::new(summary))
.child(Label::new(summary).size(LabelSize::Small))
.end_slot( .end_slot(
h_flex() h_flex()
.gap_2() .gap_2()
.child( .child(Label::new(thread_timestamp).color(Color::Disabled))
Label::new(thread_timestamp)
.color(Color::Disabled)
.size(LabelSize::Small),
)
.child( .child(
IconButton::new("delete", IconName::TrashAlt) IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square) .shape(IconButtonShape::Square)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
.on_click({ .on_click({
let assistant_panel = self.assistant_panel.clone(); let assistant_panel = self.assistant_panel.clone();
let id = id.clone(); let id = id.clone();

View File

@@ -44,6 +44,7 @@ gpui.workspace = true
language.workspace = true language.workspace = true
menu.workspace = true menu.workspace = true
notifications.workspace = true notifications.workspace = true
parking_lot.workspace = true
picker.workspace = true picker.workspace = true
project.workspace = true project.workspace = true
release_channel.workspace = true release_channel.workspace = true

View File

@@ -12,15 +12,10 @@ use language::{
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry, language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
LanguageServerId, ToOffset, LanguageServerId, ToOffset,
}; };
use parking_lot::RwLock;
use project::{search::SearchQuery, Completion}; use project::{search::SearchQuery, Completion};
use settings::Settings; use settings::Settings;
use std::{ use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
cell::RefCell,
ops::Range,
rc::Rc,
sync::{Arc, LazyLock},
time::Duration,
};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, TextSize}; use ui::{prelude::*, TextSize};
@@ -73,7 +68,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
&self, &self,
_buffer: Model<Buffer>, _buffer: Model<Buffer>,
_completion_indices: Vec<usize>, _completion_indices: Vec<usize>,
_completions: Rc<RefCell<Box<[Completion]>>>, _completions: Arc<RwLock<Box<[Completion]>>>,
_cx: &mut ViewContext<Editor>, _cx: &mut ViewContext<Editor>,
) -> Task<anyhow::Result<bool>> { ) -> Task<anyhow::Result<bool>> {
Task::ready(Ok(false)) Task::ready(Ok(false))
@@ -386,7 +381,11 @@ impl MessageEditor {
let candidates = names let candidates = names
.into_iter() .into_iter()
.map(|user| StringMatchCandidate::new(0, &user)) .map(|user| StringMatchCandidate {
id: 0,
string: user.clone(),
char_bag: user.chars().collect(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Some((start_anchor, query, candidates)) Some((start_anchor, query, candidates))
@@ -402,7 +401,11 @@ impl MessageEditor {
LazyLock::new(|| { LazyLock::new(|| {
let emojis = emojis::iter() let emojis = emojis::iter()
.flat_map(|s| s.shortcodes()) .flat_map(|s| s.shortcodes())
.map(|emoji| StringMatchCandidate::new(0, emoji)) .map(|emoji| StringMatchCandidate {
id: 0,
string: emoji.to_string(),
char_bag: emoji.chars().collect(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
emojis emojis
}); });

View File

@@ -393,8 +393,11 @@ impl CollabPanel {
// Populate the active user. // Populate the active user.
if let Some(user) = user_store.current_user() { if let Some(user) = user_store.current_user() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates.push(StringMatchCandidate {
.push(StringMatchCandidate::new(0, &user.github_login)); id: 0,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
});
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@@ -433,10 +436,11 @@ impl CollabPanel {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend(room.remote_participants().values().map(|participant| { .extend(room.remote_participants().values().map(|participant| {
StringMatchCandidate::new( StringMatchCandidate {
participant.user.id as usize, id: participant.user.id as usize,
&participant.user.github_login, string: participant.user.github_login.clone(),
) char_bag: participant.user.github_login.chars().collect(),
}
})); }));
let mut matches = executor.block(match_strings( let mut matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
@@ -485,8 +489,10 @@ impl CollabPanel {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend(room.pending_participants().iter().enumerate().map( .extend(room.pending_participants().iter().enumerate().map(
|(id, participant)| { |(id, participant)| StringMatchCandidate {
StringMatchCandidate::new(id, &participant.github_login) id,
string: participant.github_login.clone(),
char_bag: participant.github_login.chars().collect(),
}, },
)); ));
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
@@ -513,12 +519,17 @@ impl CollabPanel {
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates.extend( self.match_candidates
channel_store .extend(
.ordered_channels() channel_store
.enumerate() .ordered_channels()
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)), .enumerate()
); .map(|(ix, (_, channel))| StringMatchCandidate {
id: ix,
string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(),
}),
);
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@@ -589,12 +600,14 @@ impl CollabPanel {
let channel_invites = channel_store.channel_invitations(); let channel_invites = channel_store.channel_invitations();
if !channel_invites.is_empty() { if !channel_invites.is_empty() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates.extend( self.match_candidates
channel_invites .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
.iter() StringMatchCandidate {
.enumerate() id: ix,
.map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)), string: channel.name.clone().into(),
); char_bag: channel.name.chars().collect(),
}
}));
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@@ -624,12 +637,17 @@ impl CollabPanel {
let incoming = user_store.incoming_contact_requests(); let incoming = user_store.incoming_contact_requests();
if !incoming.is_empty() { if !incoming.is_empty() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates.extend( self.match_candidates
incoming .extend(
.iter() incoming
.enumerate() .iter()
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)), .enumerate()
); .map(|(ix, user)| StringMatchCandidate {
id: ix,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
}),
);
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@@ -648,12 +666,17 @@ impl CollabPanel {
let outgoing = user_store.outgoing_contact_requests(); let outgoing = user_store.outgoing_contact_requests();
if !outgoing.is_empty() { if !outgoing.is_empty() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates.extend( self.match_candidates
outgoing .extend(
.iter() outgoing
.enumerate() .iter()
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)), .enumerate()
); .map(|(ix, user)| StringMatchCandidate {
id: ix,
string: user.github_login.clone(),
char_bag: user.github_login.chars().collect(),
}),
);
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,
&query, &query,
@@ -680,12 +703,17 @@ impl CollabPanel {
let contacts = user_store.contacts(); let contacts = user_store.contacts();
if !contacts.is_empty() { if !contacts.is_empty() {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates.extend( self.match_candidates
contacts .extend(
.iter() contacts
.enumerate() .iter()
.map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)), .enumerate()
); .map(|(ix, contact)| StringMatchCandidate {
id: ix,
string: contact.user.github_login.clone(),
char_bag: contact.user.github_login.chars().collect(),
}),
);
let matches = executor.block(match_strings( let matches = executor.block(match_strings(
&self.match_candidates, &self.match_candidates,

View File

@@ -272,7 +272,11 @@ impl PickerDelegate for ChannelModalDelegate {
self.match_candidates.clear(); self.match_candidates.clear();
self.match_candidates self.match_candidates
.extend(self.members.iter().enumerate().map(|(id, member)| { .extend(self.members.iter().enumerate().map(|(id, member)| {
StringMatchCandidate::new(id, &member.user.github_login) StringMatchCandidate {
id,
string: member.user.github_login.clone(),
char_bag: member.user.github_login.chars().collect(),
}
})); }));
let matches = cx.background_executor().block(match_strings( let matches = cx.background_executor().block(match_strings(

View File

@@ -283,7 +283,11 @@ impl PickerDelegate for CommandPaletteDelegate {
let candidates = commands let candidates = commands
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name)) .map(|(ix, command)| StringMatchCandidate {
id: ix,
string: command.name.to_string(),
char_bag: command.name.chars().collect(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = if query.is_empty() { let matches = if query.is_empty() {
candidates candidates

View File

@@ -373,17 +373,6 @@ mod tests {
// Ensure existing inline completion is interpolated when inserting again. // Ensure existing inline completion is interpolated when inserting again.
cx.simulate_keystroke("c"); cx.simulate_keystroke("c");
// We still request a normal LSP completion, but we interpolate the
// existing inline completion.
drop(handle_completion_request(
&mut cx,
indoc! {"
one.c|<>
two
three
"},
vec!["ompletion_a", "ompletion_b"],
));
executor.run_until_parked(); executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
assert!(!editor.context_menu_visible()); assert!(!editor.context_menu_visible());

View File

@@ -1050,7 +1050,6 @@ fn editor_blocks(
.ok()? .ok()?
} }
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
Block::ExcerptBoundary { Block::ExcerptBoundary {
starts_new_buffer, .. starts_new_buffer, ..
} => { } => {

View File

@@ -1,5 +1,4 @@
use std::cell::RefCell; use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
@@ -12,6 +11,7 @@ use language::{CodeLabel, Documentation};
use lsp::LanguageServerId; use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId}; use multi_buffer::{Anchor, ExcerptId};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use parking_lot::RwLock;
use project::{CodeAction, Completion, TaskSourceKind}; use project::{CodeAction, Completion, TaskSourceKind};
use task::ResolvedTask; use task::ResolvedTask;
use ui::{ use ui::{
@@ -137,9 +137,9 @@ pub struct CompletionsMenu {
sort_completions: bool, sort_completions: bool,
pub initial_position: Anchor, pub initial_position: Anchor,
pub buffer: Model<Buffer>, pub buffer: Model<Buffer>,
pub completions: Rc<RefCell<Box<[Completion]>>>, pub completions: Arc<RwLock<Box<[Completion]>>>,
match_candidates: Rc<[StringMatchCandidate]>, match_candidates: Arc<[StringMatchCandidate]>,
pub matches: Rc<[StringMatch]>, pub matches: Arc<[StringMatch]>,
pub selected_item: usize, pub selected_item: usize,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
resolve_completions: bool, resolve_completions: bool,
@@ -160,7 +160,12 @@ impl CompletionsMenu {
let match_candidates = completions let match_candidates = completions
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.filter_text())) .map(|(id, completion)| {
StringMatchCandidate::new(
id,
completion.label.text[completion.label.filter_range.clone()].into(),
)
})
.collect(); .collect();
Self { Self {
@@ -169,7 +174,7 @@ impl CompletionsMenu {
initial_position, initial_position,
buffer, buffer,
show_completion_documentation, show_completion_documentation,
completions: RefCell::new(completions).into(), completions: Arc::new(RwLock::new(completions)),
match_candidates, match_candidates,
matches: Vec::new().into(), matches: Vec::new().into(),
selected_item: 0, selected_item: 0,
@@ -206,7 +211,7 @@ impl CompletionsMenu {
let match_candidates = choices let match_candidates = choices
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, completion)| StringMatchCandidate::new(id, &completion)) .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
.collect(); .collect();
let matches = choices let matches = choices
.iter() .iter()
@@ -223,7 +228,7 @@ impl CompletionsMenu {
sort_completions, sort_completions,
initial_position: selection.start, initial_position: selection.start,
buffer, buffer,
completions: RefCell::new(completions).into(), completions: Arc::new(RwLock::new(completions)),
match_candidates, match_candidates,
matches, matches,
selected_item: 0, selected_item: 0,
@@ -329,13 +334,13 @@ impl CompletionsMenu {
workspace: Option<WeakView<Workspace>>, workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> AnyElement { ) -> AnyElement {
let completions = self.completions.borrow_mut();
let show_completion_documentation = self.show_completion_documentation; let show_completion_documentation = self.show_completion_documentation;
let widest_completion_ix = self let widest_completion_ix = self
.matches .matches
.iter() .iter()
.enumerate() .enumerate()
.max_by_key(|(_, mat)| { .max_by_key(|(_, mat)| {
let completions = self.completions.read();
let completion = &completions[mat.candidate_id]; let completion = &completions[mat.candidate_id];
let documentation = &completion.documentation; let documentation = &completion.documentation;
@@ -350,12 +355,14 @@ impl CompletionsMenu {
}) })
.map(|(ix, _)| ix); .map(|(ix, _)| ix);
let completions = self.completions.clone();
let matches = self.matches.clone();
let selected_item = self.selected_item; let selected_item = self.selected_item;
let style = style.clone(); let style = style.clone();
let multiline_docs = if show_completion_documentation { let multiline_docs = if show_completion_documentation {
let mat = &self.matches[selected_item]; let mat = &self.matches[selected_item];
match &completions[mat.candidate_id].documentation { match &self.completions.read()[mat.candidate_id].documentation {
Some(Documentation::MultiLinePlainText(text)) => { Some(Documentation::MultiLinePlainText(text)) => {
Some(div().child(SharedString::from(text.clone()))) Some(div().child(SharedString::from(text.clone())))
} }
@@ -399,16 +406,13 @@ impl CompletionsMenu {
.occlude() .occlude()
}); });
drop(completions);
let completions = self.completions.clone();
let matches = self.matches.clone();
let list = uniform_list( let list = uniform_list(
cx.view().clone(), cx.view().clone(),
"completions", "completions",
matches.len(), matches.len(),
move |_editor, range, cx| { move |_editor, range, cx| {
let start_ix = range.start; let start_ix = range.start;
let completions_guard = completions.borrow_mut(); let completions_guard = completions.read();
matches[range] matches[range]
.iter() .iter()
@@ -424,14 +428,8 @@ impl CompletionsMenu {
&None &None
}; };
let filter_start = completion.label.filter_range.start;
let highlights = gpui::combine_highlights( let highlights = gpui::combine_highlights(
mat.ranges().map(|range| { mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
(
filter_start + range.start..filter_start + range.end,
FontWeight::BOLD.into(),
)
}),
styled_runs_for_code_label(&completion.label, &style.syntax).map( styled_runs_for_code_label(&completion.label, &style.syntax).map(
|(range, mut highlight)| { |(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it // Ignore font weight for syntax highlighting, as we'll use it
@@ -549,7 +547,7 @@ impl CompletionsMenu {
} }
} }
let completions = self.completions.borrow_mut(); let completions = self.completions.read();
if self.sort_completions { if self.sort_completions {
matches.sort_unstable_by_key(|mat| { matches.sort_unstable_by_key(|mat| {
// We do want to strike a balance here between what the language server tells us // We do want to strike a balance here between what the language server tells us
@@ -601,6 +599,14 @@ impl CompletionsMenu {
} }
}); });
} }
for mat in &mut matches {
let completion = &completions[mat.candidate_id];
mat.string.clone_from(&completion.label.text);
for position in &mut mat.positions {
*position += completion.label.filter_range.start;
}
}
drop(completions); drop(completions);
self.matches = matches.into(); self.matches = matches.into();
@@ -612,13 +618,13 @@ impl CompletionsMenu {
pub struct AvailableCodeAction { pub struct AvailableCodeAction {
pub excerpt_id: ExcerptId, pub excerpt_id: ExcerptId,
pub action: CodeAction, pub action: CodeAction,
pub provider: Rc<dyn CodeActionProvider>, pub provider: Arc<dyn CodeActionProvider>,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct CodeActionContents { pub struct CodeActionContents {
pub tasks: Option<Rc<ResolvedTasks>>, pub tasks: Option<Arc<ResolvedTasks>>,
pub actions: Option<Rc<[AvailableCodeAction]>>, pub actions: Option<Arc<[AvailableCodeAction]>>,
} }
impl CodeActionContents { impl CodeActionContents {
@@ -703,7 +709,7 @@ pub enum CodeActionsItem {
CodeAction { CodeAction {
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
action: CodeAction, action: CodeAction,
provider: Rc<dyn CodeActionProvider>, provider: Arc<dyn CodeActionProvider>,
}, },
} }

View File

@@ -269,7 +269,7 @@ impl DisplayMap {
let start = buffer_snapshot.anchor_before(range.start); let start = buffer_snapshot.anchor_before(range.start);
let end = buffer_snapshot.anchor_after(range.end); let end = buffer_snapshot.anchor_after(range.end);
BlockProperties { BlockProperties {
placement: BlockPlacement::Replace(start..=end), placement: BlockPlacement::Replace(start..end),
render, render,
height, height,
style, style,
@@ -336,38 +336,6 @@ impl DisplayMap {
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive); block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
} }
pub fn fold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.fold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub fn unfold_buffer(&mut self, buffer_id: language::BufferId, cx: &mut ModelContext<Self>) {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let mut block_map = self.block_map.write(snapshot, edits);
block_map.unfold_buffer(buffer_id, self.buffer.read(cx), cx)
}
pub(crate) fn buffer_folded(&self, buffer_id: language::BufferId) -> bool {
self.block_map.folded_buffers.contains(&buffer_id)
}
pub fn insert_creases( pub fn insert_creases(
&mut self, &mut self,
creases: impl IntoIterator<Item = Crease<Anchor>>, creases: impl IntoIterator<Item = Crease<Anchor>>,
@@ -744,11 +712,7 @@ impl DisplaySnapshot {
} }
} }
pub fn next_line_boundary( pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
&self,
mut point: MultiBufferPoint,
) -> (MultiBufferPoint, DisplayPoint) {
let original_point = point;
loop { loop {
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right); let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
@@ -759,7 +723,7 @@ impl DisplaySnapshot {
let mut display_point = self.point_to_display_point(point, Bias::Right); let mut display_point = self.point_to_display_point(point, Bias::Right);
*display_point.column_mut() = self.line_len(display_point.row()); *display_point.column_mut() = self.line_len(display_point.row());
let next_point = self.display_point_to_point(display_point, Bias::Right); let next_point = self.display_point_to_point(display_point, Bias::Right);
if next_point == point || original_point == point || original_point == next_point { if next_point == point {
return (point, display_point); return (point, display_point);
} }
point = next_point; point = next_point;
@@ -1117,6 +1081,10 @@ impl DisplaySnapshot {
|| self.fold_snapshot.is_line_folded(buffer_row) || self.fold_snapshot.is_line_folded(buffer_row)
} }
pub fn is_line_replaced(&self, buffer_row: MultiBufferRow) -> bool {
self.block_snapshot.is_line_replaced(buffer_row)
}
pub fn is_block_line(&self, display_row: DisplayRow) -> bool { pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
self.block_snapshot.is_block_line(BlockRow(display_row.0)) self.block_snapshot.is_block_line(BlockRow(display_row.0))
} }
@@ -2263,7 +2231,7 @@ pub mod tests {
[BlockProperties { [BlockProperties {
placement: BlockPlacement::Replace( placement: BlockPlacement::Replace(
buffer_snapshot.anchor_before(Point::new(1, 2)) buffer_snapshot.anchor_before(Point::new(1, 2))
..=buffer_snapshot.anchor_after(Point::new(2, 3)), ..buffer_snapshot.anchor_after(Point::new(2, 3)),
), ),
height: 4, height: 4,
style: BlockStyle::Fixed, style: BlockStyle::Fixed,

File diff suppressed because it is too large Load Diff

View File

@@ -127,6 +127,7 @@ pub use multi_buffer::{
use multi_buffer::{ use multi_buffer::{
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
}; };
use parking_lot::RwLock;
use project::{ use project::{
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle}, lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
@@ -605,7 +606,7 @@ pub struct Editor {
scrollbar_marker_state: ScrollbarMarkerState, scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState, active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>, nav_history: Option<ItemNavHistory>,
context_menu: RefCell<Option<CodeContextMenu>>, context_menu: RwLock<Option<CodeContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>, mouse_context_menu: Option<MouseContextMenu>,
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>, hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>, completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
@@ -613,7 +614,7 @@ pub struct Editor {
auto_signature_help: Option<bool>, auto_signature_help: Option<bool>,
find_all_references_task_sources: Vec<Anchor>, find_all_references_task_sources: Vec<Anchor>,
next_completion_id: CompletionId, next_completion_id: CompletionId,
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>,
code_actions_task: Option<Task<Result<()>>>, 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<()>>>,
@@ -634,7 +635,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<Rc<dyn CodeActionProvider>>, code_action_providers: Vec<Arc<dyn CodeActionProvider>>,
active_inline_completion: Option<InlineCompletionState>, active_inline_completion: Option<InlineCompletionState>,
// 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.
@@ -677,7 +678,6 @@ pub struct Editor {
next_scroll_position: NextScrollCursorCenterTopBottom, next_scroll_position: NextScrollCursorCenterTopBottom,
addons: HashMap<TypeId, Box<dyn Addon>>, addons: HashMap<TypeId, Box<dyn Addon>>,
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>, registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
toggle_fold_multiple_buffers: Task<()>,
_scroll_cursor_center_top_bottom_task: Task<()>, _scroll_cursor_center_top_bottom_task: Task<()>,
} }
@@ -1191,7 +1191,7 @@ impl Editor {
let mut code_action_providers = Vec::new(); let mut code_action_providers = Vec::new();
if let Some(project) = project.clone() { if let Some(project) = project.clone() {
get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx); get_unstaged_changes_for_buffers(&project, buffer.read(cx).all_buffers(), cx);
code_action_providers.push(Rc::new(project) as Rc<_>); code_action_providers.push(Arc::new(project) as Arc<_>);
} }
let mut this = Self { let mut this = Self {
@@ -1237,7 +1237,7 @@ impl Editor {
scrollbar_marker_state: ScrollbarMarkerState::default(), scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(), active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None, nav_history: None,
context_menu: RefCell::new(None), context_menu: RwLock::new(None),
mouse_context_menu: None, mouse_context_menu: None,
hunk_controls_menu_handle: PopoverMenuHandle::default(), hunk_controls_menu_handle: PopoverMenuHandle::default(),
completion_tasks: Default::default(), completion_tasks: Default::default(),
@@ -1325,7 +1325,6 @@ impl Editor {
addons: HashMap::default(), addons: HashMap::default(),
registered_buffers: HashMap::default(), registered_buffers: HashMap::default(),
_scroll_cursor_center_top_bottom_task: Task::ready(()), _scroll_cursor_center_top_bottom_task: Task::ready(()),
toggle_fold_multiple_buffers: Task::ready(()),
text_style_refinement: None, text_style_refinement: None,
}; };
this.tasks_update_task = Some(this.refresh_runnables(cx)); this.tasks_update_task = Some(this.refresh_runnables(cx));
@@ -1383,7 +1382,7 @@ impl Editor {
key_context.add("renaming"); key_context.add("renaming");
} }
if self.context_menu_visible() { if self.context_menu_visible() {
match self.context_menu.borrow().as_ref() { match self.context_menu.read().as_ref() {
Some(CodeContextMenu::Completions(_)) => { Some(CodeContextMenu::Completions(_)) => {
key_context.add("menu"); key_context.add("menu");
key_context.add("showing_completions") key_context.add("showing_completions")
@@ -1885,9 +1884,10 @@ impl Editor {
if local { if local {
let new_cursor_position = self.selections.newest_anchor().head(); let new_cursor_position = self.selections.newest_anchor().head();
let mut context_menu = self.context_menu.borrow_mut(); let mut context_menu = self.context_menu.write();
let completion_menu = match context_menu.as_ref() { let completion_menu = match context_menu.as_ref() {
Some(CodeContextMenu::Completions(menu)) => Some(menu), Some(CodeContextMenu::Completions(menu)) => Some(menu),
_ => { _ => {
*context_menu = None; *context_menu = None;
None None
@@ -1911,7 +1911,7 @@ impl Editor {
.await; .await;
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
let mut context_menu = this.context_menu.borrow_mut(); let mut context_menu = this.context_menu.write();
let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref()
else { else {
return; return;
@@ -2429,7 +2429,7 @@ impl Editor {
cx.notify(); cx.notify();
return; return;
} }
if self.dismiss_menus_and_popups(false, true, cx) { if self.dismiss_menus_and_popups(true, cx) {
return; return;
} }
@@ -2444,7 +2444,6 @@ impl Editor {
pub fn dismiss_menus_and_popups( pub fn dismiss_menus_and_popups(
&mut self, &mut self,
keep_inline_completion: bool,
should_report_inline_completion_event: bool, should_report_inline_completion_event: bool,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> bool { ) -> bool {
@@ -2468,9 +2467,7 @@ impl Editor {
return true; return true;
} }
if !keep_inline_completion if self.discard_inline_completion(should_report_inline_completion_event, cx) {
&& self.discard_inline_completion(should_report_inline_completion_event, cx)
{
return true; return true;
} }
@@ -2843,6 +2840,7 @@ impl Editor {
); );
} }
let had_active_inline_completion = this.has_active_inline_completion();
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| { this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
s.select(new_selections) s.select(new_selections)
}); });
@@ -2863,7 +2861,8 @@ impl Editor {
this.show_signature_help(&ShowSignatureHelp, cx); this.show_signature_help(&ShowSignatureHelp, cx);
} }
this.trigger_completion_on_input(&text, true, cx); let trigger_in_words = !had_active_inline_completion;
this.trigger_completion_on_input(&text, trigger_in_words, cx);
linked_editing_ranges::refresh_linked_ranges(this, cx); linked_editing_ranges::refresh_linked_ranges(this, cx);
this.refresh_inline_completion(true, false, cx); this.refresh_inline_completion(true, false, cx);
}); });
@@ -3650,7 +3649,7 @@ impl Editor {
return; return;
}; };
if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { if !self.snippet_stack.is_empty() && self.context_menu.read().as_ref().is_some() {
return; return;
} }
@@ -3669,7 +3668,7 @@ impl Editor {
let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); let query = Self::completion_query(&self.buffer.read(cx).read(cx), position);
let aside_was_displayed = match self.context_menu.borrow().deref() { let aside_was_displayed = match self.context_menu.read().deref() {
Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(), Some(CodeContextMenu::Completions(menu)) => menu.aside_was_displayed.get(),
_ => false, _ => false,
}; };
@@ -3722,14 +3721,16 @@ impl Editor {
}; };
editor.update(&mut cx, |editor, cx| { editor.update(&mut cx, |editor, cx| {
let mut context_menu = editor.context_menu.borrow_mut(); let mut context_menu = editor.context_menu.write();
match context_menu.as_ref() { match context_menu.as_ref() {
None => {} None => {}
Some(CodeContextMenu::Completions(prev_menu)) => { Some(CodeContextMenu::Completions(prev_menu)) => {
if prev_menu.id > id { if prev_menu.id > id {
return; return;
} }
} }
_ => return, _ => return,
} }
@@ -3792,7 +3793,7 @@ impl Editor {
.matches .matches
.get(item_ix.unwrap_or(completions_menu.selected_item))?; .get(item_ix.unwrap_or(completions_menu.selected_item))?;
let buffer_handle = completions_menu.buffer; let buffer_handle = completions_menu.buffer;
let completions = completions_menu.completions.borrow_mut(); let completions = completions_menu.completions.read();
let completion = completions.get(mat.candidate_id)?; let completion = completions.get(mat.candidate_id)?;
cx.stop_propagation(); cx.stop_propagation();
@@ -3958,7 +3959,7 @@ impl Editor {
} }
pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) { pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
let mut context_menu = self.context_menu.borrow_mut(); let mut context_menu = self.context_menu.write();
if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
if code_actions.deployed_from_indicator == action.deployed_from_indicator { if code_actions.deployed_from_indicator == action.deployed_from_indicator {
// Toggle if we're selecting the same one // Toggle if we're selecting the same one
@@ -4038,7 +4039,7 @@ impl Editor {
}; };
let resolved_tasks = let resolved_tasks =
tasks.zip(task_context).map(|(tasks, task_context)| { tasks.zip(task_context).map(|(tasks, task_context)| {
Rc::new(ResolvedTasks { Arc::new(ResolvedTasks {
templates: tasks.resolve(&task_context).collect(), templates: tasks.resolve(&task_context).collect(),
position: snapshot.buffer_snapshot.anchor_before(Point::new( position: snapshot.buffer_snapshot.anchor_before(Point::new(
multibuffer_point.row, multibuffer_point.row,
@@ -4053,7 +4054,7 @@ impl Editor {
.as_ref() .as_ref()
.map_or(true, |actions| actions.is_empty()); .map_or(true, |actions| actions.is_empty());
if let Ok(task) = editor.update(&mut cx, |editor, cx| { if let Ok(task) = editor.update(&mut cx, |editor, cx| {
*editor.context_menu.borrow_mut() = *editor.context_menu.write() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu { Some(CodeContextMenu::CodeActions(CodeActionsMenu {
buffer, buffer,
actions: CodeActionContents { actions: CodeActionContents {
@@ -4238,7 +4239,7 @@ impl Editor {
pub fn push_code_action_provider( pub fn push_code_action_provider(
&mut self, &mut self,
provider: Rc<dyn CodeActionProvider>, provider: Arc<dyn CodeActionProvider>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.code_action_providers.push(provider); self.code_action_providers.push(provider);
@@ -5001,7 +5002,7 @@ impl Editor {
pub fn context_menu_visible(&self) -> bool { pub fn context_menu_visible(&self) -> bool {
self.context_menu self.context_menu
.borrow() .read()
.as_ref() .as_ref()
.map_or(false, |menu| menu.visible()) .map_or(false, |menu| menu.visible())
} }
@@ -5013,7 +5014,7 @@ impl Editor {
max_height: Pixels, max_height: Pixels,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Option<(ContextMenuOrigin, AnyElement)> { ) -> Option<(ContextMenuOrigin, AnyElement)> {
self.context_menu.borrow().as_ref().map(|menu| { self.context_menu.read().as_ref().map(|menu| {
menu.render( menu.render(
cursor_position, cursor_position,
style, style,
@@ -5027,7 +5028,7 @@ impl Editor {
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> { fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
cx.notify(); cx.notify();
self.completion_tasks.clear(); self.completion_tasks.clear();
self.context_menu.borrow_mut().take() self.context_menu.write().take()
} }
fn show_snippet_choices( fn show_snippet_choices(
@@ -5044,7 +5045,7 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id); let id = post_inc(&mut self.next_completion_id);
if let Some(buffer) = buffer { if let Some(buffer) = buffer {
*self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( *self.context_menu.write() = Some(CodeContextMenu::Completions(
CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer), CompletionsMenu::new_snippet_choices(id, true, choices, selection, buffer),
)); ));
} }
@@ -7098,7 +7099,7 @@ impl Editor {
if self if self
.context_menu .context_menu
.borrow_mut() .write()
.as_mut() .as_mut()
.map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
.unwrap_or(false) .unwrap_or(false)
@@ -7207,7 +7208,7 @@ impl Editor {
if self if self
.context_menu .context_menu
.borrow_mut() .write()
.as_mut() .as_mut()
.map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
.unwrap_or(false) .unwrap_or(false)
@@ -7260,25 +7261,25 @@ impl Editor {
} }
pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) { pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_first(self.completion_provider.as_deref(), cx); context_menu.select_first(self.completion_provider.as_deref(), cx);
} }
} }
pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) { pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_prev(self.completion_provider.as_deref(), cx); context_menu.select_prev(self.completion_provider.as_deref(), cx);
} }
} }
pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) { pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_next(self.completion_provider.as_deref(), cx); context_menu.select_next(self.completion_provider.as_deref(), cx);
} }
} }
pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) { pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { if let Some(context_menu) = self.context_menu.write().as_mut() {
context_menu.select_last(self.completion_provider.as_deref(), cx); context_menu.select_last(self.completion_provider.as_deref(), cx);
} }
} }
@@ -9501,7 +9502,7 @@ impl Editor {
let location_tasks = definitions let location_tasks = definitions
.into_iter() .into_iter()
.map(|definition| match definition { .map(|definition| match definition {
HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), HoverLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
HoverLink::InlayHint(lsp_location, server_id) => { HoverLink::InlayHint(lsp_location, server_id) => {
editor.compute_target_location(lsp_location, server_id, cx) editor.compute_target_location(lsp_location, server_id, cx)
} }
@@ -9543,7 +9544,7 @@ impl Editor {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<anyhow::Result<Option<Location>>> { ) -> Task<anyhow::Result<Option<Location>>> {
let Some(project) = self.project.clone() else { let Some(project) = self.project.clone() else {
return Task::ready(Ok(None)); return Task::Ready(Some(Ok(None)));
}; };
cx.spawn(move |editor, mut cx| async move { cx.spawn(move |editor, mut cx| async move {
@@ -10310,53 +10311,22 @@ impl Editor {
} }
pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) { pub fn toggle_fold(&mut self, _: &actions::ToggleFold, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) { let selection = self.selections.newest::<Point>(cx);
let selection = self.selections.newest::<Point>(cx);
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let range = if selection.is_empty() { let range = if selection.is_empty() {
let point = selection.head().to_display_point(&display_map); let point = selection.head().to_display_point(&display_map);
let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); let start = DisplayPoint::new(point.row(), 0).to_point(&display_map);
let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) let end = DisplayPoint::new(point.row(), display_map.line_len(point.row()))
.to_point(&display_map); .to_point(&display_map);
start..end start..end
} else {
selection.range()
};
if display_map.folds_in_range(range).next().is_some() {
self.unfold_lines(&Default::default(), cx)
} else {
self.fold(&Default::default(), cx)
}
} else { } else {
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx); selection.range()
let mut toggled_buffers = HashSet::default(); };
for selection in selections { if display_map.folds_in_range(range).next().is_some() {
if let Some(buffer_id) = display_snapshot self.unfold_lines(&Default::default(), cx)
.display_point_to_anchor(selection.head(), Bias::Right) } else {
.buffer_id self.fold(&Default::default(), cx)
{
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
}
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if toggled_buffers.insert(buffer_id) {
if self.buffer_folded(buffer_id, cx) {
self.unfold_buffer(buffer_id, cx);
} else {
self.fold_buffer(buffer_id, cx);
}
}
}
}
} }
} }
@@ -10385,68 +10355,44 @@ impl Editor {
} }
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) { pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) { let mut to_fold = Vec::new();
let mut to_fold = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all_adjusted(cx);
let selections = self.selections.all_adjusted(cx);
for selection in selections { for selection in selections {
let range = selection.range().sorted(); let range = selection.range().sorted();
let buffer_start_row = range.start.row; let buffer_start_row = range.start.row;
if range.start.row != range.end.row { if range.start.row != range.end.row {
let mut found = false; let mut found = false;
let mut row = range.start.row; let mut row = range.start.row;
while row <= range.end.row { while row <= range.end.row {
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
{ found = true;
found = true; row = crease.range().end.row + 1;
row = crease.range().end.row + 1; to_fold.push(crease);
to_fold.push(crease); } else {
} else { row += 1
row += 1
}
}
if found {
continue;
} }
} }
if found {
for row in (0..=range.start.row).rev() { continue;
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
if crease.range().end.row >= buffer_start_row {
to_fold.push(crease);
if row <= range.start.row {
break;
}
}
}
} }
} }
self.fold_creases(to_fold, true, cx); for row in (0..=range.start.row).rev() {
} else { if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx); if crease.range().end.row >= buffer_start_row {
let mut folded_buffers = HashSet::default(); to_fold.push(crease);
for selection in selections { if row <= range.start.row {
if let Some(buffer_id) = display_snapshot break;
.display_point_to_anchor(selection.head(), Bias::Right) }
.buffer_id
{
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if folded_buffers.insert(buffer_id) {
self.fold_buffer(buffer_id, cx);
} }
} }
} }
} }
self.fold_creases(to_fold, true, cx);
} }
fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) { fn fold_at_level(&mut self, fold_at: &FoldAtLevel, cx: &mut ViewContext<Self>) {
@@ -10486,30 +10432,22 @@ impl Editor {
} }
pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) { pub fn fold_all(&mut self, _: &actions::FoldAll, cx: &mut ViewContext<Self>) {
if self.buffer.read(cx).is_singleton() { if !self.buffer.read(cx).is_singleton() {
let mut fold_ranges = Vec::new(); return;
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
self.fold_creases(fold_ranges, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.fold_buffer(buffer_id, cx);
}
})
.ok();
});
} }
let mut fold_ranges = Vec::new();
let snapshot = self.buffer.read(cx).snapshot(cx);
for row in 0..snapshot.max_row().0 {
if let Some(foldable_range) =
self.snapshot(cx).crease_for_buffer_row(MultiBufferRow(row))
{
fold_ranges.push(foldable_range);
}
}
self.fold_creases(fold_ranges, true, cx);
} }
pub fn fold_function_bodies( pub fn fold_function_bodies(
@@ -10581,45 +10519,22 @@ impl Editor {
} }
pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) { pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
if self.is_singleton(cx) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot;
let buffer = &display_map.buffer_snapshot; let selections = self.selections.all::<Point>(cx);
let selections = self.selections.all::<Point>(cx); let ranges = selections
let ranges = selections .iter()
.iter() .map(|s| {
.map(|s| { let range = s.display_range(&display_map).sorted();
let range = s.display_range(&display_map).sorted(); let mut start = range.start.to_point(&display_map);
let mut start = range.start.to_point(&display_map); let mut end = range.end.to_point(&display_map);
let mut end = range.end.to_point(&display_map); start.column = 0;
start.column = 0; end.column = buffer.line_len(MultiBufferRow(end.row));
end.column = buffer.line_len(MultiBufferRow(end.row)); start..end
start..end })
}) .collect::<Vec<_>>();
.collect::<Vec<_>>();
self.unfold_ranges(&ranges, true, true, cx); self.unfold_ranges(&ranges, true, true, cx);
} else {
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
let mut unfolded_buffers = HashSet::default();
for selection in selections {
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.head(), Bias::Right)
.buffer_id
{
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
}
if let Some(buffer_id) = display_snapshot
.display_point_to_anchor(selection.tail(), Bias::Left)
.buffer_id
{
if unfolded_buffers.insert(buffer_id) {
self.unfold_buffer(buffer_id, cx);
}
}
}
}
} }
pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) { pub fn unfold_recursive(&mut self, _: &UnfoldRecursive, cx: &mut ViewContext<Self>) {
@@ -10659,20 +10574,8 @@ impl Editor {
} }
pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) { pub fn unfold_all(&mut self, _: &actions::UnfoldAll, cx: &mut ViewContext<Self>) {
if self.buffer.read(cx).is_singleton() { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx);
} else {
self.toggle_fold_multiple_buffers = cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() {
editor.unfold_buffer(buffer_id, cx);
}
})
.ok();
});
}
} }
pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
@@ -10759,45 +10662,6 @@ impl Editor {
}); });
} }
pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map
.update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx));
cx.emit(EditorEvent::BufferFoldToggled {
ids: folded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: true,
});
cx.notify();
}
pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut ViewContext<Self>) {
if self.buffer().read(cx).is_singleton() || !self.buffer_folded(buffer_id, cx) {
return;
}
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
return;
};
let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx);
self.display_map.update(cx, |display_map, cx| {
display_map.unfold_buffer(buffer_id, cx);
});
cx.emit(EditorEvent::BufferFoldToggled {
ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(),
folded: false,
});
cx.notify();
}
pub fn buffer_folded(&self, buffer: BufferId, cx: &AppContext) -> bool {
self.display_map.read(cx).buffer_folded(buffer)
}
/// Removes any folds with the given ranges. /// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset + Clone>( pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self, &mut self,
@@ -12815,7 +12679,7 @@ impl Editor {
} }
pub fn has_active_completions_menu(&self) -> bool { pub fn has_active_completions_menu(&self) -> bool {
self.context_menu.borrow().as_ref().map_or(false, |menu| { self.context_menu.read().as_ref().map_or(false, |menu| {
menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) menu.visible() && matches!(menu, CodeContextMenu::Completions(_))
}) })
} }
@@ -13311,7 +13175,7 @@ pub trait CompletionProvider {
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
completion_indices: Vec<usize>, completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>>; ) -> Task<Result<bool>>;
@@ -13429,7 +13293,7 @@ fn snippet_completions(
snippet snippet
.prefix .prefix
.iter() .iter()
.map(move |prefix| StringMatchCandidate::new(ix, &prefix)) .map(move |prefix| StringMatchCandidate::new(ix, prefix.clone()))
}) })
.collect::<Vec<StringMatchCandidate>>(); .collect::<Vec<StringMatchCandidate>>();
@@ -13541,7 +13405,7 @@ impl CompletionProvider for Model<Project> {
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
completion_indices: Vec<usize>, completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
self.update(cx, |project, cx| { self.update(cx, |project, cx| {
@@ -13956,10 +13820,6 @@ pub enum EditorEvent {
ExcerptsRemoved { ExcerptsRemoved {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },
BufferFoldToggled {
ids: Vec<ExcerptId>,
folded: bool,
},
ExcerptsEdited { ExcerptsEdited {
ids: Vec<ExcerptId>, ids: Vec<ExcerptId>,
}, },

View File

@@ -11,7 +11,6 @@ pub struct EditorSettings {
pub current_line_highlight: CurrentLineHighlight, pub current_line_highlight: CurrentLineHighlight,
pub lsp_highlight_debounce: u64, pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool, pub hover_popover_enabled: bool,
pub hover_popover_delay: u64,
pub toolbar: Toolbar, pub toolbar: Toolbar,
pub scrollbar: Scrollbar, pub scrollbar: Scrollbar,
pub gutter: Gutter, pub gutter: Gutter,
@@ -197,10 +196,7 @@ pub struct EditorSettingsContent {
/// ///
/// Default: true /// Default: true
pub hover_popover_enabled: Option<bool>, pub hover_popover_enabled: Option<bool>,
/// Time to wait before showing the informational hover box
///
/// Default: 350
pub hover_popover_delay: Option<u64>,
/// Toolbar related settings /// Toolbar related settings
pub toolbar: Option<ToolbarContent>, pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings /// Scrollbar related settings

View File

@@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
let snapshot = editor.snapshot(cx); let snapshot = editor.snapshot(cx);
let snapshot = &snapshot.buffer_snapshot; let snapshot = &snapshot.buffer_snapshot;
let placement = BlockPlacement::Replace( let placement = BlockPlacement::Replace(
snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)), snapshot.anchor_after(Point::new(1, 0))..snapshot.anchor_after(Point::new(3, 0)),
); );
editor.insert_blocks( editor.insert_blocks(
[BlockProperties { [BlockProperties {
@@ -8342,9 +8342,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
additional edit additional edit
"}); "});
cx.simulate_keystroke(" "); cx.simulate_keystroke(" ");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("s"); cx.simulate_keystroke("s");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
one.second_completion one.second_completion
@@ -8406,12 +8406,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
}); });
cx.set_state("editorˇ"); cx.set_state("editorˇ");
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.simulate_keystroke("c"); cx.simulate_keystroke("c");
cx.simulate_keystroke("l"); cx.simulate_keystroke("l");
cx.simulate_keystroke("o"); cx.simulate_keystroke("o");
cx.assert_editor_state("editor.cloˇ"); cx.assert_editor_state("editor.cloˇ");
assert!(cx.editor(|e, _| e.context_menu.borrow_mut().is_none())); assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, cx); editor.show_completions(&ShowCompletions { trigger: None }, cx);
}); });
@@ -8468,8 +8468,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert_eq!( assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(), menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["first", "last"] &["first", "last"]
@@ -8481,8 +8480,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.move_page_down(&MovePageDown::default(), cx); editor.move_page_down(&MovePageDown::default(), cx);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert!( assert!(
menu.selected_item == 1, menu.selected_item == 1,
"expected PageDown to select the last item from the context menu" "expected PageDown to select the last item from the context menu"
@@ -8494,8 +8492,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
editor.move_page_up(&MovePageUp::default(), cx); editor.move_page_up(&MovePageUp::default(), cx);
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert!( assert!(
menu.selected_item == 0, menu.selected_item == 0,
"expected PageUp to select the first item from the context menu" "expected PageUp to select the first item from the context menu"
@@ -8563,8 +8560,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert_eq!( assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(), menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["r", "ret", "Range", "return"] &["r", "ret", "Range", "return"]
@@ -10652,9 +10648,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
} }
#[gpui::test] #[gpui::test]
async fn test_completions_resolve_updates_labels_if_filter_text_matches( async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext) {
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
@@ -10673,34 +10667,20 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"}); cx.set_state(indoc! {"fn main() { let a = 2ˇ; }"});
cx.simulate_keystroke("."); cx.simulate_keystroke(".");
let item1 = lsp::CompletionItem { let completion_item = lsp::CompletionItem {
label: "id".to_string(), label: "unresolved".to_string(),
filter_text: Some("id".to_string()),
detail: None, detail: None,
documentation: None, documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(), new_text: ".unresolved".to_string(),
})),
..lsp::CompletionItem::default()
};
let item2 = lsp::CompletionItem {
label: "other".to_string(),
filter_text: Some("other".to_string()),
detail: None,
documentation: None,
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".other".to_string(),
})), })),
..lsp::CompletionItem::default() ..lsp::CompletionItem::default()
}; };
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| { cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
let item1 = item1.clone(); let item = completion_item.clone();
let item2 = item2.clone(); async move { Ok(Some(lsp::CompletionResponse::Array(vec![item]))) }
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
}) })
.next() .next()
.await; .await;
@@ -10708,20 +10688,15 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.condition(|editor, _| editor.context_menu_visible()) cx.condition(|editor, _| editor.context_menu_visible())
.await; .await;
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut(); let context_menu = editor.context_menu.read();
let context_menu = context_menu let context_menu = context_menu
.as_ref() .as_ref()
.expect("Should have the context menu deployed"); .expect("Should have the context menu deployed");
match context_menu { match context_menu {
CodeContextMenu::Completions(completions_menu) => { CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut(); let completions = completions_menu.completions.read();
assert_eq!( assert_eq!(completions.len(), 1, "Should have one completion");
completions assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
.iter()
.map(|completion| &completion.label.text)
.collect::<Vec<_>>(),
vec!["id", "other"]
)
} }
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"), CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
} }
@@ -10729,33 +10704,12 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move { cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem { Ok(lsp::CompletionItem {
label: "method id()".to_string(), label: "resolved".to_string(),
filter_text: Some("id".to_string()),
detail: Some("Now resolved!".to_string()), detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())), documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)), range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(), new_text: ".resolved".to_string(),
})),
..lsp::CompletionItem::default()
})
})
.next()
.await;
cx.run_until_parked();
cx.update_editor(|editor, cx| {
editor.context_menu_next(&Default::default(), cx);
});
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| async move {
Ok(lsp::CompletionItem {
label: "invalid changed label".to_string(),
detail: Some("Now resolved!".to_string()),
documentation: Some(lsp::Documentation::String("Docs".to_string())),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
new_text: ".id".to_string(),
})), })),
..lsp::CompletionItem::default() ..lsp::CompletionItem::default()
}) })
@@ -10765,20 +10719,18 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
cx.run_until_parked(); cx.run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
let context_menu = editor.context_menu.borrow_mut(); let context_menu = editor.context_menu.read();
let context_menu = context_menu let context_menu = context_menu
.as_ref() .as_ref()
.expect("Should have the context menu deployed"); .expect("Should have the context menu deployed");
match context_menu { match context_menu {
CodeContextMenu::Completions(completions_menu) => { CodeContextMenu::Completions(completions_menu) => {
let completions = completions_menu.completions.borrow_mut(); let completions = completions_menu.completions.read();
assert_eq!(completions.len(), 1, "Should have one completion");
assert_eq!( assert_eq!(
completions completions.get(0).unwrap().label.text,
.iter() "resolved",
.map(|completion| &completion.label.text) "Should update the completion label after resolving"
.collect::<Vec<_>>(),
vec!["method id()", "other"],
"Should update first completion label, but not second as the filter text did not match."
); );
} }
CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"), CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
@@ -10958,7 +10910,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
.await; .await;
cx.run_until_parked(); cx.run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
let menu = editor.context_menu.borrow_mut(); let menu = editor.context_menu.read();
match menu.as_ref().expect("should have the completions menu") { match menu.as_ref().expect("should have the completions menu") {
CodeContextMenu::Completions(completions_menu) => { CodeContextMenu::Completions(completions_menu) => {
assert_eq!( assert_eq!(
@@ -11063,8 +11015,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("-"); cx.simulate_keystroke("-");
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert_eq!( assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(), menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-red", "bg-blue", "bg-yellow"] &["bg-red", "bg-blue", "bg-yellow"]
@@ -11077,8 +11028,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l"); cx.simulate_keystroke("l");
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert_eq!( assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(), menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-blue", "bg-yellow"] &["bg-blue", "bg-yellow"]
@@ -11094,8 +11044,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
cx.simulate_keystroke("l"); cx.simulate_keystroke("l");
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.update_editor(|editor, _| { cx.update_editor(|editor, _| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
{
assert_eq!( assert_eq!(
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(), menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
&["bg-yellow"] &["bg-yellow"]
@@ -13956,412 +13905,6 @@ async fn test_find_enclosing_node_with_task(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_multi_buffer_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
},
ExcerptRange {
context: Point::new(5, 0)..Point::new(7, 0),
primary: None,
},
ExcerptRange {
context: Point::new(9, 0)..Point::new(10, 4),
primary: None,
},
],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n\n1111\n2222\n\n\n\n5555\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
// Emulate selection inside the fold logic, that should work
multi_buffer_editor.update(cx, |editor, cx| {
editor.snapshot(cx).next_line_boundary(Point::new(0, 4));
});
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\naaaa\nbbbb\ncccc\n\n\n\nffff\ngggg\n\n\n\njjjj\n\n\n\n\nllll\nmmmm\nnnnn\n\n\n\nqqqq\nrrrr\n\n\n\nuuuu\n\n\n",
"After unfolding the first buffer, its and 2nd buffer's text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding the all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_single_excerpts_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text_1 = "1111\n2222\n3333".to_string();
let sample_text_2 = "4444\n5555\n6666".to_string();
let sample_text_3 = "7777\n8888\n9999".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"first.rs": sample_text_1,
"second.rs": sample_text_2,
"third.rs": sample_text_3,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "first.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "second.rs"), cx)
})
.await
.unwrap();
let buffer_3 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "third.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer.push_excerpts(
buffer_3.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(3, 0),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let full_text = "\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n";
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n\n\n7777\n8888\n9999\n",
"After folding the first buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n\n\n7777\n8888\n9999\n",
"After folding the second buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n",
"After folding the third buffer, its text should not be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the second buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
"\n\n\n1111\n2222\n3333\n\n\n\n\n4444\n5555\n6666\n\n\n",
"After unfolding the first buffer, its text should be displayed"
);
multi_buffer_editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
});
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
"After unfolding all buffers, all original text should be displayed"
);
}
#[gpui::test]
async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/a",
json!({
"main.rs": sample_text,
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
let worktree = project.update(cx, |project, cx| {
let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
assert_eq!(worktrees.len(), 1);
worktrees.pop().unwrap()
});
let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let multi_buffer = cx.new_model(|cx| {
let mut multi_buffer = MultiBuffer::new(ReadWrite);
multi_buffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)
..Point::new(
sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
0,
),
primary: None,
}],
cx,
);
multi_buffer
});
let multi_buffer_editor = cx.new_view(|cx| {
Editor::new(
EditorMode::Full,
multi_buffer,
Some(project.clone()),
true,
cx,
)
});
let selection_range = Point::new(1, 0)..Point::new(2, 0);
multi_buffer_editor.update(cx, |editor, cx| {
enum TestHighlight {}
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
editor.highlight_text::<TestHighlight>(
vec![highlight_range.clone()],
HighlightStyle::color(Hsla::green()),
cx,
);
editor.change_selections(None, cx, |s| s.select_ranges(Some(highlight_range)));
});
let full_text = format!("\n\n\n{sample_text}\n");
assert_eq!(
multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
full_text,
);
}
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> { fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point point..point

View File

@@ -22,23 +22,23 @@ use crate::{
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown, HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
SoftWrap, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
}; };
use client::ParticipantIndex; use client::ParticipantIndex;
use collections::{BTreeMap, HashMap, HashSet}; use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons;
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid}; use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
use gpui::{ use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg, anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClickEvent, transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
ElementInputHandler, Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext, WeakView, WindowContext,
}; };
use gpui::{ClickEvent, Subscription};
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
language_settings::{ language_settings::{
@@ -49,8 +49,8 @@ use language::{
}; };
use lsp::DiagnosticSeverity; use lsp::DiagnosticSeverity;
use multi_buffer::{ use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
MultiBufferRow, MultiBufferSnapshot, ToOffset, MultiBufferSnapshot, ToOffset,
}; };
use project::{ use project::{
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
@@ -1686,7 +1686,7 @@ impl EditorElement {
deployed_from_indicator, deployed_from_indicator,
actions, actions,
.. ..
})) = editor.context_menu.borrow().as_ref() })) = editor.context_menu.read().as_ref()
{ {
actions actions
.tasks .tasks
@@ -1713,15 +1713,6 @@ impl EditorElement {
} }
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot); let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
let multibuffer_row = MultiBufferRow(multibuffer_point.row); let multibuffer_row = MultiBufferRow(multibuffer_point.row);
let buffer_folded = snapshot
.buffer_snapshot
.buffer_line_for_row(multibuffer_row)
.map(|(buffer_snapshot, _)| buffer_snapshot.remote_id())
.map(|buffer_id| editor.buffer_folded(buffer_id, cx))
.unwrap_or(false);
if buffer_folded {
return None;
}
if snapshot.is_line_folded(multibuffer_row) { if snapshot.is_line_folded(multibuffer_row) {
// Skip folded indicators, unless it's the starting line of a fold. // Skip folded indicators, unless it's the starting line of a fold.
@@ -1777,7 +1768,7 @@ impl EditorElement {
if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu { if let Some(crate::CodeContextMenu::CodeActions(CodeActionsMenu {
deployed_from_indicator, deployed_from_indicator,
.. ..
})) = editor.context_menu.borrow().as_ref() })) = editor.context_menu.read().as_ref()
{ {
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row); active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
}; };
@@ -2096,7 +2087,6 @@ impl EditorElement {
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> (AnyElement, Size<Pixels>) { ) -> (AnyElement, Size<Pixels>) {
let header_padding = px(6.0);
let mut element = match block { let mut element = match block {
Block::Custom(block) => { Block::Custom(block) => {
let block_start = block.start().to_point(&snapshot.buffer_snapshot); let block_start = block.start().to_point(&snapshot.buffer_snapshot);
@@ -2146,58 +2136,21 @@ impl EditorElement {
.into_any() .into_any()
} }
Block::FoldedBuffer {
first_excerpt,
prev_excerpt,
show_excerpt_controls,
height,
..
} => {
let icon_offset = gutter_dimensions.width
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls {
result = result.child(
h_flex()
.w(icon_offset)
.h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height())
.flex_none()
.justify_end()
.child(self.render_expand_excerpt_button(
prev_excerpt.id,
ExpandExcerptDirection::Down,
IconName::ArrowDownFromLine,
cx,
)),
);
}
}
let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx);
result
.child(self.render_buffer_header(
first_excerpt,
header_padding,
true,
jump_data,
cx,
))
.into_any_element()
}
Block::ExcerptBoundary { Block::ExcerptBoundary {
prev_excerpt, prev_excerpt,
next_excerpt, next_excerpt,
show_excerpt_controls, show_excerpt_controls,
height,
starts_new_buffer, starts_new_buffer,
height,
.. ..
} => { } => {
let icon_offset = gutter_dimensions.width let icon_offset = gutter_dimensions.width
- (gutter_dimensions.left_padding + gutter_dimensions.margin); - (gutter_dimensions.left_padding + gutter_dimensions.margin);
let header_padding = px(6.0);
let mut result = v_flex().id(block_id).w_full(); let mut result = v_flex().id(block_id).w_full();
if let Some(prev_excerpt) = prev_excerpt { if let Some(prev_excerpt) = prev_excerpt {
if *show_excerpt_controls { if *show_excerpt_controls {
result = result.child( result = result.child(
@@ -2217,15 +2170,115 @@ impl EditorElement {
} }
if let Some(next_excerpt) = next_excerpt { if let Some(next_excerpt) = next_excerpt {
let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx); let buffer = &next_excerpt.buffer;
let range = &next_excerpt.range;
let jump_data = {
let jump_path =
project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let excerpt_start = range.context.start;
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_row =
language::ToPoint::to_point(&jump_anchor, buffer).row;
jump_position.row - excerpt_start_row
};
let line_offset_from_top =
block_row_start.0 + *height + offset_from_excerpt_start
- snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32;
JumpData {
excerpt_id: next_excerpt.id,
anchor: jump_anchor,
position: language::ToPoint::to_point(&jump_anchor, buffer),
path: jump_path,
line_offset_from_top,
}
};
if *starts_new_buffer { if *starts_new_buffer {
result = result.child(self.render_buffer_header( let include_root = self
next_excerpt, .editor
header_padding, .read(cx)
false, .project
jump_data, .as_ref()
cx, .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
)); .unwrap_or_default();
let path = buffer.resolve_file_path(cx, include_root);
let filename = path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
let parent_path = path.as_ref().and_then(|path| {
Some(path.parent()?.to_string_lossy().to_string() + "/")
});
result = result.child(
div()
.px(header_padding)
.pt(header_padding)
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
.child(
h_flex()
.id("path header block")
.size_full()
.flex_basis(Length::Definite(DefiniteLength::Fraction(
0.667,
)))
.px(gpui::px(12.))
.rounded_md()
.shadow_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_subheader_background)
.justify_between()
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(
h_flex().gap_3().child(
h_flex()
.gap_2()
.child(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.when_some(parent_path, |then, path| {
then.child(div().child(path).text_color(
cx.theme().colors().text_muted,
))
}),
),
)
.child(Icon::new(IconName::ArrowUpRight))
.cursor_pointer()
.tooltip(|cx| {
Tooltip::for_action("Jump to File", &OpenExcerpts, cx)
})
.on_mouse_down(MouseButton::Left, |_, cx| {
cx.stop_propagation()
})
.on_click(cx.listener_for(&self.editor, {
move |editor, e: &ClickEvent, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.down.modifiers.secondary(),
cx,
);
}
})),
),
);
if *show_excerpt_controls { if *show_excerpt_controls {
result = result.child( result = result.child(
h_flex() h_flex()
@@ -2375,137 +2428,6 @@ impl EditorElement {
(element, final_size) (element, final_size)
} }
fn render_buffer_header(
&self,
for_excerpt: &ExcerptInfo,
header_padding: Pixels,
is_folded: bool,
jump_data: JumpData,
cx: &mut WindowContext,
) -> Div {
let include_root = self
.editor
.read(cx)
.project
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
let filename = path
.as_ref()
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
let parent_path = path
.as_ref()
.and_then(|path| Some(path.parent()?.to_string_lossy().to_string() + "/"));
let focus_handle = self.editor.focus_handle(cx);
div()
.px(header_padding)
.pt(header_padding)
.w_full()
.h(FILE_HEADER_HEIGHT as f32 * cx.line_height())
.child(
h_flex()
.size_full()
.gap_2()
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
.pl_0p5()
.pr_4()
.rounded_md()
.shadow_md()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_subheader_background)
.hover(|style| style.bg(cx.theme().colors().element_hover))
.map(|header| {
let editor = self.editor.clone();
let buffer_id = for_excerpt.buffer_id;
let toggle_chevron_icon =
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
header.child(
div()
.hover(|style| style.bg(cx.theme().colors().element_selected))
.rounded_sm()
.child(
ButtonLike::new("toggle-buffer-fold")
.style(ui::ButtonStyle::Transparent)
.size(ButtonSize::Large)
.width(px(30.).into())
.children(toggle_chevron_icon)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Toggle Excerpt Fold",
&ToggleFold,
&focus_handle,
cx,
)
}
})
.on_click(move |_, cx| {
if is_folded {
editor.update(cx, |editor, cx| {
editor.unfold_buffer(buffer_id, cx);
});
} else {
editor.update(cx, |editor, cx| {
editor.fold_buffer(buffer_id, cx);
});
}
}),
),
)
})
.child(
h_flex()
.id("path header block")
.size_full()
.justify_between()
.child(
h_flex()
.gap_2()
.child(
filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into()),
)
.when_some(parent_path, |then, path| {
then.child(
div()
.child(path)
.text_color(cx.theme().colors().text_muted),
)
}),
)
.child(Icon::new(IconName::ArrowUpRight))
.cursor_pointer()
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Jump To File",
&OpenExcerpts,
&focus_handle,
cx,
)
}
})
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.on_click(cx.listener_for(&self.editor, {
move |editor, e: &ClickEvent, cx| {
editor.open_excerpts_common(
Some(jump_data.clone()),
e.down.modifiers.secondary(),
cx,
);
}
})),
),
)
}
fn render_expand_excerpt_button( fn render_expand_excerpt_button(
&self, &self,
excerpt_id: ExcerptId, excerpt_id: ExcerptId,
@@ -2826,7 +2748,7 @@ impl EditorElement {
style: &EditorStyle, style: &EditorStyle,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Option<AnyElement> { ) -> Option<AnyElement> {
const PADDING_X: Pixels = Pixels(24.); const PADDING_X: Pixels = Pixels(25.);
const PADDING_Y: Pixels = Pixels(2.); const PADDING_Y: Pixels = Pixels(2.);
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?; let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
@@ -2929,7 +2851,6 @@ impl EditorElement {
} }
let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx); let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
let line_count = text.lines().count() + 1;
let longest_row = let longest_row =
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
@@ -2947,8 +2868,7 @@ impl EditorElement {
.width .width
}; };
let styled_text = let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
gpui::StyledText::new(text).with_highlights(&style.text, highlights);
let mut element = div() let mut element = div()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
@@ -2956,38 +2876,15 @@ impl EditorElement {
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.rounded_md() .rounded_md()
.px_1() .px_1()
.child(styled_text) .child(text)
.into_any(); .into_any();
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), cx); let origin = text_bounds.origin
let is_fully_visible = + point(
editor_width >= longest_line_width + PADDING_X + element_bounds.width; longest_line_width + PADDING_X - scroll_pixel_position.x,
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
let origin = if is_fully_visible { );
text_bounds.origin element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
+ point(
longest_line_width + PADDING_X - scroll_pixel_position.x,
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
)
} else {
let target_above =
DisplayRow(edit_start.row().0.saturating_sub(line_count as u32));
let row_target = if visible_row_range
.contains(&DisplayRow(target_above.0.saturating_sub(1)))
{
target_above
} else {
DisplayRow(edit_end.row().0 + 1)
};
text_bounds.origin
+ point(
-scroll_pixel_position.x,
row_target.as_f32() * line_height - scroll_pixel_position.y,
)
};
element.prepaint_as_root(origin, element_bounds.into(), cx);
Some(element) Some(element)
} }
} }
@@ -4417,46 +4314,6 @@ impl EditorElement {
} }
} }
fn jump_data(
snapshot: &EditorSnapshot,
block_row_start: DisplayRow,
height: u32,
for_excerpt: &ExcerptInfo,
cx: &mut WindowContext<'_>,
) -> JumpData {
let range = &for_excerpt.range;
let buffer = &for_excerpt.buffer;
let jump_path = project::File::from_dyn(buffer.file()).map(|file| ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
});
let jump_anchor = range
.primary
.as_ref()
.map_or(range.context.start, |primary| primary.start);
let excerpt_start = range.context.start;
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
let offset_from_excerpt_start = if jump_anchor == excerpt_start {
0
} else {
let excerpt_start_row = language::ToPoint::to_point(&jump_anchor, buffer).row;
jump_position.row - excerpt_start_row
};
let line_offset_from_top = block_row_start.0 + height + offset_from_excerpt_start
- snapshot
.scroll_anchor
.scroll_position(&snapshot.display_snapshot)
.y as u32;
JumpData {
excerpt_id: for_excerpt.id,
anchor: jump_anchor,
position: language::ToPoint::to_point(&jump_anchor, buffer),
path: jump_path,
line_offset_from_top,
}
}
fn inline_completion_popover_text( fn inline_completion_popover_text(
editor_snapshot: &EditorSnapshot, editor_snapshot: &EditorSnapshot,
edits: &Vec<(Range<Anchor>, String)>, edits: &Vec<(Range<Anchor>, String)>,
@@ -5900,33 +5757,29 @@ impl Element for EditorElement {
if !expanded_add_hunks_by_rows if !expanded_add_hunks_by_rows
.contains_key(&newest_selection_display_row) .contains_key(&newest_selection_display_row)
{ {
if !snapshot let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
.is_line_folded(MultiBufferRow(newest_selection_point.row)) MultiBufferRow(newest_selection_point.row),
{ );
let buffer = snapshot.buffer_snapshot.buffer_line_for_row( if let Some((buffer, range)) = buffer {
MultiBufferRow(newest_selection_point.row), let buffer_id = buffer.remote_id();
); let row = range.start.row;
if let Some((buffer, range)) = buffer { let has_test_indicator = self
let buffer_id = buffer.remote_id(); .editor
let row = range.start.row; .read(cx)
let has_test_indicator = self .tasks
.editor .contains_key(&(buffer_id, row));
.read(cx)
.tasks
.contains_key(&(buffer_id, row));
if !has_test_indicator { if !has_test_indicator {
code_actions_indicator = self code_actions_indicator = self
.layout_code_actions_indicator( .layout_code_actions_indicator(
line_height, line_height,
newest_selection_head, newest_selection_head,
scroll_pixel_position, scroll_pixel_position,
&gutter_dimensions, &gutter_dimensions,
&gutter_hitbox, &gutter_hitbox,
&rows_with_hunk_bounds, &rows_with_hunk_bounds,
cx, cx,
); );
}
} }
} }
} }

View File

@@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState}; use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
use util::TryFutureExt; use util::TryFutureExt;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
@@ -130,12 +131,10 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
hide_hover(editor, cx); hide_hover(editor, cx);
} }
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| { let task = cx.spawn(|this, mut cx| {
async move { async move {
cx.background_executor() cx.background_executor()
.timer(Duration::from_millis(hover_popover_delay)) .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
.await; .await;
this.update(&mut cx, |this, _| { this.update(&mut cx, |this, _| {
this.hover_state.diagnostic_popover = None; this.hover_state.diagnostic_popover = None;
@@ -237,8 +236,6 @@ fn show_hover(
} }
} }
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
let task = cx.spawn(|this, mut cx| { let task = cx.spawn(|this, mut cx| {
async move { async move {
// If we need to delay, delay a set amount initially before making the lsp request // If we need to delay, delay a set amount initially before making the lsp request
@@ -248,7 +245,7 @@ fn show_hover(
// Construct delay task to wait for later // Construct delay task to wait for later
let total_delay = Some( let total_delay = Some(
cx.background_executor() cx.background_executor()
.timer(Duration::from_millis(hover_popover_delay)), .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
); );
cx.background_executor() cx.background_executor()
@@ -859,7 +856,6 @@ mod tests {
InlayId, PointForPosition, InlayId, PointForPosition,
}; };
use collections::BTreeSet; use collections::BTreeSet;
use gpui::AppContext;
use indoc::indoc; use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId; use lsp::LanguageServerId;
@@ -869,10 +865,6 @@ mod tests {
use std::sync::atomic::AtomicUsize; use std::sync::atomic::AtomicUsize;
use text::Bias; use text::Bias;
fn get_hover_popover_delay(cx: &gpui::TestAppContext) -> u64 {
cx.read(|cx: &AppContext| -> u64 { EditorSettings::get_global(cx).hover_popover_delay })
}
impl InfoPopover { impl InfoPopover {
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String { fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
let mut rendered_text = String::new(); let mut rendered_text = String::new();
@@ -897,6 +889,7 @@ mod tests {
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
init_test(cx, |_| {}); init_test(cx, |_| {});
const HOVER_DELAY_MILLIS: u64 = 350;
let mut cx = EditorLspTestContext::new_rust( let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities { lsp::ServerCapabilities {
@@ -970,7 +963,7 @@ mod tests {
})) }))
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await; requests.next().await;
cx.editor(|editor, cx| { cx.editor(|editor, cx| {
@@ -1049,7 +1042,7 @@ mod tests {
hover_at(editor, Some(anchor), cx) hover_at(editor, Some(anchor), cx)
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await; request.next().await;
// verify that the information popover is no longer visible // verify that the information popover is no longer visible
@@ -1103,7 +1096,7 @@ mod tests {
})) }))
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await; requests.next().await;
cx.editor(|editor, cx| { cx.editor(|editor, cx| {
@@ -1139,7 +1132,7 @@ mod tests {
hover_at(editor, Some(anchor), cx) hover_at(editor, Some(anchor), cx)
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
request.next().await; request.next().await;
cx.editor(|editor, _| { cx.editor(|editor, _| {
assert!(!editor.hover_state.visible()); assert!(!editor.hover_state.visible());
@@ -1401,7 +1394,7 @@ mod tests {
})) }))
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
cx.editor(|Editor { hover_state, .. }, _| { cx.editor(|Editor { hover_state, .. }, _| {
@@ -1689,7 +1682,7 @@ mod tests {
); );
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state; let hover_state = &editor.hover_state;
@@ -1743,7 +1736,7 @@ mod tests {
); );
}); });
cx.background_executor cx.background_executor
.advance_clock(Duration::from_millis(get_hover_popover_delay(&cx) + 100)); .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
cx.background_executor.run_until_parked(); cx.background_executor.run_until_parked();
cx.update_editor(|editor, cx| { cx.update_editor(|editor, cx| {
let hover_state = &editor.hover_state; let hover_state = &editor.hover_state;

View File

@@ -172,7 +172,13 @@ pub fn indent_guides_in_range(
let start = let start =
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1)); MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
// Filter out indent guides that are inside a fold // Filter out indent guides that are inside a fold
!snapshot.is_line_folded(start) let is_folded = snapshot.is_line_folded(start);
let line_indent = snapshot.line_indent_for_buffer_row(start);
let contained_in_fold =
line_indent.len(indent_guide.tab_size) <= indent_guide.indent_level();
!(is_folded && contained_in_fold)
}) })
.collect() .collect()
} }

View File

@@ -113,7 +113,13 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, extension)| { .map(|(id, extension)| {
StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version)) let text = format!("v{}", extension.manifest.version);
StringMatchCandidate {
id,
char_bag: text.as_str().into(),
string: text,
}
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -328,7 +328,11 @@ impl ExtensionsPage {
let match_candidates = dev_extensions let match_candidates = dev_extensions
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, manifest)| StringMatchCandidate::new(ix, &manifest.name)) .map(|(ix, manifest)| StringMatchCandidate {
id: ix,
string: manifest.name.clone(),
char_bag: manifest.name.as_str().into(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = match_strings( let matches = match_strings(

View File

@@ -131,7 +131,7 @@ impl PickerDelegate for OpenPathDelegate {
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, path)| { .map(|(ix, path)| {
StringMatchCandidate::new(ix, &path.to_string_lossy()) StringMatchCandidate::new(ix, path.to_string_lossy().into())
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();

View File

@@ -18,12 +18,22 @@ pub struct StringMatchCandidate {
pub char_bag: CharBag, pub char_bag: CharBag,
} }
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatchCandidate { impl StringMatchCandidate {
pub fn new(id: usize, string: &str) -> Self { pub fn new(id: usize, string: String) -> Self {
Self { Self {
id, id,
string: string.into(), char_bag: CharBag::from(string.as_str()),
char_bag: string.into(), string,
} }
} }
} }
@@ -46,39 +56,29 @@ pub struct StringMatch {
pub string: String, pub string: String,
} }
impl Match for StringMatch {
fn score(&self) -> f64 {
self.score
}
fn set_positions(&mut self, positions: Vec<usize>) {
self.positions = positions;
}
}
impl StringMatch { impl StringMatch {
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> { pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
let mut positions = self.positions.iter().peekable(); let mut positions = self.positions.iter().peekable();
iter::from_fn(move || { iter::from_fn(move || {
if let Some(start) = positions.next().copied() { if let Some(start) = positions.next().copied() {
let Some(char_len) = self.char_len_at_index(start) else { if start >= self.string.len() {
log::error!( log::error!(
"Invariant violation: Index {start} out of range or not on a utf-8 boundary in string {:?}", "Invariant violation: Index {start} out of range in string {:?}",
self.string self.string
); );
return None; return None;
}; }
let mut end = start + char_len; let mut end = start + self.char_len_at_index(start);
while let Some(next_start) = positions.peek() { while let Some(next_start) = positions.peek() {
if end == **next_start { if end == **next_start {
let Some(char_len) = self.char_len_at_index(end) else { if end >= self.string.len() {
log::error!( log::error!(
"Invariant violation: Index {end} out of range or not on a utf-8 boundary in string {:?}", "Invariant violation: Index {end} out of range in string {:?}",
self.string self.string
); );
return None; return None;
}; }
end += char_len; end += self.char_len_at_index(end);
positions.next(); positions.next();
} else { } else {
break; break;
@@ -91,12 +91,8 @@ impl StringMatch {
}) })
} }
/// Gets the byte length of the utf-8 character at a byte offset. If the index is out of range fn char_len_at_index(&self, ix: usize) -> usize {
/// or not on a utf-8 boundary then None is returned. self.string[ix..].chars().next().unwrap().len_utf8()
fn char_len_at_index(&self, ix: usize) -> Option<usize> {
self.string
.get(ix..)
.and_then(|slice| slice.chars().next().map(|char| char.len_utf8()))
} }
} }

View File

@@ -46,8 +46,7 @@ pub trait GitRepository: Send + Sync {
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>; fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
/// Returns the path to the repository, typically the `.git` folder. fn path(&self) -> PathBuf;
fn dot_git_dir(&self) -> PathBuf;
} }
impl std::fmt::Debug for dyn GitRepository { impl std::fmt::Debug for dyn GitRepository {
@@ -86,7 +85,7 @@ impl GitRepository for RealGitRepository {
} }
} }
fn dot_git_dir(&self) -> PathBuf { fn path(&self) -> PathBuf {
let repo = self.repository.lock(); let repo = self.repository.lock();
repo.path().into() repo.path().into()
} }
@@ -234,7 +233,7 @@ pub struct FakeGitRepository {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FakeGitRepositoryState { pub struct FakeGitRepositoryState {
pub dot_git_dir: PathBuf, pub path: PathBuf,
pub event_emitter: smol::channel::Sender<PathBuf>, pub event_emitter: smol::channel::Sender<PathBuf>,
pub index_contents: HashMap<PathBuf, String>, pub index_contents: HashMap<PathBuf, String>,
pub blames: HashMap<PathBuf, Blame>, pub blames: HashMap<PathBuf, Blame>,
@@ -250,9 +249,9 @@ impl FakeGitRepository {
} }
impl FakeGitRepositoryState { impl FakeGitRepositoryState {
pub fn new(dot_git_dir: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self { pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
FakeGitRepositoryState { FakeGitRepositoryState {
dot_git_dir, path,
event_emitter, event_emitter,
index_contents: Default::default(), index_contents: Default::default(),
blames: Default::default(), blames: Default::default(),
@@ -284,9 +283,9 @@ impl GitRepository for FakeGitRepository {
None None
} }
fn dot_git_dir(&self) -> PathBuf { fn path(&self) -> PathBuf {
let state = self.state.lock(); let state = self.state.lock();
state.dot_git_dir.clone() state.path.clone()
} }
fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> { fn status(&self, path_prefixes: &[PathBuf]) -> Result<GitStatus> {
@@ -335,7 +334,7 @@ impl GitRepository for FakeGitRepository {
state.current_branch_name = Some(name.to_owned()); state.current_branch_name = Some(name.to_owned());
state state
.event_emitter .event_emitter
.try_send(state.dot_git_dir.clone()) .try_send(state.path.clone())
.expect("Dropped repo change event"); .expect("Dropped repo change event");
Ok(()) Ok(())
} }
@@ -345,7 +344,7 @@ impl GitRepository for FakeGitRepository {
state.branches.insert(name.to_owned()); state.branches.insert(name.to_owned());
state state
.event_emitter .event_emitter
.try_send(state.dot_git_dir.clone()) .try_send(state.path.clone())
.expect("Dropped repo change event"); .expect("Dropped repo change event");
Ok(()) Ok(())
} }

View File

@@ -14,19 +14,22 @@ path = "src/git_ui.rs"
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
collections.workspace = true
db.workspace = true db.workspace = true
editor.workspace = true
git.workspace = true
gpui.workspace = true gpui.workspace = true
language.workspace = true
project.workspace = true project.workspace = true
schemars.workspace = true schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true
settings.workspace = true settings.workspace = true
theme.workspace = true
ui.workspace = true ui.workspace = true
util.workspace = true util.workspace = true
workspace.workspace = true workspace.workspace = true
git.workspace = true
collections.workspace = true
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows.workspace = true windows.workspace = true

View File

@@ -1,4 +1,7 @@
use anyhow::Context;
use collections::HashMap; use collections::HashMap;
use editor::Editor;
use language::Buffer;
use std::{ use std::{
cell::OnceCell, cell::OnceCell,
collections::HashSet, collections::HashSet,
@@ -8,6 +11,7 @@ use std::{
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use theme::ThemeSettings;
use git::repository::GitFileStatus; use git::repository::GitFileStatus;
@@ -24,7 +28,7 @@ use ui::{
use workspace::dock::{DockPosition, Panel, PanelEvent}; use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace; use workspace::Workspace;
use crate::{git_status_icon, settings::GitPanelSettings}; use crate::{git_status_icon, settings::GitPanelSettings, GitState};
use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll}; use crate::{CommitAllChanges, CommitStagedChanges, DiscardAll, StageAll, UnstageAll};
actions!(git_panel, [ToggleFocus]); actions!(git_panel, [ToggleFocus]);
@@ -84,6 +88,8 @@ pub struct GitPanel {
selected_item: Option<usize>, selected_item: Option<usize>,
show_scrollbar: bool, show_scrollbar: bool,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>, expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
git_state: Model<GitState>,
editor: View<Editor>,
// The entries that are currently shown in the panel, aka // The entries that are currently shown in the panel, aka
// not hidden by folding or such // not hidden by folding or such
@@ -104,11 +110,17 @@ impl GitPanel {
} }
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let git_state = GitState::get_global(cx);
let fs = workspace.app_state().fs.clone(); let fs = workspace.app_state().fs.clone();
let weak_workspace = workspace.weak_handle(); let weak_workspace = workspace.weak_handle();
let project = workspace.project().clone(); let project = workspace.project().clone();
let language_registry = workspace.app_state().languages.clone();
let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| { let git_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
let state = git_state.read(cx);
let current_commit_message = state.commit_message.clone();
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, Self::focus_in).detach(); cx.on_focus(&focus_handle, Self::focus_in).detach();
cx.on_focus_out(&focus_handle, |this, _, cx| { cx.on_focus_out(&focus_handle, |this, _, cx| {
@@ -131,6 +143,55 @@ impl GitPanel {
}) })
.detach(); .detach();
let editor = cx.new_view(|cx| {
let theme = ThemeSettings::get_global(cx);
let mut text_style = cx.text_style();
let refinement = TextStyleRefinement {
font_family: Some(theme.buffer_font.family.clone()),
font_features: Some(FontFeatures::disable_ligatures()),
font_size: Some(px(12.).into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(gpui::transparent_black()),
..Default::default()
};
text_style.refine(&refinement);
let mut editor = Editor::auto_height(10, cx);
if let Some(message) = current_commit_message {
editor.set_text(message, cx);
} else {
editor.set_text("", cx);
}
// editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_text_style_refinement(refinement);
editor.set_placeholder_text("Enter commit message", cx);
editor
});
let buffer = editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("commit editor must be singleton");
cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown");
cx.spawn(|_, mut cx| async move {
let markdown = markdown.await.context("failed to load Markdown language")?;
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx)
})
})
.detach_and_log_err(cx);
let scroll_handle = UniformListScrollHandle::new(); let scroll_handle = UniformListScrollHandle::new();
let mut this = Self { let mut this = Self {
@@ -142,6 +203,8 @@ impl GitPanel {
visible_entries: Vec::new(), visible_entries: Vec::new(),
current_modifiers: cx.modifiers(), current_modifiers: cx.modifiers(),
expanded_dir_ids: Default::default(), expanded_dir_ids: Default::default(),
git_state,
editor,
width: Some(px(360.)), width: Some(px(360.)),
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()), scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
@@ -269,14 +332,24 @@ impl GitPanel {
println!("Discard all triggered"); println!("Discard all triggered");
} }
fn clear_message(&mut self, cx: &mut ViewContext<Self>) {
let git_state = self.git_state.clone();
git_state.update(cx, |state, _cx| state.clear_message());
self.editor.update(cx, |editor, cx| editor.set_text("", cx));
}
/// Commit all staged changes /// Commit all staged changes
fn commit_staged_changes(&mut self, _: &CommitStagedChanges, _cx: &mut ViewContext<Self>) { fn commit_staged_changes(&mut self, _: &CommitStagedChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all staged // TODO: Implement commit all staged
println!("Commit staged changes triggered"); println!("Commit staged changes triggered");
} }
/// Commit all changes, regardless of whether they are staged or not /// Commit all changes, regardless of whether they are staged or not
fn commit_all_changes(&mut self, _: &CommitAllChanges, _cx: &mut ViewContext<Self>) { fn commit_all_changes(&mut self, _: &CommitAllChanges, cx: &mut ViewContext<Self>) {
self.clear_message(cx);
// TODO: Implement commit all changes // TODO: Implement commit all changes
println!("Commit all changes triggered"); println!("Commit all changes triggered");
} }
@@ -426,6 +499,23 @@ impl GitPanel {
cx.notify(); cx.notify();
} }
fn on_buffer_event(
&mut self,
_buffer: Model<Buffer>,
event: &language::BufferEvent,
cx: &mut ViewContext<Self>,
) {
if let language::BufferEvent::Reparsed | language::BufferEvent::Edited = event {
let commit_message = self.editor.update(cx, |editor, cx| editor.text(cx));
self.git_state.update(cx, |state, _cx| {
state.commit_message = Some(commit_message.into());
});
cx.notify();
}
}
} }
impl GitPanel { impl GitPanel {
@@ -499,6 +589,13 @@ impl GitPanel {
} }
pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement { pub fn render_commit_editor(&self, cx: &ViewContext<Self>) -> impl IntoElement {
let git_state = self.git_state.clone();
let commit_message = git_state.read(cx).commit_message.clone();
let editor = self.editor.clone();
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
println!("{:?}", commit_message);
let focus_handle_1 = self.focus_handle(cx).clone(); let focus_handle_1 = self.focus_handle(cx).clone();
let focus_handle_2 = self.focus_handle(cx).clone(); let focus_handle_2 = self.focus_handle(cx).clone();
@@ -534,25 +631,26 @@ impl GitPanel {
div().w_full().h(px(140.)).px_2().pt_1().pb_2().child( div().w_full().h(px(140.)).px_2().pt_1().pb_2().child(
v_flex() v_flex()
.id("commit-editor-container")
.relative()
.h_full() .h_full()
.py_2p5() .py_2p5()
.px_3() .px_3()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.font_buffer(cx) .on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
.text_ui_sm(cx) .child(self.editor.clone())
.text_color(cx.theme().colors().text_muted) .child(
.child("Add a message") h_flex()
.gap_1() .absolute()
.child(div().flex_grow()) .bottom_2p5()
.child(h_flex().child(div().gap_1().flex_grow()).child( .right_3()
if self.current_modifiers.alt { .child(div().gap_1().flex_grow())
commit_all_button .child(if self.current_modifiers.alt {
} else { commit_all_button
commit_staged_button } else {
}, commit_staged_button
)) }),
.cursor(CursorStyle::OperationNotAllowed) ),
.opacity(0.5),
) )
} }

View File

@@ -1,8 +1,8 @@
use ::settings::Settings; use ::settings::Settings;
use git::repository::GitFileStatus; use git::repository::GitFileStatus;
use gpui::{actions, AppContext, Hsla}; use gpui::{actions, prelude::*, AppContext, Global, Hsla, Model};
use settings::GitPanelSettings; use settings::GitPanelSettings;
use ui::{Color, Icon, IconName, IntoElement}; use ui::{Color, Icon, IconName, IntoElement, SharedString};
pub mod git_panel; pub mod git_panel;
mod settings; mod settings;
@@ -14,14 +14,50 @@ actions!(
UnstageAll, UnstageAll,
DiscardAll, DiscardAll,
CommitStagedChanges, CommitStagedChanges,
CommitAllChanges CommitAllChanges,
ClearMessage
] ]
); );
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
GitPanelSettings::register(cx); GitPanelSettings::register(cx);
let git_state = cx.new_model(|_cx| GitState::new());
cx.set_global(GlobalGitState(git_state));
} }
struct GlobalGitState(Model<GitState>);
impl Global for GlobalGitState {}
pub struct GitState {
commit_message: Option<SharedString>,
}
impl GitState {
pub fn new() -> Self {
GitState {
commit_message: None,
}
}
pub fn set_message(&mut self, message: Option<SharedString>) {
self.commit_message = message;
}
pub fn clear_message(&mut self) {
self.commit_message = None;
}
pub fn get_global(cx: &mut AppContext) -> Model<GitState> {
cx.global::<GlobalGitState>().0.clone()
}
}
// impl EventEmitter<Event> for GitState {}
// #[derive(Clone, Debug, PartialEq, Eq)]
// pub enum Event {}
const ADDED_COLOR: Hsla = Hsla { const ADDED_COLOR: Hsla = Hsla {
h: 142. / 360., h: 142. / 360.,
s: 0.68, s: 0.68,

View File

@@ -50,10 +50,7 @@ pub struct ForegroundExecutor {
/// the task to continue running, but with no way to return a value. /// the task to continue running, but with no way to return a value.
#[must_use] #[must_use]
#[derive(Debug)] #[derive(Debug)]
pub struct Task<T>(TaskState<T>); pub enum Task<T> {
#[derive(Debug)]
enum TaskState<T> {
/// A task that is ready to return a value /// A task that is ready to return a value
Ready(Option<T>), Ready(Option<T>),
@@ -64,14 +61,14 @@ enum TaskState<T> {
impl<T> Task<T> { impl<T> Task<T> {
/// Creates a new task that will resolve with the value /// Creates a new task that will resolve with the value
pub fn ready(val: T) -> Self { pub fn ready(val: T) -> Self {
Task(TaskState::Ready(Some(val))) Task::Ready(Some(val))
} }
/// Detaching a task runs it to completion in the background /// Detaching a task runs it to completion in the background
pub fn detach(self) { pub fn detach(self) {
match self { match self {
Task(TaskState::Ready(_)) => {} Task::Ready(_) => {}
Task(TaskState::Spawned(task)) => task.detach(), Task::Spawned(task) => task.detach(),
} }
} }
} }
@@ -97,8 +94,8 @@ impl<T> Future for Task<T> {
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } { match unsafe { self.get_unchecked_mut() } {
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), Task::Ready(val) => Poll::Ready(val.take().unwrap()),
Task(TaskState::Spawned(task)) => task.poll(cx), Task::Spawned(task) => task.poll(cx),
} }
} }
} }
@@ -166,7 +163,7 @@ impl BackgroundExecutor {
let (runnable, task) = let (runnable, task) =
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label)); async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
runnable.schedule(); runnable.schedule();
Task(TaskState::Spawned(task)) Task::Spawned(task)
} }
/// Used by the test harness to run an async test in a synchronous fashion. /// Used by the test harness to run an async test in a synchronous fashion.
@@ -343,7 +340,7 @@ impl BackgroundExecutor {
move |runnable| dispatcher.dispatch_after(duration, runnable) move |runnable| dispatcher.dispatch_after(duration, runnable)
}); });
runnable.schedule(); runnable.schedule();
Task(TaskState::Spawned(task)) Task::Spawned(task)
} }
/// in tests, start_waiting lets you indicate which task is waiting (for debugging only) /// in tests, start_waiting lets you indicate which task is waiting (for debugging only)
@@ -463,7 +460,7 @@ impl ForegroundExecutor {
dispatcher.dispatch_on_main_thread(runnable) dispatcher.dispatch_on_main_thread(runnable)
}); });
runnable.schedule(); runnable.schedule();
Task(TaskState::Spawned(task)) Task::Spawned(task)
} }
inner::<R>(dispatcher, Box::pin(future)) inner::<R>(dispatcher, Box::pin(future))
} }

View File

@@ -208,7 +208,7 @@ impl IndexedDocsStore {
let candidates = items let candidates = items
.iter() .iter()
.enumerate() .enumerate()
.map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path)) .map(|(ix, item_path)| StringMatchCandidate::new(ix, item_path.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = fuzzy::match_strings( let matches = fuzzy::match_strings(

View File

@@ -1678,10 +1678,6 @@ impl CodeLabel {
pub fn text(&self) -> &str { pub fn text(&self) -> &str {
self.text.as_str() self.text.as_str()
} }
pub fn filter_text(&self) -> &str {
&self.text[self.filter_range.clone()]
}
} }
impl From<String> for CodeLabel { impl From<String> for CodeLabel {

View File

@@ -73,8 +73,8 @@ impl<T> Outline<T> {
.map(|range| &item.text[range.start..range.end]) .map(|range| &item.text[range.start..range.end])
.collect::<String>(); .collect::<String>();
path_candidates.push(StringMatchCandidate::new(id, &path_text)); path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
candidates.push(StringMatchCandidate::new(id, &candidate_text)); candidates.push(StringMatchCandidate::new(id, candidate_text));
} }
Self { Self {

View File

@@ -112,7 +112,7 @@ impl LanguageSelectorDelegate {
.then_some(name) .then_some(name)
}) })
.enumerate() .enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Self { Self {

View File

@@ -375,7 +375,7 @@ impl ContextProvider for PythonContextProvider {
args: vec![ args: vec![
"-m".to_owned(), "-m".to_owned(),
"unittest".to_owned(), "unittest".to_owned(),
PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(), PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace()
], ],
tags: vec![ tags: vec![
"python-unittest-class".to_owned(), "python-unittest-class".to_owned(),

View File

@@ -443,7 +443,7 @@ impl LanguageServer {
let stderr_captures = stderr_capture.clone(); let stderr_captures = stderr_capture.clone();
cx.spawn(|_| Self::handle_stderr(stderr, io_handlers, stderr_captures).log_err()) cx.spawn(|_| Self::handle_stderr(stderr, io_handlers, stderr_captures).log_err())
}) })
.unwrap_or_else(|| Task::ready(None)); .unwrap_or_else(|| Task::Ready(Some(None)));
let input_task = cx.spawn(|_| async move { let input_task = cx.spawn(|_| async move {
let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task); let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
stdout.or(stderr) stdout.or(stderr)

View File

@@ -19,6 +19,5 @@ actions!(
SelectNext, SelectNext,
SelectFirst, SelectFirst,
SelectLast, SelectLast,
Restart
] ]
); );

View File

@@ -195,7 +195,6 @@ pub struct ExcerptInfo {
pub buffer: BufferSnapshot, pub buffer: BufferSnapshot,
pub buffer_id: BufferId, pub buffer_id: BufferId,
pub range: ExcerptRange<text::Anchor>, pub range: ExcerptRange<text::Anchor>,
pub text_summary: TextSummary,
} }
impl std::fmt::Debug for ExcerptInfo { impl std::fmt::Debug for ExcerptInfo {
@@ -1547,33 +1546,6 @@ impl MultiBuffer {
excerpts excerpts
} }
pub fn excerpt_ranges_for_buffer(
&self,
buffer_id: BufferId,
cx: &AppContext,
) -> Vec<Range<Point>> {
let snapshot = self.read(cx);
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, Point)>(&());
buffers
.get(&buffer_id)
.into_iter()
.flat_map(|state| &state.excerpts)
.filter_map(move |locator| {
cursor.seek_forward(&Some(locator), Bias::Left, &());
cursor.item().and_then(|excerpt| {
if excerpt.locator == *locator {
let excerpt_start = cursor.start().1;
let excerpt_end = excerpt_start + excerpt.text_summary.lines;
Some(excerpt_start..excerpt_end)
} else {
None
}
})
})
.collect()
}
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> { pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
self.snapshot self.snapshot
.borrow() .borrow()
@@ -3587,7 +3559,6 @@ impl MultiBufferSnapshot {
buffer: excerpt.buffer.clone(), buffer: excerpt.buffer.clone(),
buffer_id: excerpt.buffer_id, buffer_id: excerpt.buffer_id,
range: excerpt.range.clone(), range: excerpt.range.clone(),
text_summary: excerpt.text_summary.clone(),
}); });
if next.is_none() { if next.is_none() {
@@ -3603,7 +3574,6 @@ impl MultiBufferSnapshot {
buffer: prev_excerpt.buffer.clone(), buffer: prev_excerpt.buffer.clone(),
buffer_id: prev_excerpt.buffer_id, buffer_id: prev_excerpt.buffer_id,
range: prev_excerpt.range.clone(), range: prev_excerpt.range.clone(),
text_summary: prev_excerpt.text_summary.clone(),
}); });
let row = MultiBufferRow(cursor.start().1.row); let row = MultiBufferRow(cursor.start().1.row);

File diff suppressed because it is too large Load Diff

View File

@@ -1211,7 +1211,7 @@ impl BufferStore {
return Task::ready(Err(anyhow!("buffer has no file"))); return Task::ready(Err(anyhow!("buffer has no file")));
}; };
match file.worktree.read(cx) { match file.worktree.clone().read(cx) {
Worktree::Local(worktree) => { Worktree::Local(worktree) => {
let Some(repo) = worktree.local_git_repo(file.path()) else { let Some(repo) = worktree.local_git_repo(file.path()) else {
return Task::ready(Err(anyhow!("no repository for buffer found"))); return Task::ready(Err(anyhow!("no repository for buffer found")));

View File

@@ -52,7 +52,7 @@ use lsp::{
WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
}; };
use node_runtime::read_package_installed_version; use node_runtime::read_package_installed_version;
use parking_lot::Mutex; use parking_lot::{Mutex, RwLock};
use postage::watch; use postage::watch;
use rand::prelude::*; use rand::prelude::*;
@@ -65,14 +65,12 @@ use smol::channel::Sender;
use snippet::Snippet; use snippet::Snippet;
use std::{ use std::{
any::Any, any::Any,
cell::RefCell,
cmp::Ordering, cmp::Ordering,
convert::TryInto, convert::TryInto,
ffi::OsStr, ffi::OsStr,
iter, mem, iter, mem,
ops::{ControlFlow, Range}, ops::{ControlFlow, Range},
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
rc::Rc,
str, str,
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
@@ -4139,7 +4137,7 @@ impl LspStore {
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
completion_indices: Vec<usize>, completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
let client = self.upstream_client(); let client = self.upstream_client();
@@ -4153,8 +4151,8 @@ impl LspStore {
if let Some((client, project_id)) = client { if let Some((client, project_id)) = client {
for completion_index in completion_indices { for completion_index in completion_indices {
let (server_id, completion) = { let (server_id, completion) = {
let completions = completions.borrow_mut(); let completions_guard = completions.read();
let completion = &completions[completion_index]; let completion = &completions_guard[completion_index];
did_resolve = true; did_resolve = true;
let server_id = completion.server_id; let server_id = completion.server_id;
let completion = completion.lsp_completion.clone(); let completion = completion.lsp_completion.clone();
@@ -4177,8 +4175,8 @@ impl LspStore {
} else { } else {
for completion_index in completion_indices { for completion_index in completion_indices {
let (server_id, completion) = { let (server_id, completion) = {
let completions = completions.borrow_mut(); let completions_guard = completions.read();
let completion = &completions[completion_index]; let completion = &completions_guard[completion_index];
let server_id = completion.server_id; let server_id = completion.server_id;
let completion = completion.lsp_completion.clone(); let completion = completion.lsp_completion.clone();
@@ -4220,7 +4218,7 @@ impl LspStore {
server: Arc<lsp::LanguageServer>, server: Arc<lsp::LanguageServer>,
adapter: Arc<CachedLspAdapter>, adapter: Arc<CachedLspAdapter>,
snapshot: &BufferSnapshot, snapshot: &BufferSnapshot,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
completion: lsp::CompletionItem, completion: lsp::CompletionItem,
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
@@ -4248,11 +4246,11 @@ impl LspStore {
) )
.await; .await;
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.documentation = Some(documentation); completion.documentation = Some(documentation);
} else { } else {
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.documentation = Some(Documentation::Undocumented); completion.documentation = Some(Documentation::Undocumented);
} }
@@ -4267,7 +4265,7 @@ impl LspStore {
if let Some((old_range, mut new_text)) = edit { if let Some((old_range, mut new_text)) = edit {
LineEnding::normalize(&mut new_text); LineEnding::normalize(&mut new_text);
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.new_text = new_text; completion.new_text = new_text;
@@ -4276,7 +4274,7 @@ impl LspStore {
} }
if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) { if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
// vtsls might change the type of completion after resolution. // vtsls might change the type of completion after resolution.
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
if completion_item.insert_text_format != completion.lsp_completion.insert_text_format { if completion_item.insert_text_format != completion.lsp_completion.insert_text_format {
completion.lsp_completion.insert_text_format = completion_item.insert_text_format; completion.lsp_completion.insert_text_format = completion_item.insert_text_format;
@@ -4302,21 +4300,10 @@ impl LspStore {
) )
}); });
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.lsp_completion = completion_item; completion.lsp_completion = completion_item;
if completion.label.filter_text() == new_label.filter_text() { completion.label = new_label;
completion.label = new_label;
} else {
log::error!(
"Resolved completion changed display label from {} to {}. \
Refusing to apply this because it changes the fuzzy match text from {} to {}",
completion.label.text(),
new_label.text(),
completion.label.filter_text(),
new_label.filter_text()
);
}
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
@@ -4324,7 +4311,7 @@ impl LspStore {
project_id: u64, project_id: u64,
server_id: LanguageServerId, server_id: LanguageServerId,
buffer_id: BufferId, buffer_id: BufferId,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
completion_index: usize, completion_index: usize,
completion: lsp::CompletionItem, completion: lsp::CompletionItem,
client: AnyProtoClient, client: AnyProtoClient,
@@ -4362,7 +4349,7 @@ impl LspStore {
Documentation::MultiLinePlainText(response.documentation) Documentation::MultiLinePlainText(response.documentation)
}; };
let mut completions = completions.borrow_mut(); let mut completions = completions.write();
let completion = &mut completions[completion_index]; let completion = &mut completions[completion_index];
completion.documentation = Some(documentation); completion.documentation = Some(documentation);
completion.lsp_completion = lsp_completion; completion.lsp_completion = lsp_completion;

View File

@@ -57,7 +57,7 @@ use lsp::{
}; };
use lsp_command::*; use lsp_command::*;
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use parking_lot::Mutex; use parking_lot::{Mutex, RwLock};
pub use prettier_store::PrettierStore; pub use prettier_store::PrettierStore;
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
use remote::{SshConnectionOptions, SshRemoteClient}; use remote::{SshConnectionOptions, SshRemoteClient};
@@ -73,10 +73,8 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider; use snippet_provider::SnippetProvider;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell,
ops::Range, ops::Range,
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
rc::Rc,
str, str,
sync::Arc, sync::Arc,
time::Duration, time::Duration,
@@ -2542,7 +2540,7 @@ impl Project {
.read(cx) .read(cx)
.list_toolchains(worktree_id, language_name, cx) .list_toolchains(worktree_id, language_name, cx)
}) })
.ok()? .unwrap_or(Task::Ready(None))
.await .await
}) })
} else { } else {
@@ -2874,7 +2872,7 @@ impl Project {
&self, &self,
buffer: Model<Buffer>, buffer: Model<Buffer>,
completion_indices: Vec<usize>, completion_indices: Vec<usize>,
completions: Rc<RefCell<Box<[Completion]>>>, completions: Arc<RwLock<Box<[Completion]>>>,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<bool>> { ) -> Task<Result<bool>> {
self.lsp_store.update(cx, |lsp_store, cx| { self.lsp_store.update(cx, |lsp_store, cx| {

View File

@@ -81,8 +81,7 @@ impl Inventory {
} }
/// Pulls its task sources relevant to the worktree and the language given, /// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, worktree tasks first, language tasks second /// returns all task templates with their source kinds, in no specific order.
/// and global tasks last. No specific order inside source kinds groups.
pub fn list_tasks( pub fn list_tasks(
&self, &self,
file: Option<Arc<dyn File>>, file: Option<Arc<dyn File>>,
@@ -93,15 +92,13 @@ impl Inventory {
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name().0, name: language.name().0,
}); });
let global_tasks = self.global_templates_from_settings();
let language_tasks = language let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter() .into_iter()
.flat_map(|tasks| tasks.0.into_iter()) .flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.clone()?, task))) .flat_map(|task| Some((task_source_kind.clone()?, task)));
.chain(global_tasks);
self.worktree_templates_from_settings(worktree) self.templates_from_settings(worktree)
.chain(language_tasks) .chain(language_tasks)
.collect() .collect()
} }
@@ -168,18 +165,14 @@ impl Inventory {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let not_used_score = post_inc(&mut lru_score); let not_used_score = post_inc(&mut lru_score);
let global_tasks = self.global_templates_from_settings();
let language_tasks = language let language_tasks = language
.and_then(|language| language.context_provider()?.associated_tasks(file, cx)) .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
.into_iter() .into_iter()
.flat_map(|tasks| tasks.0.into_iter()) .flat_map(|tasks| tasks.0.into_iter())
.flat_map(|task| Some((task_source_kind.clone()?, task))) .flat_map(|task| Some((task_source_kind.clone()?, task)));
.chain(global_tasks); let new_resolved_tasks = self
let worktree_tasks = self .templates_from_settings(worktree)
.worktree_templates_from_settings(worktree) .chain(language_tasks)
.chain(language_tasks);
let new_resolved_tasks = worktree_tasks
.filter_map(|(kind, task)| { .filter_map(|(kind, task)| {
let id_base = kind.to_id_base(); let id_base = kind.to_id_base();
Some(( Some((
@@ -242,8 +235,9 @@ impl Inventory {
self.last_scheduled_tasks.retain(|(_, task)| &task.id != id); self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
} }
fn global_templates_from_settings( fn templates_from_settings(
&self, &self,
worktree: Option<WorktreeId>,
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
self.templates_from_settings self.templates_from_settings
.global .global
@@ -258,34 +252,28 @@ impl Inventory {
template, template,
) )
}) })
} .chain(worktree.into_iter().flat_map(|worktree| {
self.templates_from_settings
fn worktree_templates_from_settings( .worktree
&self, .get(&worktree)
worktree: Option<WorktreeId>, .into_iter()
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> { .flatten()
worktree.into_iter().flat_map(|worktree| { .flat_map(|(directory, templates)| {
self.templates_from_settings templates.iter().map(move |template| (directory, template))
.worktree })
.get(&worktree) .map(move |(directory, template)| {
.into_iter() (
.flatten() TaskSourceKind::Worktree {
.flat_map(|(directory, templates)| { id: worktree,
templates.iter().map(move |template| (directory, template)) directory_in_worktree: directory.to_path_buf(),
}) id_base: Cow::Owned(format!(
.map(move |(directory, template)| { "local worktree tasks from directory {directory:?}"
( )),
TaskSourceKind::Worktree { },
id: worktree, template.clone(),
directory_in_worktree: directory.to_path_buf(), )
id_base: Cow::Owned(format!( })
"local worktree tasks from directory {directory:?}" }))
)),
},
template.clone(),
)
})
})
} }
/// Updates in-memory task metadata from the JSON string given. /// Updates in-memory task metadata from the JSON string given.
@@ -378,7 +366,7 @@ mod test_inventory {
use crate::Inventory; use crate::Inventory;
use super::TaskSourceKind; use super::{task_source_kind_preference, TaskSourceKind};
pub(super) fn task_template_names( pub(super) fn task_template_names(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
@@ -420,18 +408,15 @@ mod test_inventory {
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> { ) -> Vec<(TaskSourceKind, String)> {
inventory.update(cx, |inventory, cx| { let (used, current) = inventory.update(cx, |inventory, cx| {
let task_context = &TaskContext::default(); inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
inventory });
.list_tasks(None, None, worktree, cx) let mut all = used;
.into_iter() all.extend(current);
.filter_map(|(source_kind, task)| { all.into_iter()
let id_base = source_kind.to_id_base(); .map(|(source_kind, task)| (source_kind, task.resolved_label))
Some((source_kind, task.resolve_task(&id_base, task_context)?)) .sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
}) .collect()
.map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
.collect()
})
} }
} }
@@ -804,30 +789,6 @@ mod tests {
.unwrap(); .unwrap();
}); });
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, None, cx).await,
worktree_independent_tasks,
"Without a worktree, only worktree-independent tasks should be listed"
);
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, Some(worktree_1), cx).await,
worktree_1_tasks
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
assert_eq!(
list_tasks_sorted_by_last_used(&inventory, Some(worktree_2), cx).await,
worktree_2_tasks
.iter()
.chain(worktree_independent_tasks.iter())
.cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(),
);
assert_eq!( assert_eq!(
list_tasks(&inventory, None, cx).await, list_tasks(&inventory, None, cx).await,
worktree_independent_tasks, worktree_independent_tasks,
@@ -839,6 +800,7 @@ mod tests {
.iter() .iter()
.chain(worktree_independent_tasks.iter()) .chain(worktree_independent_tasks.iter())
.cloned() .cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
assert_eq!( assert_eq!(
@@ -847,6 +809,7 @@ mod tests {
.iter() .iter()
.chain(worktree_independent_tasks.iter()) .chain(worktree_independent_tasks.iter())
.cloned() .cloned()
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }
@@ -858,7 +821,7 @@ mod tests {
TaskStore::init(None); TaskStore::init(None);
} }
async fn resolved_task_names( pub(super) async fn resolved_task_names(
inventory: &Model<Inventory>, inventory: &Model<Inventory>,
worktree: Option<WorktreeId>, worktree: Option<WorktreeId>,
cx: &mut TestAppContext, cx: &mut TestAppContext,
@@ -886,20 +849,4 @@ mod tests {
)) ))
.unwrap() .unwrap()
} }
async fn list_tasks_sorted_by_last_used(
inventory: &Model<Inventory>,
worktree: Option<WorktreeId>,
cx: &mut TestAppContext,
) -> Vec<(TaskSourceKind, String)> {
let (used, current) = inventory.update(cx, |inventory, cx| {
inventory.used_and_current_resolved_tasks(worktree, None, &TaskContext::default(), cx)
});
let mut all = used;
all.extend(current);
all.into_iter()
.map(|(source_kind, task)| (source_kind, task.resolved_label))
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
.collect()
}
} }

View File

@@ -331,7 +331,7 @@ fn local_task_context_for_location(
let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx)); let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
let worktree_abs_path = worktree_id let worktree_abs_path = worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| worktree.read(cx).root_dir()); .map(|worktree| worktree.read(cx).abs_path());
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
let worktree_abs_path = worktree_abs_path.clone(); let worktree_abs_path = worktree_abs_path.clone();

View File

@@ -50,7 +50,13 @@ impl Project {
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx)) .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
.into_iter() .into_iter()
.chain(self.worktrees(cx)) .chain(self.worktrees(cx))
.find_map(|tree| tree.read(cx).root_dir()); .find_map(|tree| {
let worktree = tree.read(cx);
worktree
.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| worktree.abs_path().clone())
});
worktree worktree
} }
@@ -94,6 +100,7 @@ impl Project {
} }
} }
}; };
let ssh_details = self.ssh_details(cx);
let mut settings_location = None; let mut settings_location = None;
if let Some(path) = path.as_ref() { if let Some(path) = path.as_ref() {
@@ -106,57 +113,10 @@ impl Project {
} }
let settings = TerminalSettings::get(settings_location, cx).clone(); let settings = TerminalSettings::get(settings_location, cx).clone();
cx.spawn(move |project, mut cx| async move {
let python_venv_directory = if let Some(path) = path.clone() {
project
.update(&mut cx, |this, cx| {
this.python_venv_directory(path, settings.detect_venv.clone(), cx)
})?
.await
} else {
None
};
project.update(&mut cx, |project, cx| {
project.create_terminal_with_venv(kind, python_venv_directory, window, cx)
})?
})
}
pub fn create_terminal_with_venv(
&mut self,
kind: TerminalKind,
python_venv_directory: Option<PathBuf>,
window: AnyWindowHandle,
cx: &mut ModelContext<Self>,
) -> Result<Model<Terminal>> {
let this = &mut *self;
let path: Option<Arc<Path>> = match &kind {
TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
TerminalKind::Task(spawn_task) => {
if let Some(cwd) = &spawn_task.cwd {
Some(Arc::from(cwd.as_ref()))
} else {
this.active_project_directory(cx)
}
}
};
let ssh_details = this.ssh_details(cx);
let mut settings_location = None;
if let Some(path) = path.as_ref() {
if let Some((worktree, _)) = this.find_worktree(path, cx) {
settings_location = Some(SettingsLocation {
worktree_id: worktree.read(cx).id(),
path,
});
}
}
let settings = TerminalSettings::get(settings_location, cx).clone();
let (completion_tx, completion_rx) = bounded(1); let (completion_tx, completion_rx) = bounded(1);
// Start with the environment that we might have inherited from the Zed CLI. // Start with the environment that we might have inherited from the Zed CLI.
let mut env = this let mut env = self
.environment .environment
.read(cx) .read(cx)
.get_cli_environment() .get_cli_environment()
@@ -171,141 +131,165 @@ impl Project {
None None
}; };
let mut python_venv_activate_command = None; cx.spawn(move |this, mut cx| async move {
let python_venv_directory = if let Some(path) = path.clone() {
this.update(&mut cx, |this, cx| {
this.python_venv_directory(path, settings.detect_venv.clone(), cx)
})?
.await
} else {
None
};
let mut python_venv_activate_command = None;
let (spawn_task, shell) = match kind { let (spawn_task, shell) = match kind {
TerminalKind::Shell(_) => { TerminalKind::Shell(_) => {
if let Some(python_venv_directory) = &python_venv_directory { if let Some(python_venv_directory) = python_venv_directory {
python_venv_activate_command = python_venv_activate_command = this
this.python_activate_command(python_venv_directory, &settings.detect_venv); .update(&mut cx, |this, _| {
} this.python_activate_command(
&python_venv_directory,
match &ssh_details { &settings.detect_venv,
Some((host, ssh_command)) => { )
log::debug!("Connecting to a remote server: {ssh_command:?}"); })
.ok()
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed .flatten();
// to properly display colors.
// We do not have the luxury of assuming the host has it installed,
// so we set it to a default that does not break the highlighting via ssh.
env.entry("TERM".to_string())
.or_insert_with(|| "xterm-256color".to_string());
let (program, args) =
wrap_for_ssh(&ssh_command, None, path.as_deref(), env, None);
env = HashMap::default();
(
Option::<TaskState>::None,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
} }
None => (None, settings.shell.clone()),
}
}
TerminalKind::Task(spawn_task) => {
let task_state = Some(TaskState {
id: spawn_task.id,
full_label: spawn_task.full_label,
label: spawn_task.label,
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
show_summary: spawn_task.show_summary,
show_command: spawn_task.show_command,
completion_rx,
});
env.extend(spawn_task.env); match &ssh_details {
Some((host, ssh_command)) => {
log::debug!("Connecting to a remote server: {ssh_command:?}");
if let Some(venv_path) = &python_venv_directory { // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
env.insert( // to properly display colors.
"VIRTUAL_ENV".to_string(), // We do not have the luxury of assuming the host has it installed,
venv_path.to_string_lossy().to_string(), // so we set it to a default that does not break the highlighting via ssh.
); env.entry("TERM".to_string())
} .or_insert_with(|| "xterm-256color".to_string());
match &ssh_details { let (program, args) =
Some((host, ssh_command)) => { wrap_for_ssh(ssh_command, None, path.as_deref(), env, None);
log::debug!("Connecting to a remote server: {ssh_command:?}"); env = HashMap::default();
env.entry("TERM".to_string()) (
.or_insert_with(|| "xterm-256color".to_string()); Option::<TaskState>::None,
let (program, args) = wrap_for_ssh( Shell::WithArguments {
&ssh_command, program,
Some((&spawn_task.command, &spawn_task.args)), args,
path.as_deref(), title_override: Some(format!("{} — Terminal", host).into()),
env, },
python_venv_directory.as_deref(), )
);
env = HashMap::default();
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => {
if let Some(venv_path) = &python_venv_directory {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
} }
None => (None, settings.shell.clone()),
(
task_state,
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
title_override: None,
},
)
} }
} }
} TerminalKind::Task(spawn_task) => {
}; let task_state = Some(TaskState {
TerminalBuilder::new( id: spawn_task.id,
local_path.map(|path| path.to_path_buf()), full_label: spawn_task.full_label,
python_venv_directory, label: spawn_task.label,
spawn_task, command_label: spawn_task.command_label,
shell, hide: spawn_task.hide,
env, status: TaskStatus::Running,
settings.cursor_shape.unwrap_or_default(), show_summary: spawn_task.show_summary,
settings.alternate_scroll, show_command: spawn_task.show_command,
settings.max_scroll_history_lines, completion_rx,
ssh_details.is_some(), });
window,
completion_tx,
cx,
)
.map(|builder| {
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
this.terminals env.extend(spawn_task.env);
.local_handles
.push(terminal_handle.downgrade());
let id = terminal_handle.entity_id(); if let Some(venv_path) = &python_venv_directory {
cx.observe_release(&terminal_handle, move |project, _terminal, cx| { env.insert(
let handles = &mut project.terminals.local_handles; "VIRTUAL_ENV".to_string(),
venv_path.to_string_lossy().to_string(),
);
}
if let Some(index) = handles match &ssh_details {
.iter() Some((host, ssh_command)) => {
.position(|terminal| terminal.entity_id() == id) log::debug!("Connecting to a remote server: {ssh_command:?}");
{ env.entry("TERM".to_string())
handles.remove(index); .or_insert_with(|| "xterm-256color".to_string());
cx.notify(); let (program, args) = wrap_for_ssh(
ssh_command,
Some((&spawn_task.command, &spawn_task.args)),
path.as_deref(),
env,
python_venv_directory,
);
env = HashMap::default();
(
task_state,
Shell::WithArguments {
program,
args,
title_override: Some(format!("{} — Terminal", host).into()),
},
)
}
None => {
if let Some(venv_path) = &python_venv_directory {
add_environment_path(&mut env, &venv_path.join("bin")).log_err();
}
(
task_state,
Shell::WithArguments {
program: spawn_task.command,
args: spawn_task.args,
title_override: None,
},
)
}
}
} }
}) };
.detach(); let terminal = this.update(&mut cx, |this, cx| {
TerminalBuilder::new(
local_path.map(|path| path.to_path_buf()),
spawn_task,
shell,
env,
settings.cursor_shape.unwrap_or_default(),
settings.alternate_scroll,
settings.max_scroll_history_lines,
ssh_details.is_some(),
window,
completion_tx,
cx,
)
.map(|builder| {
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
if let Some(activate_command) = python_venv_activate_command { this.terminals
this.activate_python_virtual_environment(activate_command, &terminal_handle, cx); .local_handles
} .push(terminal_handle.downgrade());
terminal_handle
let id = terminal_handle.entity_id();
cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
let handles = &mut project.terminals.local_handles;
if let Some(index) = handles
.iter()
.position(|terminal| terminal.entity_id() == id)
{
handles.remove(index);
cx.notify();
}
})
.detach();
if let Some(activate_command) = python_venv_activate_command {
this.activate_python_virtual_environment(
activate_command,
&terminal_handle,
cx,
);
}
terminal_handle
})
})?;
terminal
}) })
} }
@@ -440,9 +424,9 @@ impl Project {
&self, &self,
command: String, command: String,
terminal_handle: &Model<Terminal>, terminal_handle: &Model<Terminal>,
cx: &mut AppContext, cx: &mut ModelContext<Project>,
) { ) {
terminal_handle.update(cx, |terminal, _| terminal.input_bytes(command.into_bytes())); terminal_handle.update(cx, |this, _| this.input_bytes(command.into_bytes()));
} }
pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal::Terminal>> { pub fn local_terminal_handles(&self) -> &Vec<WeakModel<terminal::Terminal>> {
@@ -455,7 +439,7 @@ pub fn wrap_for_ssh(
command: Option<(&String, &Vec<String>)>, command: Option<(&String, &Vec<String>)>,
path: Option<&Path>, path: Option<&Path>,
env: HashMap<String, String>, env: HashMap<String, String>,
venv_directory: Option<&Path>, venv_directory: Option<PathBuf>,
) -> (String, Vec<String>) { ) -> (String, Vec<String>) {
let to_run = if let Some((command, args)) = command { let to_run = if let Some((command, args)) = command {
let command = Cow::Borrowed(command.as_str()); let command = Cow::Borrowed(command.as_str());

View File

@@ -3224,9 +3224,7 @@ impl ProjectPanel {
.border_1() .border_1()
.border_r_2() .border_r_2()
.border_color(border_color) .border_color(border_color)
.when(!is_marked && !is_active, |div| { .hover(|style| style.bg(bg_hover_color))
div.hover(|style| style.bg(bg_hover_color))
})
.when(is_local, |div| { .when(is_local, |div| {
div.on_drag_move::<ExternalPaths>(cx.listener( div.on_drag_move::<ExternalPaths>(cx.listener(
move |this, event: &DragMoveEvent<ExternalPaths>, cx| { move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
@@ -3899,11 +3897,6 @@ impl Render for ProjectPanel {
this.hide_scrollbar(cx); this.hide_scrollbar(cx);
} }
})) }))
.on_click(cx.listener(|this, _event, cx| {
cx.stop_propagation();
this.selection = None;
this.marked_entries.clear();
}))
.key_context(self.dispatch_context(cx)) .key_context(self.dispatch_context(cx))
.on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev)) .on_action(cx.listener(Self::select_prev))

View File

@@ -78,7 +78,10 @@ impl ProjectSymbolsDelegate {
)); ));
let sort_key_for_match = |mat: &StringMatch| { let sort_key_for_match = |mat: &StringMatch| {
let symbol = &self.symbols[mat.candidate_id]; let symbol = &self.symbols[mat.candidate_id];
(Reverse(OrderedFloat(mat.score)), symbol.label.filter_text()) (
Reverse(OrderedFloat(mat.score)),
&symbol.label.text[symbol.label.filter_range.clone()],
)
}; };
visible_matches.sort_unstable_by_key(sort_key_for_match); visible_matches.sort_unstable_by_key(sort_key_for_match);
@@ -174,7 +177,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, symbol)| { .map(|(id, symbol)| {
StringMatchCandidate::new(id, &symbol.label.filter_text()) StringMatchCandidate::new(
id,
symbol.label.text[symbol.label.filter_range.clone()].to_string(),
)
}) })
.partition(|candidate| { .partition(|candidate| {
project project
@@ -307,7 +313,7 @@ mod tests {
let candidates = fake_symbols let candidates = fake_symbols
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.name)) .map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let matches = if params.query.is_empty() { let matches = if params.query.is_empty() {
Vec::new() Vec::new()

View File

@@ -228,7 +228,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.join(""), .join(""),
}; };
StringMatchCandidate::new(id, &combined_string) StringMatchCandidate::new(id, combined_string)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.matches = smol::block_on(fuzzy::match_strings( self.matches = smol::block_on(fuzzy::match_strings(

View File

@@ -98,7 +98,7 @@ impl PickerDelegate for KernelPickerDelegate {
if query.is_empty() { if query.is_empty() {
self.filtered_kernels = all_kernels; self.filtered_kernels = all_kernels;
return Task::ready(()); return Task::Ready(Some(()));
} }
self.filtered_kernels = if query.is_empty() { self.filtered_kernels = if query.is_empty() {
@@ -110,7 +110,7 @@ impl PickerDelegate for KernelPickerDelegate {
.collect() .collect()
}; };
return Task::ready(()); return Task::Ready(Some(()));
} }
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) { fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {

View File

@@ -96,7 +96,7 @@ impl ScopeSelectorDelegate {
let candidates = candidates let candidates = candidates
.chain(languages) .chain(languages)
.enumerate() .enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name)) .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Self { Self {

View File

@@ -22,7 +22,11 @@ impl Delegate {
.iter() .iter()
.copied() .copied()
.enumerate() .enumerate()
.map(|(id, string)| StringMatchCandidate::new(id, string)) .map(|(id, string)| StringMatchCandidate {
id,
char_bag: string.into(),
string: string.into(),
})
.collect(), .collect(),
matches: vec![], matches: vec![],
selected_ix: 0, selected_ix: 0,

View File

@@ -21,7 +21,6 @@ serde_json_lenient.workspace = true
sha2.workspace = true sha2.workspace = true
shellexpand.workspace = true shellexpand.workspace = true
util.workspace = true util.workspace = true
zed_actions.workspace = true
[dev-dependencies] [dev-dependencies]
gpui = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] }

View File

@@ -15,7 +15,6 @@ use std::str::FromStr;
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates}; pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
pub use vscode_format::VsCodeTaskFile; pub use vscode_format::VsCodeTaskFile;
pub use zed_actions::RevealTarget;
/// Task identifier, unique within the application. /// Task identifier, unique within the application.
/// Based on it, task reruns and terminal tabs are managed. /// Based on it, task reruns and terminal tabs are managed.
@@ -48,8 +47,6 @@ pub struct SpawnInTerminal {
pub allow_concurrent_runs: bool, pub allow_concurrent_runs: bool,
/// What to do with the terminal pane and tab, after the command was started. /// What to do with the terminal pane and tab, after the command was started.
pub reveal: RevealStrategy, pub reveal: RevealStrategy,
/// Where to show tasks' terminal output.
pub reveal_target: RevealTarget,
/// What to do with the terminal pane and tab, after the command had finished. /// What to do with the terminal pane and tab, after the command had finished.
pub hide: HideStrategy, pub hide: HideStrategy,
/// Which shell to use when spawning the task. /// Which shell to use when spawning the task.

View File

@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt}; use util::{truncate_and_remove_front, ResultExt};
use crate::{ use crate::{
ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName, ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
ZED_VARIABLE_NAME_PREFIX, ZED_VARIABLE_NAME_PREFIX,
}; };
@@ -42,16 +42,10 @@ pub struct TaskTemplate {
#[serde(default)] #[serde(default)]
pub allow_concurrent_runs: bool, pub allow_concurrent_runs: bool,
/// What to do with the terminal pane and tab, after the command was started: /// What to do with the terminal pane and tab, after the command was started:
/// * `always` — always show the task's pane, and focus the corresponding tab in it (default) /// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it /// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
#[serde(default)] #[serde(default)]
pub reveal: RevealStrategy, pub reveal: RevealStrategy,
/// Where to place the task's terminal item after starting the task.
/// * `dock` — in the terminal dock, "regular" terminal items' place (default).
/// * `center` — in the central pane group, "main" editor area.
#[serde(default)]
pub reveal_target: RevealTarget,
/// What to do with the terminal pane and tab, after the command had finished: /// What to do with the terminal pane and tab, after the command had finished:
/// * `never` — do nothing when the command finishes (default) /// * `never` — do nothing when the command finishes (default)
/// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it
@@ -76,12 +70,12 @@ pub struct TaskTemplate {
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RevealStrategy { pub enum RevealStrategy {
/// Always show the task's pane, and focus the corresponding tab in it. /// Always show the terminal pane, add and focus the corresponding task's tab in it.
#[default] #[default]
Always, Always,
/// Always show the task's pane, add the task's tab in it, but don't focus it. /// Always show the terminal pane, add the task's tab in it, but don't focus it.
NoFocus, NoFocus,
/// Do not alter focus, but still add/reuse the task's tab in its pane. /// Do not change terminal pane focus, but still add/reuse the task's tab there.
Never, Never,
} }
@@ -241,7 +235,6 @@ impl TaskTemplate {
use_new_terminal: self.use_new_terminal, use_new_terminal: self.use_new_terminal,
allow_concurrent_runs: self.allow_concurrent_runs, allow_concurrent_runs: self.allow_concurrent_runs,
reveal: self.reveal, reveal: self.reveal,
reveal_target: self.reveal_target,
hide: self.hide, hide: self.hide,
shell: self.shell.clone(), shell: self.shell.clone(),
show_summary: self.show_summary, show_summary: self.show_summary,
@@ -637,7 +630,7 @@ mod tests {
label: "My task".into(), label: "My task".into(),
command: "echo".into(), command: "echo".into(),
args: vec!["$PATH".into()], args: vec!["$PATH".into()],
..TaskTemplate::default() ..Default::default()
}; };
let resolved_task = task let resolved_task = task
.resolve_task(TEST_ID_BASE, &TaskContext::default()) .resolve_task(TEST_ID_BASE, &TaskContext::default())
@@ -655,7 +648,7 @@ mod tests {
label: "My task".into(), label: "My task".into(),
command: "echo".into(), command: "echo".into(),
args: vec!["$ZED_VARIABLE".into()], args: vec!["$ZED_VARIABLE".into()],
..TaskTemplate::default() ..Default::default()
}; };
assert!(task assert!(task
.resolve_task(TEST_ID_BASE, &TaskContext::default()) .resolve_task(TEST_ID_BASE, &TaskContext::default())

View File

@@ -1,9 +1,9 @@
use ::settings::Settings; use ::settings::Settings;
use editor::{tasks::task_context, Editor}; use editor::{tasks::task_context, Editor};
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext}; use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
use modal::{TaskOverrides, TasksModal}; use modal::TasksModal;
use project::{Location, WorktreeId}; use project::{Location, WorktreeId};
use task::{RevealTarget, TaskId}; use task::TaskId;
use workspace::tasks::schedule_task; use workspace::tasks::schedule_task;
use workspace::{tasks::schedule_resolved_task, Workspace}; use workspace::{tasks::schedule_resolved_task, Workspace};
@@ -79,7 +79,7 @@ pub fn init(cx: &mut AppContext) {
); );
} }
} else { } else {
toggle_modal(workspace, None, cx).detach(); toggle_modal(workspace, cx).detach();
}; };
}); });
}, },
@@ -88,25 +88,13 @@ pub fn init(cx: &mut AppContext) {
} }
fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) { fn spawn_task_or_modal(workspace: &mut Workspace, action: &Spawn, cx: &mut ViewContext<Workspace>) {
match action { match &action.task_name {
Spawn::ByName { Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
task_name, None => toggle_modal(workspace, cx).detach(),
reveal_target,
} => {
let overrides = reveal_target.map(|reveal_target| TaskOverrides {
reveal_target: Some(reveal_target),
});
spawn_task_with_name(task_name.clone(), overrides, cx).detach_and_log_err(cx)
}
Spawn::ViaModal { reveal_target } => toggle_modal(workspace, *reveal_target, cx).detach(),
} }
} }
fn toggle_modal( fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
workspace: &mut Workspace,
reveal_target: Option<RevealTarget>,
cx: &mut ViewContext<'_, Workspace>,
) -> AsyncTask<()> {
let task_store = workspace.project().read(cx).task_store().clone(); let task_store = workspace.project().read(cx).task_store().clone();
let workspace_handle = workspace.weak_handle(); let workspace_handle = workspace.weak_handle();
let can_open_modal = workspace.project().update(cx, |project, cx| { let can_open_modal = workspace.project().update(cx, |project, cx| {
@@ -119,15 +107,7 @@ fn toggle_modal(
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| { workspace.toggle_modal(cx, |cx| {
TasksModal::new( TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
task_store.clone(),
task_context,
reveal_target.map(|target| TaskOverrides {
reveal_target: Some(target),
}),
workspace_handle,
cx,
)
}) })
}) })
.ok(); .ok();
@@ -139,7 +119,6 @@ fn toggle_modal(
fn spawn_task_with_name( fn spawn_task_with_name(
name: String, name: String,
overrides: Option<TaskOverrides>,
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) -> AsyncTask<anyhow::Result<()>> { ) -> AsyncTask<anyhow::Result<()>> {
cx.spawn(|workspace, mut cx| async move { cx.spawn(|workspace, mut cx| async move {
@@ -174,13 +153,8 @@ fn spawn_task_with_name(
let did_spawn = workspace let did_spawn = workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
let (task_source_kind, mut target_task) = let (task_source_kind, target_task) =
tasks.into_iter().find(|(_, task)| task.label == name)?; tasks.into_iter().find(|(_, task)| task.label == name)?;
if let Some(overrides) = &overrides {
if let Some(target_override) = overrides.reveal_target {
target_task.reveal_target = target_override;
}
}
schedule_task( schedule_task(
workspace, workspace,
task_source_kind, task_source_kind,
@@ -195,13 +169,7 @@ fn spawn_task_with_name(
if !did_spawn { if !did_spawn {
workspace workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| {
spawn_task_or_modal( spawn_task_or_modal(workspace, &Spawn::default(), cx);
workspace,
&Spawn::ViaModal {
reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
},
cx,
);
}) })
.ok(); .ok();
} }

View File

@@ -9,7 +9,7 @@ use gpui::{
}; };
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate}; use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
use project::{task_store::TaskStore, TaskSourceKind}; use project::{task_store::TaskStore, TaskSourceKind};
use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; use task::{ResolvedTask, TaskContext, TaskTemplate};
use ui::{ use ui::{
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
@@ -24,7 +24,6 @@ pub use zed_actions::{Rerun, Spawn};
pub(crate) struct TasksModalDelegate { pub(crate) struct TasksModalDelegate {
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>, candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
task_overrides: Option<TaskOverrides>,
last_used_candidate_index: Option<usize>, last_used_candidate_index: Option<usize>,
divider_index: Option<usize>, divider_index: Option<usize>,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
@@ -35,28 +34,12 @@ pub(crate) struct TasksModalDelegate {
placeholder_text: Arc<str>, placeholder_text: Arc<str>,
} }
/// Task template amendments to do before resolving the context.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct TaskOverrides {
/// See [`RevealTarget`].
pub(crate) reveal_target: Option<RevealTarget>,
}
impl TasksModalDelegate { impl TasksModalDelegate {
fn new( fn new(
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
task_context: TaskContext, task_context: TaskContext,
task_overrides: Option<TaskOverrides>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
) -> Self { ) -> Self {
let placeholder_text = if let Some(TaskOverrides {
reveal_target: Some(RevealTarget::Center),
}) = &task_overrides
{
Arc::from("Find a task, or run a command in the central pane")
} else {
Arc::from("Find a task, or run a command")
};
Self { Self {
task_store, task_store,
workspace, workspace,
@@ -67,8 +50,7 @@ impl TasksModalDelegate {
selected_index: 0, selected_index: 0,
prompt: String::default(), prompt: String::default(),
task_context, task_context,
task_overrides, placeholder_text: Arc::from("Find a task, or run a command"),
placeholder_text,
} }
} }
@@ -79,17 +61,11 @@ impl TasksModalDelegate {
let source_kind = TaskSourceKind::UserInput; let source_kind = TaskSourceKind::UserInput;
let id_base = source_kind.to_id_base(); let id_base = source_kind.to_id_base();
let mut new_oneshot = TaskTemplate { let new_oneshot = TaskTemplate {
label: self.prompt.clone(), label: self.prompt.clone(),
command: self.prompt.clone(), command: self.prompt.clone(),
..TaskTemplate::default() ..TaskTemplate::default()
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = &self.task_overrides
{
new_oneshot.reveal_target = *reveal_target;
}
Some(( Some((
source_kind, source_kind,
new_oneshot.resolve_task(&id_base, &self.task_context)?, new_oneshot.resolve_task(&id_base, &self.task_context)?,
@@ -124,13 +100,12 @@ impl TasksModal {
pub(crate) fn new( pub(crate) fn new(
task_store: Model<TaskStore>, task_store: Model<TaskStore>,
task_context: TaskContext, task_context: TaskContext,
task_overrides: Option<TaskOverrides>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let picker = cx.new_view(|cx| { let picker = cx.new_view(|cx| {
Picker::uniform_list( Picker::uniform_list(
TasksModalDelegate::new(task_store, task_context, task_overrides, workspace), TasksModalDelegate::new(task_store, task_context, workspace),
cx, cx,
) )
}); });
@@ -282,17 +257,9 @@ impl PickerDelegate for TasksModalDelegate {
.as_ref() .as_ref()
.map(|candidates| candidates[ix].clone()) .map(|candidates| candidates[ix].clone())
}); });
let Some((task_source_kind, mut task)) = task else { let Some((task_source_kind, task)) = task else {
return; return;
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = &self.task_overrides
{
if let Some(resolved_task) = &mut task.resolved {
resolved_task.reveal_target = *reveal_target;
}
}
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
@@ -429,18 +396,9 @@ impl PickerDelegate for TasksModalDelegate {
} }
fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) { fn confirm_input(&mut self, omit_history_entry: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some((task_source_kind, mut task)) = self.spawn_oneshot() else { let Some((task_source_kind, task)) = self.spawn_oneshot() else {
return; return;
}; };
if let Some(TaskOverrides {
reveal_target: Some(reveal_target),
}) = self.task_overrides
{
if let Some(resolved_task) = &mut task.resolved {
resolved_task.reveal_target = reveal_target;
}
}
self.workspace self.workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {
schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx); schedule_resolved_task(workspace, task_source_kind, task, omit_history_entry, cx);
@@ -558,7 +516,7 @@ fn string_match_candidates<'a>(
.map(|(index, (_, candidate))| StringMatchCandidate { .map(|(index, (_, candidate))| StringMatchCandidate {
id: index, id: index,
char_bag: candidate.resolved_label.chars().collect(), char_bag: candidate.resolved_label.chars().collect(),
string: candidate.display_label().into(), string: candidate.display_label().to_owned(),
}) })
.collect() .collect()
} }
@@ -724,9 +682,8 @@ mod tests {
"No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)" "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)"
); );
cx.dispatch_action(Spawn::ByName { cx.dispatch_action(Spawn {
task_name: "example task".to_string(), task_name: Some("example task".to_string()),
reveal_target: None,
}); });
let tasks_picker = workspace.update(cx, |workspace, cx| { let tasks_picker = workspace.update(cx, |workspace, cx| {
workspace workspace
@@ -1037,7 +994,7 @@ mod tests {
workspace: &View<Workspace>, workspace: &View<Workspace>,
cx: &mut VisualTestContext, cx: &mut VisualTestContext,
) -> View<Picker<TasksModalDelegate>> { ) -> View<Picker<TasksModalDelegate>> {
cx.dispatch_action(Spawn::modal()); cx.dispatch_action(Spawn::default());
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
workspace workspace
.active_modal::<TasksModal>(cx) .active_modal::<TasksModal>(cx)

View File

@@ -324,7 +324,6 @@ impl TerminalBuilder {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn new( pub fn new(
working_directory: Option<PathBuf>, working_directory: Option<PathBuf>,
python_venv_directory: Option<PathBuf>,
task: Option<TaskState>, task: Option<TaskState>,
shell: Shell, shell: Shell,
mut env: HashMap<String, String>, mut env: HashMap<String, String>,
@@ -472,7 +471,6 @@ impl TerminalBuilder {
word_regex: RegexSearch::new(WORD_REGEX).unwrap(), word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
vi_mode_enabled: false, vi_mode_enabled: false,
is_ssh_terminal, is_ssh_terminal,
python_venv_directory,
}; };
Ok(TerminalBuilder { Ok(TerminalBuilder {
@@ -621,7 +619,6 @@ pub struct Terminal {
pub breadcrumb_text: String, pub breadcrumb_text: String,
pub pty_info: PtyProcessInfo, pub pty_info: PtyProcessInfo,
title_override: Option<SharedString>, title_override: Option<SharedString>,
pub python_venv_directory: Option<PathBuf>,
scroll_px: Pixels, scroll_px: Pixels,
next_link_id: usize, next_link_id: usize,
selection_phase: SelectionPhase, selection_phase: SelectionPhase,

View File

@@ -251,13 +251,7 @@ async fn deserialize_pane_group(
let terminal = terminal.await.ok()?; let terminal = terminal.await.ok()?;
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
let terminal_view = Box::new(cx.new_view(|cx| { let terminal_view = Box::new(cx.new_view(|cx| {
TerminalView::new( TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx)
terminal,
workspace.clone(),
Some(workspace_id),
project.downgrade(),
cx,
)
})); }));
pane.add_item(terminal_view, true, false, None, cx); pane.add_item(terminal_view, true, false, None, cx);
}) })

View File

@@ -12,7 +12,7 @@ use collections::HashMap;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use futures::future::join_all; use futures::future::join_all;
use gpui::{ use gpui::{
actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, Entity, EventEmitter, actions, Action, AnchorCorner, AnyView, AppContext, AsyncWindowContext, EventEmitter,
ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render, ExternalPaths, FocusHandle, FocusableView, IntoElement, Model, ParentElement, Pixels, Render,
Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
@@ -20,7 +20,7 @@ use itertools::Itertools;
use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId}; use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
use search::{buffer_search::DivRegistrar, BufferSearchBar}; use search::{buffer_search::DivRegistrar, BufferSearchBar};
use settings::Settings; use settings::Settings;
use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId}; use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
use terminal::{ use terminal::{
terminal_settings::{TerminalDockPosition, TerminalSettings}, terminal_settings::{TerminalDockPosition, TerminalSettings},
Terminal, Terminal,
@@ -40,7 +40,7 @@ use workspace::{
SplitUp, SwapPaneInDirection, ToggleZoom, Workspace, SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::Result;
use zed_actions::InlineAssist; use zed_actions::InlineAssist;
const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; const TERMINAL_PANEL_KEY: &str = "TerminalPanel";
@@ -53,7 +53,11 @@ pub fn init(cx: &mut AppContext) {
workspace.register_action(TerminalPanel::new_terminal); workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal); workspace.register_action(TerminalPanel::open_terminal);
workspace.register_action(|workspace, _: &ToggleFocus, cx| { workspace.register_action(|workspace, _: &ToggleFocus, cx| {
if is_enabled_in_workspace(workspace, cx) { if workspace
.panel::<TerminalPanel>(cx)
.as_ref()
.is_some_and(|panel| panel.read(cx).enabled)
{
workspace.toggle_panel_focus::<TerminalPanel>(cx); workspace.toggle_panel_focus::<TerminalPanel>(cx);
} }
}); });
@@ -72,6 +76,7 @@ pub struct TerminalPanel {
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
pending_terminals_to_add: usize, pending_terminals_to_add: usize,
deferred_tasks: HashMap<TaskId, Task<()>>, deferred_tasks: HashMap<TaskId, Task<()>>,
enabled: bool,
assistant_enabled: bool, assistant_enabled: bool,
assistant_tab_bar_button: Option<AnyView>, assistant_tab_bar_button: Option<AnyView>,
} }
@@ -81,6 +86,7 @@ impl TerminalPanel {
let project = workspace.project(); let project = workspace.project();
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx); let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
let center = PaneGroup::new(pane.clone()); let center = PaneGroup::new(pane.clone());
let enabled = project.read(cx).supports_terminal(cx);
cx.focus_view(&pane); cx.focus_view(&pane);
let terminal_panel = Self { let terminal_panel = Self {
center, center,
@@ -92,6 +98,7 @@ impl TerminalPanel {
height: None, height: None,
pending_terminals_to_add: 0, pending_terminals_to_add: 0,
deferred_tasks: HashMap::default(), deferred_tasks: HashMap::default(),
enabled,
assistant_enabled: false, assistant_enabled: false,
assistant_tab_bar_button: None, assistant_tab_bar_button: None,
}; };
@@ -248,10 +255,7 @@ impl TerminalPanel {
terminal_panel terminal_panel
.update(&mut cx, |_, cx| { .update(&mut cx, |_, cx| {
cx.subscribe(&workspace, |terminal_panel, _, e, cx| { cx.subscribe(&workspace, |terminal_panel, _, e, cx| {
if let workspace::Event::SpawnTask { if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
action: spawn_in_terminal,
} = e
{
terminal_panel.spawn_task(spawn_in_terminal, cx); terminal_panel.spawn_task(spawn_in_terminal, cx);
}; };
}) })
@@ -335,13 +339,24 @@ impl TerminalPanel {
self.serialize(cx); self.serialize(cx);
} }
pane::Event::Split(direction) => { pane::Event::Split(direction) => {
let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else { let new_pane = self.new_pane_with_cloned_active_terminal(cx);
return;
};
let pane = pane.clone(); let pane = pane.clone();
let direction = *direction; let direction = *direction;
self.center.split(&pane, &new_pane, direction).log_err(); cx.spawn(move |terminal_panel, mut cx| async move {
cx.focus_view(&new_pane); let Some(new_pane) = new_pane.await else {
return;
};
terminal_panel
.update(&mut cx, |terminal_panel, cx| {
terminal_panel
.center
.split(&pane, &new_pane, direction)
.log_err();
cx.focus_view(&new_pane);
})
.ok();
})
.detach();
} }
pane::Event::Focus => { pane::Event::Focus => {
self.active_pane = pane.clone(); self.active_pane = pane.clone();
@@ -354,56 +369,63 @@ impl TerminalPanel {
fn new_pane_with_cloned_active_terminal( fn new_pane_with_cloned_active_terminal(
&mut self, &mut self,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Option<View<Pane>> { ) -> Task<Option<View<Pane>>> {
let workspace = self.workspace.clone().upgrade()?; let Some(workspace) = self.workspace.clone().upgrade() else {
let workspace = workspace.read(cx); return Task::ready(None);
let database_id = workspace.database_id(); };
let database_id = workspace.read(cx).database_id();
let weak_workspace = self.workspace.clone(); let weak_workspace = self.workspace.clone();
let project = workspace.project().clone(); let project = workspace.read(cx).project().clone();
let (working_directory, python_venv_directory) = self let working_directory = self
.active_pane .active_pane
.read(cx) .read(cx)
.active_item() .active_item()
.and_then(|item| item.downcast::<TerminalView>()) .and_then(|item| item.downcast::<TerminalView>())
.map(|terminal_view| { .and_then(|terminal_view| {
let terminal = terminal_view.read(cx).terminal().read(cx); terminal_view
( .read(cx)
terminal .terminal()
.working_directory() .read(cx)
.or_else(|| default_working_directory(workspace, cx)), .working_directory()
terminal.python_venv_directory.clone(),
)
}) })
.unwrap_or((None, None)); .or_else(|| default_working_directory(workspace.read(cx), cx));
let kind = TerminalKind::Shell(working_directory); let kind = TerminalKind::Shell(working_directory);
let window = cx.window_handle(); let window = cx.window_handle();
let terminal = project cx.spawn(move |terminal_panel, mut cx| async move {
.update(cx, |project, cx| { let terminal = project
project.create_terminal_with_venv(kind, python_venv_directory, window, cx) .update(&mut cx, |project, cx| {
project.create_terminal(kind, window, cx)
})
.log_err()?
.await
.log_err()?;
let terminal_view = Box::new(
cx.new_view(|cx| {
TerminalView::new(terminal.clone(), weak_workspace.clone(), database_id, cx)
})
.ok()?,
);
let pane = terminal_panel
.update(&mut cx, |terminal_panel, cx| {
let pane = new_terminal_pane(
weak_workspace,
project,
terminal_panel.active_pane.read(cx).is_zoomed(),
cx,
);
terminal_panel.apply_tab_bar_buttons(&pane, cx);
pane
})
.ok()?;
pane.update(&mut cx, |pane, cx| {
pane.add_item(terminal_view, true, true, None, cx);
}) })
.ok()?; .ok()?;
let terminal_view = Box::new(cx.new_view(|cx| { Some(pane)
TerminalView::new( })
terminal.clone(),
weak_workspace.clone(),
database_id,
project.downgrade(),
cx,
)
}));
let pane = new_terminal_pane(
weak_workspace,
project,
self.active_pane.read(cx).is_zoomed(),
cx,
);
self.apply_tab_bar_buttons(&pane, cx);
pane.update(cx, |pane, cx| {
pane.add_item(terminal_view, true, true, None, cx);
});
Some(pane)
} }
pub fn open_terminal( pub fn open_terminal(
@@ -428,17 +450,83 @@ impl TerminalPanel {
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) { fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
let mut spawn_task = spawn_in_terminal.clone(); let mut spawn_task = spawn_in_terminal.clone();
let Ok(is_local) = self // Set up shell args unconditionally, as tasks are always spawned inside of a shell.
.workspace let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
.update(cx, |workspace, cx| workspace.project().read(cx).is_local()) Shell::System => {
else { match self
.workspace
.update(cx, |workspace, cx| workspace.project().read(cx).is_local())
{
Ok(local) => {
if local {
retrieve_system_shell().map(|shell| (shell, Vec::new()))
} else {
Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
}
}
Err(_no_window_e) => return,
}
}
Shell::Program(shell) => Some((shell, Vec::new())),
Shell::WithArguments { program, args, .. } => Some((program, args)),
}) else {
return; return;
}; };
if let ControlFlow::Break(_) = #[cfg(target_os = "windows")]
Self::fill_command(is_local, spawn_in_terminal, &mut spawn_task) let windows_shell_type = to_windows_shell_type(&shell);
#[cfg(not(target_os = "windows"))]
{ {
return; spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
} }
#[cfg(target_os = "windows")]
{
use crate::terminal_panel::WindowsShellType;
match windows_shell_type {
WindowsShellType::Powershell => {
spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
}
WindowsShellType::Cmd => {
spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
}
WindowsShellType::Other => {
spawn_task.command_label =
format!("{shell} -i -c '{}'", spawn_task.command_label)
}
}
}
let task_command = std::mem::replace(&mut spawn_task.command, shell);
let task_args = std::mem::take(&mut spawn_task.args);
let combined_command = task_args
.into_iter()
.fold(task_command, |mut command, arg| {
command.push(' ');
#[cfg(not(target_os = "windows"))]
command.push_str(&arg);
#[cfg(target_os = "windows")]
command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
command
});
#[cfg(not(target_os = "windows"))]
user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
#[cfg(target_os = "windows")]
{
use crate::terminal_panel::WindowsShellType;
match windows_shell_type {
WindowsShellType::Powershell => {
user_args.extend(["-C".to_owned(), combined_command])
}
WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
WindowsShellType::Other => {
user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
}
}
}
spawn_task.args = user_args;
let spawn_task = spawn_task; let spawn_task = spawn_task;
let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs; let allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
@@ -467,8 +555,8 @@ impl TerminalPanel {
!use_new_terminal, !use_new_terminal,
"Should have handled 'allow_concurrent_runs && use_new_terminal' case above" "Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
); );
this.update(&mut cx, |terminal_panel, cx| { this.update(&mut cx, |this, cx| {
terminal_panel.replace_terminal( this.replace_terminal(
spawn_task, spawn_task,
task_pane, task_pane,
existing_item_index, existing_item_index,
@@ -514,98 +602,13 @@ impl TerminalPanel {
.detach() .detach()
} }
pub fn fill_command(
is_local: bool,
spawn_in_terminal: &SpawnInTerminal,
spawn_task: &mut SpawnInTerminal,
) -> ControlFlow<()> {
let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
Shell::System => {
if is_local {
retrieve_system_shell().map(|shell| (shell, Vec::new()))
} else {
Some(("\"${SHELL:-sh}\"".to_string(), Vec::new()))
}
}
Shell::Program(shell) => Some((shell, Vec::new())),
Shell::WithArguments { program, args, .. } => Some((program, args)),
}) else {
return ControlFlow::Break(());
};
#[cfg(target_os = "windows")]
let windows_shell_type = to_windows_shell_type(&shell);
#[cfg(not(target_os = "windows"))]
{
spawn_task.command_label = format!("{shell} -i -c '{}'", spawn_task.command_label);
}
#[cfg(target_os = "windows")]
{
use crate::terminal_panel::WindowsShellType;
match windows_shell_type {
WindowsShellType::Powershell => {
spawn_task.command_label = format!("{shell} -C '{}'", spawn_task.command_label)
}
WindowsShellType::Cmd => {
spawn_task.command_label = format!("{shell} /C '{}'", spawn_task.command_label)
}
WindowsShellType::Other => {
spawn_task.command_label =
format!("{shell} -i -c '{}'", spawn_task.command_label)
}
}
}
let task_command = std::mem::replace(&mut spawn_task.command, shell);
let task_args = std::mem::take(&mut spawn_task.args);
let combined_command = task_args
.into_iter()
.fold(task_command, |mut command, arg| {
command.push(' ');
#[cfg(not(target_os = "windows"))]
command.push_str(&arg);
#[cfg(target_os = "windows")]
command.push_str(&to_windows_shell_variable(windows_shell_type, arg));
command
});
#[cfg(not(target_os = "windows"))]
user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command]);
#[cfg(target_os = "windows")]
{
use crate::terminal_panel::WindowsShellType;
match windows_shell_type {
WindowsShellType::Powershell => {
user_args.extend(["-C".to_owned(), combined_command])
}
WindowsShellType::Cmd => user_args.extend(["/C".to_owned(), combined_command]),
WindowsShellType::Other => {
user_args.extend(["-i".to_owned(), "-c".to_owned(), combined_command])
}
}
}
spawn_task.args = user_args;
// Set up shell args unconditionally, as tasks are always spawned inside of a shell.
ControlFlow::Continue(())
}
pub fn spawn_in_new_terminal( pub fn spawn_in_new_terminal(
&mut self, &mut self,
spawn_task: SpawnInTerminal, spawn_task: SpawnInTerminal,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
let reveal = spawn_task.reveal; let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target; self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
let kind = TerminalKind::Task(spawn_task);
match reveal_target {
RevealTarget::Center => self
.workspace
.update(cx, |workspace, cx| {
Self::add_center_terminal(workspace, kind, cx)
})
.unwrap_or_else(|e| Task::ready(Err(e))),
RevealTarget::Dock => self.add_terminal(kind, reveal, cx),
}
} }
/// Create a new Terminal in the current working directory or the user's home directory /// Create a new Terminal in the current working directory or the user's home directory
@@ -632,40 +635,24 @@ impl TerminalPanel {
label: &str, label: &str,
cx: &mut AppContext, cx: &mut AppContext,
) -> Vec<(usize, View<Pane>, View<TerminalView>)> { ) -> Vec<(usize, View<Pane>, View<TerminalView>)> {
let Some(workspace) = self.workspace.upgrade() else {
return Vec::new();
};
let pane_terminal_views = |pane: View<Pane>| {
pane.read(cx)
.items()
.enumerate()
.filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
.filter_map(|(index, terminal_view)| {
let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
if &task_state.full_label == label {
Some((index, terminal_view))
} else {
None
}
})
.map(move |(index, terminal_view)| (index, pane.clone(), terminal_view))
};
self.center self.center
.panes() .panes()
.into_iter() .into_iter()
.cloned() .flat_map(|pane| {
.flat_map(pane_terminal_views) pane.read(cx)
.chain( .items()
workspace .enumerate()
.read(cx) .filter_map(|(index, item)| Some((index, item.act_as::<TerminalView>(cx)?)))
.panes() .filter_map(|(index, terminal_view)| {
.into_iter() let task_state = terminal_view.read(cx).terminal().read(cx).task()?;
.cloned() if &task_state.full_label == label {
.flat_map(pane_terminal_views), Some((index, terminal_view))
) } else {
.sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id()) None
}
})
.map(|(index, terminal_view)| (index, pane.clone(), terminal_view))
})
.collect() .collect()
} }
@@ -681,60 +668,23 @@ impl TerminalPanel {
}) })
} }
pub fn add_center_terminal(
workspace: &mut Workspace,
kind: TerminalKind,
cx: &mut ViewContext<Workspace>,
) -> Task<Result<Model<Terminal>>> {
if !is_enabled_in_workspace(workspace, cx) {
return Task::ready(Err(anyhow!(
"terminal not yet supported for remote projects"
)));
}
let window = cx.window_handle();
let project = workspace.project().downgrade();
cx.spawn(move |workspace, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(kind, window, cx)
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal.clone(),
workspace.weak_handle(),
workspace.database_id(),
workspace.project().downgrade(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
})?;
Ok(terminal)
})
}
fn add_terminal( fn add_terminal(
&mut self, &mut self,
kind: TerminalKind, kind: TerminalKind,
reveal_strategy: RevealStrategy, reveal_strategy: RevealStrategy,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<Model<Terminal>>> { ) -> Task<Result<Model<Terminal>>> {
if !self.enabled {
return Task::ready(Err(anyhow::anyhow!(
"terminal not yet supported for remote projects"
)));
}
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
self.pending_terminals_to_add += 1; self.pending_terminals_to_add += 1;
cx.spawn(|terminal_panel, mut cx| async move { cx.spawn(|terminal_panel, mut cx| async move {
if workspace.update(&mut cx, |workspace, cx| { let pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
!is_enabled_in_workspace(workspace, cx)
})? {
anyhow::bail!("terminal not yet supported for remote projects");
}
let pane = terminal_panel.update(&mut cx, |terminal_panel, _| {
terminal_panel.pending_terminals_to_add += 1;
terminal_panel.active_pane.clone()
})?;
let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?; let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
let window = cx.window_handle(); let window = cx.window_handle();
let terminal = project let terminal = project
@@ -748,7 +698,6 @@ impl TerminalPanel {
terminal.clone(), terminal.clone(),
workspace.weak_handle(), workspace.weak_handle(),
workspace.database_id(), workspace.database_id(),
workspace.project().downgrade(),
cx, cx,
) )
})); }));
@@ -825,11 +774,10 @@ impl TerminalPanel {
cx: &mut ViewContext<'_, Self>, cx: &mut ViewContext<'_, Self>,
) -> Task<Option<()>> { ) -> Task<Option<()>> {
let reveal = spawn_task.reveal; let reveal = spawn_task.reveal;
let reveal_target = spawn_task.reveal_target;
let window = cx.window_handle(); let window = cx.window_handle();
let task_workspace = self.workspace.clone(); let task_workspace = self.workspace.clone();
cx.spawn(move |terminal_panel, mut cx| async move { cx.spawn(move |this, mut cx| async move {
let project = terminal_panel let project = this
.update(&mut cx, |this, cx| { .update(&mut cx, |this, cx| {
this.workspace this.workspace
.update(cx, |workspace, _| workspace.project().clone()) .update(cx, |workspace, _| workspace.project().clone())
@@ -851,68 +799,32 @@ impl TerminalPanel {
.ok()?; .ok()?;
match reveal { match reveal {
RevealStrategy::Always => match reveal_target { RevealStrategy::Always => {
RevealTarget::Center => { this.update(&mut cx, |this, cx| {
task_workspace this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
.update(&mut cx, |workspace, cx| { })
workspace .ok()?;
.active_item(cx)
.context("retrieving active terminal item in the workspace")
.log_err()?
.focus_handle(cx)
.focus(cx);
Some(())
})
.ok()??;
}
RevealTarget::Dock => {
terminal_panel
.update(&mut cx, |terminal_panel, cx| {
terminal_panel.activate_terminal_view(
&task_pane,
terminal_item_index,
true,
cx,
)
})
.ok()?;
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
task_workspace
.update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
.ok()
})
.detach();
}
},
RevealStrategy::NoFocus => match reveal_target {
RevealTarget::Center => {
task_workspace task_workspace
.update(&mut cx, |workspace, cx| { .update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
workspace.active_pane().focus_handle(cx).focus(cx); .ok()
}) })
.ok()?; .detach();
} }
RevealTarget::Dock => { RevealStrategy::NoFocus => {
terminal_panel this.update(&mut cx, |this, cx| {
.update(&mut cx, |terminal_panel, cx| { this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
terminal_panel.activate_terminal_view( })
&task_pane, .ok()?;
terminal_item_index,
false,
cx,
)
})
.ok()?;
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
task_workspace task_workspace
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx)) .update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
.ok() .ok()
}) })
.detach(); .detach();
} }
},
RevealStrategy::Never => {} RevealStrategy::Never => {}
} }
@@ -927,16 +839,6 @@ impl TerminalPanel {
pub fn assistant_enabled(&self) -> bool { pub fn assistant_enabled(&self) -> bool {
self.assistant_enabled self.assistant_enabled
} }
fn is_enabled(&self, cx: &WindowContext) -> bool {
self.workspace.upgrade().map_or(false, |workspace| {
is_enabled_in_workspace(workspace.read(cx), cx)
})
}
}
fn is_enabled_in_workspace(workspace: &Workspace, cx: &WindowContext) -> bool {
workspace.project().read(cx).supports_terminal(cx)
} }
pub fn new_terminal_pane( pub fn new_terminal_pane(
@@ -997,7 +899,8 @@ pub fn new_terminal_pane(
pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| { pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() { if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
let this_pane = cx.view().clone(); let this_pane = cx.view().clone();
let item = if tab.pane == this_pane { let belongs_to_this_pane = tab.pane == this_pane;
let item = if belongs_to_this_pane {
pane.item_for_index(tab.ix) pane.item_for_index(tab.ix)
} else { } else {
tab.pane.read(cx).item_for_index(tab.ix) tab.pane.read(cx).item_for_index(tab.ix)
@@ -1007,57 +910,53 @@ pub fn new_terminal_pane(
let source = tab.pane.clone(); let source = tab.pane.clone();
let item_id_to_move = item.item_id(); let item_id_to_move = item.item_id();
let new_split_pane = pane let new_pane = pane.drag_split_direction().and_then(|split_direction| {
.drag_split_direction() terminal_panel.update(cx, |terminal_panel, cx| {
.map(|split_direction| { let is_zoomed = if terminal_panel.active_pane == this_pane {
terminal_panel.update(cx, |terminal_panel, cx| { pane.is_zoomed()
let is_zoomed = if terminal_panel.active_pane == this_pane { } else {
pane.is_zoomed() terminal_panel.active_pane.read(cx).is_zoomed()
} else { };
terminal_panel.active_pane.read(cx).is_zoomed() let new_pane = new_terminal_pane(
}; workspace.clone(),
let new_pane = new_terminal_pane( project.clone(),
workspace.clone(), is_zoomed,
project.clone(), cx,
is_zoomed, );
cx, terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
); terminal_panel
terminal_panel.apply_tab_bar_buttons(&new_pane, cx); .center
terminal_panel.center.split( .split(&this_pane, &new_pane, split_direction)
&this_pane, .log_err()?;
&new_pane, Some(new_pane)
split_direction,
)?;
anyhow::Ok(new_pane)
})
}) })
.transpose(); });
match new_split_pane { let destination;
// Source pane may be the one currently updated, so defer the move. let destination_index;
Ok(Some(new_pane)) => cx if let Some(new_pane) = new_pane {
.spawn(|_, mut cx| async move { destination_index = new_pane.read(cx).active_item_index();
cx.update(|cx| { destination = new_pane;
move_item( } else if belongs_to_this_pane {
&source, return ControlFlow::Break(());
&new_pane, } else {
item_id_to_move, destination = cx.view().clone();
new_pane.read(cx).active_item_index(), destination_index = pane.active_item_index();
cx, }
); // Destination pane may be the one currently updated, so defer the move.
}) cx.spawn(|_, mut cx| async move {
.ok(); cx.update(|cx| {
}) move_item(
.detach(), &source,
// If we drop into existing pane or current pane, &destination,
// regular pane drop handler will take care of it, item_id_to_move,
// using the right tab index for the operation. destination_index,
Ok(None) => return ControlFlow::Continue(()), cx,
err @ Err(_) => { );
err.log_err(); })
return ControlFlow::Break(()); .ok();
} })
}; .detach();
} else if let Some(project_path) = item.project_path(cx) { } else if let Some(project_path) = item.project_path(cx) {
if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) if let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx)
{ {
@@ -1204,19 +1103,25 @@ impl Render for TerminalPanel {
if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
cx.focus_view(&pane); cx.focus_view(&pane);
} else { } else {
if let Some(new_pane) = let new_pane = terminal_panel.new_pane_with_cloned_active_terminal(cx);
terminal_panel.new_pane_with_cloned_active_terminal(cx) cx.spawn(|terminal_panel, mut cx| async move {
{ if let Some(new_pane) = new_pane.await {
terminal_panel terminal_panel
.center .update(&mut cx, |terminal_panel, cx| {
.split( terminal_panel
&terminal_panel.active_pane, .center
&new_pane, .split(
SplitDirection::Right, &terminal_panel.active_pane,
) &new_pane,
.log_err(); SplitDirection::Right,
cx.focus_view(&new_pane); )
} .log_err();
cx.focus_view(&new_pane);
})
.ok();
}
})
.detach();
} }
})) }))
.on_action(cx.listener( .on_action(cx.listener(
@@ -1315,7 +1220,7 @@ impl Panel for TerminalPanel {
return; return;
}; };
this.add_terminal(kind, RevealStrategy::Always, cx) this.add_terminal(kind, RevealStrategy::Never, cx)
.detach_and_log_err(cx) .detach_and_log_err(cx)
}) })
} }
@@ -1339,9 +1244,7 @@ impl Panel for TerminalPanel {
} }
fn icon(&self, cx: &WindowContext) -> Option<IconName> { fn icon(&self, cx: &WindowContext) -> Option<IconName> {
if (self.is_enabled(cx) || !self.has_no_terminals(cx)) if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
&& TerminalSettings::get_global(cx).button
{
Some(IconName::Terminal) Some(IconName::Terminal)
} else { } else {
None None

View File

@@ -9,7 +9,7 @@ use gpui::{
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter, anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View,
VisualContext, WeakModel, WeakView, VisualContext, WeakView,
}; };
use language::Bias; use language::Bias;
use persistence::TERMINAL_DB; use persistence::TERMINAL_DB;
@@ -30,6 +30,7 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
use util::{paths::PathWithPosition, ResultExt}; use util::{paths::PathWithPosition, ResultExt};
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams}, item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
notifications::NotifyResultExt,
register_serializable_item, register_serializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace, CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
@@ -77,7 +78,7 @@ pub fn init(cx: &mut AppContext) {
register_serializable_item::<TerminalView>(cx); register_serializable_item::<TerminalView>(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| { cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(TerminalView::deploy); workspace.register_action(TerminalView::deploy);
}) })
.detach(); .detach();
@@ -97,7 +98,6 @@ pub struct BlockContext<'a, 'b> {
pub struct TerminalView { pub struct TerminalView {
terminal: Model<Terminal>, terminal: Model<Terminal>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
project: WeakModel<Project>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
//Currently using iTerm bell, show bell emoji in tab until input is received //Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool, has_bell: bool,
@@ -134,15 +134,44 @@ impl TerminalView {
cx: &mut ViewContext<Workspace>, cx: &mut ViewContext<Workspace>,
) { ) {
let working_directory = default_working_directory(workspace, cx); let working_directory = default_working_directory(workspace, cx);
TerminalPanel::add_center_terminal(workspace, TerminalKind::Shell(working_directory), cx)
.detach_and_log_err(cx); let window = cx.window_handle();
let project = workspace.project().downgrade();
cx.spawn(move |workspace, mut cx| async move {
let terminal = project
.update(&mut cx, |project, cx| {
project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
})
.ok()?
.await;
let terminal = workspace
.update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
.ok()
.flatten()?;
workspace
.update(&mut cx, |workspace, cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
})
.ok();
Some(())
})
.detach()
} }
pub fn new( pub fn new(
terminal: Model<Terminal>, terminal: Model<Terminal>,
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
workspace_id: Option<WorkspaceId>, workspace_id: Option<WorkspaceId>,
project: WeakModel<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
let workspace_handle = workspace.clone(); let workspace_handle = workspace.clone();
@@ -162,7 +191,6 @@ impl TerminalView {
Self { Self {
terminal, terminal,
workspace: workspace_handle, workspace: workspace_handle,
project,
has_bell: false, has_bell: false,
focus_handle, focus_handle,
context_menu: None, context_menu: None,
@@ -1078,37 +1106,21 @@ impl Item for TerminalView {
fn clone_on_split( fn clone_on_split(
&self, &self,
workspace_id: Option<WorkspaceId>, _workspace_id: Option<WorkspaceId>,
cx: &mut ViewContext<Self>, _cx: &mut ViewContext<Self>,
) -> Option<View<Self>> { ) -> Option<View<Self>> {
let window = cx.window_handle(); //From what I can tell, there's no way to tell the current working
let terminal = self //Directory of the terminal from outside the shell. There might be
.project //solutions to this, but they are non-trivial and require more IPC
.update(cx, |project, cx| {
let terminal = self.terminal().read(cx);
let working_directory = terminal
.working_directory()
.or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
let python_venv_directory = terminal.python_venv_directory.clone();
project.create_terminal_with_venv(
TerminalKind::Shell(working_directory),
python_venv_directory,
window,
cx,
)
})
.ok()?
.log_err()?;
Some(cx.new_view(|cx| { // Some(TerminalContainer::new(
TerminalView::new( // Err(anyhow::anyhow!("failed to instantiate terminal")),
terminal, // workspace_id,
self.workspace.clone(), // cx,
workspace_id, // ))
self.project.clone(),
cx, // TODO
) None
}))
} }
fn is_dirty(&self, cx: &gpui::AppContext) -> bool { fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
@@ -1237,15 +1249,7 @@ impl SerializableItem for TerminalView {
})? })?
.await?; .await?;
cx.update(|cx| { cx.update(|cx| {
cx.new_view(|cx| { cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
TerminalView::new(
terminal,
workspace,
Some(workspace_id),
project.downgrade(),
cx,
)
})
}) })
}) })
} }

View File

@@ -230,7 +230,11 @@ impl PickerDelegate for ThemeSelectorDelegate {
.themes .themes
.iter() .iter()
.enumerate() .enumerate()
.map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) .map(|(id, meta)| StringMatchCandidate {
id,
char_bag: meta.name.as_ref().into(),
string: meta.name.to_string(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {

View File

@@ -296,7 +296,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
.map(|(candidate_id, toolchain)| { .map(|(candidate_id, toolchain)| {
let path = Self::relativize_path(toolchain.path, &worktree_root_path); let path = Self::relativize_path(toolchain.path, &worktree_root_path);
let string = format!("{}{}", toolchain.name, path); let string = format!("{}{}", toolchain.name, path);
StringMatchCandidate::new(candidate_id, &string) StringMatchCandidate::new(candidate_id, string)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
match_strings( match_strings(

View File

@@ -38,7 +38,6 @@ pub struct ListItem {
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>, on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
selectable: bool, selectable: bool,
outlined: bool,
overflow_x: bool, overflow_x: bool,
focused: Option<bool>, focused: Option<bool>,
} }
@@ -63,7 +62,6 @@ impl ListItem {
tooltip: None, tooltip: None,
children: SmallVec::new(), children: SmallVec::new(),
selectable: true, selectable: true,
outlined: false,
overflow_x: false, overflow_x: false,
focused: None, focused: None,
} }
@@ -140,11 +138,6 @@ impl ListItem {
self self
} }
pub fn outlined(mut self) -> Self {
self.outlined = true;
self
}
pub fn overflow_x(mut self) -> Self { pub fn overflow_x(mut self) -> Self {
self.overflow_x = true; self.overflow_x = true;
self self
@@ -210,7 +203,6 @@ impl RenderOnce for ListItem {
.child( .child(
h_flex() h_flex()
.id("inner_list_item") .id("inner_list_item")
.group("list_item")
.w_full() .w_full()
.relative() .relative()
.items_center() .items_center()
@@ -220,6 +212,7 @@ impl RenderOnce for ListItem {
ListItemSpacing::Dense => this, ListItemSpacing::Dense => this,
ListItemSpacing::Sparse => this.py_1(), ListItemSpacing::Sparse => this.py_1(),
}) })
.group("list_item")
.when(self.inset && !self.disabled, |this| { .when(self.inset && !self.disabled, |this| {
this this
// TODO: Add focus state // TODO: Add focus state
@@ -245,12 +238,6 @@ impl RenderOnce for ListItem {
.when_some(self.on_click, |this, on_click| { .when_some(self.on_click, |this, on_click| {
this.cursor_pointer().on_click(on_click) this.cursor_pointer().on_click(on_click)
}) })
.when(self.outlined, |this| {
this.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.overflow_hidden()
})
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
this.on_mouse_down(MouseButton::Right, move |event, cx| { this.on_mouse_down(MouseButton::Right, move |event, cx| {
(on_mouse_down)(event, cx) (on_mouse_down)(event, cx)

View File

@@ -172,7 +172,11 @@ impl PickerDelegate for BranchListDelegate {
branches branches
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name)) .map(|(ix, command)| StringMatchCandidate {
id: ix,
char_bag: command.name.chars().collect(),
string: command.name.into(),
})
.collect::<Vec<StringMatchCandidate>>() .collect::<Vec<StringMatchCandidate>>()
}); });
let Some(candidates) = candidates.log_err() else { let Some(candidates) = candidates.log_err() else {

View File

@@ -22,7 +22,7 @@ impl Vim {
if count <= 1 || Vim::globals(cx).dot_replaying { if count <= 1 || Vim::globals(cx).dot_replaying {
self.create_mark("^".into(), false, cx); self.create_mark("^".into(), false, cx);
self.update_editor(cx, |_, editor, cx| { self.update_editor(cx, |_, editor, cx| {
editor.dismiss_menus_and_popups(true, false, cx); editor.dismiss_menus_and_popups(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| { s.move_cursors_with(|map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1); *cursor.column_mut() = cursor.column().saturating_sub(1);

View File

@@ -626,23 +626,10 @@ impl Vim {
pub fn cursor_shape(&self) -> CursorShape { pub fn cursor_shape(&self) -> CursorShape {
match self.mode { match self.mode {
Mode::Normal => { Mode::Normal => {
if let Some(operator) = self.operator_stack.last() { if self.operator_stack.is_empty() {
match operator {
// Navigation operators -> Block cursor
Operator::FindForward { .. }
| Operator::FindBackward { .. }
| Operator::Mark
| Operator::Jump { .. }
| Operator::Register
| Operator::RecordRegister
| Operator::ReplayRegister => CursorShape::Block,
// All other operators -> Underline cursor
_ => CursorShape::Underline,
}
} else {
// No operator active -> Block cursor
CursorShape::Block CursorShape::Block
} else {
CursorShape::Underline
} }
} }
Mode::Replace => CursorShape::Underline, Mode::Replace => CursorShape::Underline,

View File

@@ -127,7 +127,11 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
let background = cx.background_executor().clone(); let background = cx.background_executor().clone();
let candidates = BaseKeymap::names() let candidates = BaseKeymap::names()
.enumerate() .enumerate()
.map(|(id, name)| StringMatchCandidate::new(id, name)) .map(|(id, name)| StringMatchCandidate {
id,
char_bag: name.into(),
string: name.into(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {

View File

@@ -896,8 +896,6 @@ impl Pane {
destination_index: Option<usize>, destination_index: Option<usize>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.close_items_over_max_tabs(cx);
if item.is_singleton(cx) { if item.is_singleton(cx) {
if let Some(&entry_id) = item.project_entry_ids(cx).first() { if let Some(&entry_id) = item.project_entry_ids(cx).first() {
let project = self.project.read(cx); let project = self.project.read(cx);
@@ -1300,43 +1298,6 @@ impl Pane {
)) ))
} }
pub fn close_items_over_max_tabs(&mut self, cx: &mut ViewContext<Self>) {
let Some(max_tabs) = WorkspaceSettings::get_global(cx).max_tabs.map(|i| i.get()) else {
return;
};
// Reduce over the activation history to get every dirty items up to max_tabs
// count.
let mut index_list = Vec::new();
let mut items_len = self.items_len();
let mut indexes: HashMap<EntityId, usize> = HashMap::default();
for (index, item) in self.items.iter().enumerate() {
indexes.insert(item.item_id(), index);
}
for entry in self.activation_history.iter() {
if items_len < max_tabs {
break;
}
let Some(&index) = indexes.get(&entry.entity_id) else {
continue;
};
if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) {
continue;
}
index_list.push(index);
items_len -= 1;
}
// The sort and reverse is necessary since we remove items
// using their index position, hence removing from the end
// of the list first to avoid changing indexes.
index_list.sort_unstable();
index_list
.iter()
.rev()
.for_each(|&index| self._remove_item(index, false, false, None, cx));
}
pub(super) fn file_names_for_prompt( pub(super) fn file_names_for_prompt(
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>, items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
all_dirty_items: usize, all_dirty_items: usize,
@@ -1468,7 +1429,7 @@ impl Pane {
// Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal. // Always propose to save singleton files without any project paths: those cannot be saved via multibuffer, as require a file path selection modal.
|| cx || cx
.update(|cx| { .update(|cx| {
item_to_close.can_save(cx) && item_to_close.is_dirty(cx) item_to_close.is_dirty(cx)
&& item_to_close.is_singleton(cx) && item_to_close.is_singleton(cx)
&& item_to_close.project_path(cx).is_none() && item_to_close.project_path(cx).is_none()
}) })
@@ -3321,8 +3282,6 @@ impl Render for DraggedTab {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::num::NonZero;
use super::*; use super::*;
use crate::item::test::{TestItem, TestProjectItem}; use crate::item::test::{TestItem, TestProjectItem};
use gpui::{TestAppContext, VisualTestContext}; use gpui::{TestAppContext, VisualTestContext};
@@ -3346,54 +3305,6 @@ mod tests {
}); });
} }
#[gpui::test]
async fn test_add_item_capped_to_max_tabs(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, None, cx).await;
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
for i in 0..7 {
add_labeled_item(&pane, format!("{}", i).as_str(), false, cx);
}
set_max_tabs(cx, Some(5));
add_labeled_item(&pane, "7", false, cx);
// Remove items to respect the max tab cap.
assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx);
pane.update(cx, |pane, cx| {
pane.activate_item(0, false, false, cx);
});
add_labeled_item(&pane, "X", false, cx);
// Respect activation order.
assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx);
for i in 0..7 {
add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx);
}
// Keeps dirty items, even over max tab cap.
assert_item_labels(
&pane,
["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"],
cx,
);
set_max_tabs(cx, None);
for i in 0..7 {
add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx);
}
// No cap when max tabs is None.
assert_item_labels(
&pane,
[
"D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4",
"N5", "N6*",
],
cx,
);
}
#[gpui::test] #[gpui::test]
async fn test_add_item_with_new_item(cx: &mut TestAppContext) { async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
@@ -4025,8 +3936,11 @@ mod tests {
cx.executor().run_until_parked(); cx.executor().run_until_parked();
cx.simulate_prompt_answer(2); cx.simulate_prompt_answer(2);
cx.executor().run_until_parked();
cx.simulate_prompt_answer(2);
cx.executor().run_until_parked();
save.await.unwrap(); save.await.unwrap();
assert_item_labels(&pane, [], cx); assert_item_labels(&pane, ["A*^", "B^", "C^"], cx);
} }
#[gpui::test] #[gpui::test]
@@ -4070,14 +3984,6 @@ mod tests {
}); });
} }
fn set_max_tabs(cx: &mut TestAppContext, value: Option<usize>) {
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<WorkspaceSettings>(cx, |settings| {
settings.max_tabs = value.map(|v| NonZero::new(v).unwrap())
});
});
}
fn add_labeled_item( fn add_labeled_item(
pane: &View<Pane>, pane: &View<Pane>,
label: &str, label: &str,

View File

@@ -6,7 +6,7 @@ use ui::ViewContext;
use crate::Workspace; use crate::Workspace;
pub fn schedule_task( pub fn schedule_task(
workspace: &mut Workspace, workspace: &Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,
task_to_resolve: &TaskTemplate, task_to_resolve: &TaskTemplate,
task_cx: &TaskContext, task_cx: &TaskContext,
@@ -40,7 +40,7 @@ pub fn schedule_task(
} }
pub fn schedule_resolved_task( pub fn schedule_resolved_task(
workspace: &mut Workspace, workspace: &Workspace,
task_source_kind: TaskSourceKind, task_source_kind: TaskSourceKind,
mut resolved_task: ResolvedTask, mut resolved_task: ResolvedTask,
omit_history: bool, omit_history: bool,
@@ -59,9 +59,6 @@ pub fn schedule_resolved_task(
} }
}); });
} }
cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal)));
cx.emit(crate::Event::SpawnTask {
action: Box::new(spawn_in_terminal),
});
} }
} }

View File

@@ -686,9 +686,7 @@ pub enum Event {
}, },
ContactRequestedJoin(u64), ContactRequestedJoin(u64),
WorkspaceCreated(WeakView<Workspace>), WorkspaceCreated(WeakView<Workspace>),
SpawnTask { SpawnTask(Box<SpawnInTerminal>),
action: Box<SpawnInTerminal>,
},
OpenBundledFile { OpenBundledFile {
text: Cow<'static, str>, text: Cow<'static, str>,
title: &'static str, title: &'static str,
@@ -1651,7 +1649,7 @@ impl Workspace {
F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T, F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{ {
if self.project.read(cx).is_local() { if self.project.read(cx).is_local() {
Task::ready(Ok(callback(self, cx))) Task::Ready(Some(Ok(callback(self, cx))))
} else { } else {
let env = self.project.read(cx).cli_environment(cx); let env = self.project.read(cx).cli_environment(cx);
let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx); let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);

View File

@@ -1,5 +1,3 @@
use std::num::NonZeroUsize;
use anyhow::Result; use anyhow::Result;
use collections::HashMap; use collections::HashMap;
use gpui::AppContext; use gpui::AppContext;
@@ -22,7 +20,6 @@ pub struct WorkspaceSettings {
pub use_system_path_prompts: bool, pub use_system_path_prompts: bool,
pub command_aliases: HashMap<String, String>, pub command_aliases: HashMap<String, String>,
pub show_user_picture: bool, pub show_user_picture: bool,
pub max_tabs: Option<NonZeroUsize>,
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -87,15 +84,15 @@ pub enum RestoreOnStartupBehavior {
pub struct WorkspaceSettingsContent { pub struct WorkspaceSettingsContent {
/// Active pane styling settings. /// Active pane styling settings.
pub active_pane_modifiers: Option<ActivePanelModifiers>, pub active_pane_modifiers: Option<ActivePanelModifiers>,
/// Direction to split horizontally. // Direction to split horizontally.
/// //
/// Default: "up" // Default: "up"
pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>, pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>,
/// Direction to split vertically. // Direction to split vertically.
/// //
/// Default: "left" // Default: "left"
pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>, pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>,
/// Centered layout related settings. // Centered layout related settings.
pub centered_layout: Option<CenteredLayoutSettings>, pub centered_layout: Option<CenteredLayoutSettings>,
/// Whether or not to prompt the user to confirm before closing the application. /// Whether or not to prompt the user to confirm before closing the application.
/// ///
@@ -136,11 +133,6 @@ pub struct WorkspaceSettingsContent {
/// ///
/// Default: true /// Default: true
pub show_user_picture: Option<bool>, pub show_user_picture: Option<bool>,
// Maximum open tabs in a pane. Will not close an unsaved
// tab. Set to `None` for unlimited tabs.
//
// Default: none
pub max_tabs: Option<NonZeroUsize>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -2533,12 +2533,6 @@ impl Snapshot {
self.entry_for_path("") self.entry_for_path("")
} }
pub fn root_dir(&self) -> Option<Arc<Path>> {
self.root_entry()
.filter(|entry| entry.is_dir())
.map(|_| self.abs_path().clone())
}
pub fn root_name(&self) -> &str { pub fn root_name(&self) -> &str {
&self.root_name &self.root_name
} }
@@ -3118,7 +3112,7 @@ impl BackgroundScannerState {
let t0 = Instant::now(); let t0 = Instant::now();
let repository = fs.open_repo(&dot_git_abs_path)?; let repository = fs.open_repo(&dot_git_abs_path)?;
let actual_repo_path = repository.dot_git_dir(); let actual_repo_path = repository.path();
let actual_dot_git_dir_abs_path = smol::block_on(find_git_dir(&actual_repo_path, fs))?; let actual_dot_git_dir_abs_path = smol::block_on(find_git_dir(&actual_repo_path, fs))?;
watcher.add(&actual_repo_path).log_err()?; watcher.add(&actual_repo_path).log_err()?;

View File

@@ -10,5 +10,4 @@ workspace = true
[dependencies] [dependencies]
gpui.workspace = true gpui.workspace = true
schemars.workspace = true
serde.workspace = true serde.workspace = true

View File

@@ -1,6 +1,5 @@
use gpui::{actions, impl_actions}; use gpui::{actions, impl_actions};
use schemars::JsonSchema; use serde::Deserialize;
use serde::{Deserialize, Serialize};
// If the zed binary doesn't use anything in this crate, it will be optimized away // If the zed binary doesn't use anything in this crate, it will be optimized away
// and the actions won't initialize. So we just provide an empty initialization function // and the actions won't initialize. So we just provide an empty initialization function
@@ -91,40 +90,19 @@ pub struct OpenRecent {
gpui::impl_actions!(projects, [OpenRecent]); gpui::impl_actions!(projects, [OpenRecent]);
gpui::actions!(projects, [OpenRemote]); gpui::actions!(projects, [OpenRemote]);
/// Where to spawn the task in the UI.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RevealTarget {
/// In the central pane group, "main" editor area.
Center,
/// In the terminal dock, "regular" terminal items' place.
#[default]
Dock,
}
/// Spawn a task with name or open tasks modal /// Spawn a task with name or open tasks modal
#[derive(Debug, PartialEq, Clone, Deserialize)] #[derive(PartialEq, Clone, Deserialize, Default)]
#[serde(untagged)] pub struct Spawn {
pub enum Spawn { #[serde(default)]
/// Spawns a task by the name given. /// Name of the task to spawn.
ByName { /// If it is not set, a modal with a list of available tasks is opened instead.
task_name: String, /// Defaults to None.
#[serde(default)] pub task_name: Option<String>,
reveal_target: Option<RevealTarget>,
},
/// Spawns a task via modal's selection.
ViaModal {
/// Selected task's `reveal_target` property override.
#[serde(default)]
reveal_target: Option<RevealTarget>,
},
} }
impl Spawn { impl Spawn {
pub fn modal() -> Self { pub fn modal() -> Self {
Self::ViaModal { Self { task_name: None }
reveal_target: None,
}
} }
} }

View File

@@ -13,7 +13,7 @@ This extension can be [installed as a dev extension](./developing-extensions.htm
A given extension may provide one or more context servers. Each context server must be registered in the `extension.toml`: A given extension may provide one or more context servers. Each context server must be registered in the `extension.toml`:
```toml ```toml
[context_servers.my-context-server] [context-servers.my-context-server]
``` ```
Then, in the Rust code for your extension, implement the `context_server_command` method on your extension: Then, in the Rust code for your extension, implement the `context_server_command` method on your extension:

View File

@@ -13,18 +13,6 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp
`phpactor` is enabled by default. `phpactor` is enabled by default.
## Phpactor
The Zed PHP Extension can install `phpactor` automatically but requires `php` to installed and available in your path:
```sh
# brew install php # macOS
# sudo apt-get install php # Debian/Ubuntu
# yum install php # CentOS/RHEL
# pacman -S php # Arch Linux
which php
```
## Intelephense ## Intelephense
[Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/). To use these features you must place your [license.txt file](https://intelephense.com/faq.html) at `~/intelephense/licence.txt` inside your home directory. [Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/). To use these features you must place your [license.txt file](https://intelephense.com/faq.html) at `~/intelephense/licence.txt` inside your home directory.

View File

@@ -17,9 +17,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`. // Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish, defaults to `false`.
"allow_concurrent_runs": false, "allow_concurrent_runs": false,
// What to do with the terminal pane and tab, after the command was started: // What to do with the terminal pane and tab, after the command was started:
// * `always` — always show the task's pane, and focus the corresponding tab in it (default) // * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it // * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane // * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
"reveal": "always", "reveal": "always",
// What to do with the terminal pane and tab, after the command had finished: // What to do with the terminal pane and tab, after the command had finished:
// * `never` — Do nothing when the command finishes (default) // * `never` — Do nothing when the command finishes (default)
@@ -155,30 +155,6 @@ You can define your own keybindings for your tasks via additional argument to `t
} }
``` ```
Note that these tasks can also have a 'target' specified to control where the spawned task should show up.
This could be useful for launching a terminal application that you want to use in the center area:
```json
// In tasks.json
{
"label": "start lazygit",
"command": "lazygit -p $ZED_WORKTREE_ROOT"
}
```
```json
// In keymap.json
{
"context": "Workspace",
"bindings": {
"alt-g": [
"task::Spawn",
{ "task_name": "start lazygit", "reveal_target": "center" }
]
}
}
```
## Binding runnable tags to task templates ## Binding runnable tags to task templates
Zed supports overriding default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy: Zed supports overriding default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy: