Compare commits
9 Commits
show-lua-s
...
git-panel-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3e04867ab | ||
|
|
3abf165c1d | ||
|
|
48eae645f2 | ||
|
|
78b6cae754 | ||
|
|
1cac4b6c00 | ||
|
|
aee641d79d | ||
|
|
4f439ae35f | ||
|
|
c2eea3a474 | ||
|
|
695f06c020 |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2768,6 +2768,7 @@ dependencies = [
|
||||
"language",
|
||||
"menu",
|
||||
"notifications",
|
||||
"parking_lot",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
@@ -5181,14 +5182,17 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"db",
|
||||
"editor",
|
||||
"git",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"windows 0.58.0",
|
||||
@@ -12634,7 +12638,6 @@ dependencies = [
|
||||
"sha2",
|
||||
"shellexpand 2.1.2",
|
||||
"util",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16107,7 +16110,6 @@ name = "zed_actions"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 303 B |
@@ -17,7 +17,6 @@
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
@@ -426,10 +425,7 @@
|
||||
"ctrl-shift-r": "task::Rerun",
|
||||
"ctrl-alt-r": "task::Rerun",
|
||||
"alt-t": "task::Rerun",
|
||||
"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" }]
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -470,25 +466,22 @@
|
||||
"enter": "editor::ConfirmRename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion",
|
||||
"shift-tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"cmd-escape": "menu::Cancel",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
@@ -495,9 +494,8 @@
|
||||
"bindings": {
|
||||
"cmd-shift-r": "task::Spawn",
|
||||
"cmd-alt-r": "task::Rerun",
|
||||
"ctrl-alt-shift-r": ["task::Spawn", { "reveal_target": "center" }]
|
||||
// also possible to spawn tasks by name:
|
||||
// "foo-bar": ["task_name::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }]
|
||||
"alt-t": "task::Spawn",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -539,25 +537,22 @@
|
||||
"enter": "editor::ConfirmRename"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && !inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && showing_completions",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "editor::ConfirmCompletion",
|
||||
"tab": "editor::ComposeCompletion",
|
||||
"shift-tab": "editor::AcceptInlineCompletion"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && inline_completion && !showing_completions",
|
||||
"context": "Editor && inline_completion",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"tab": "editor::AcceptInlineCompletion"
|
||||
|
||||
@@ -101,8 +101,6 @@
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Time to wait before showing the informational hover box
|
||||
"hover_popover_delay": 350,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// 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:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Maximum number of tabs per pane. Unset for unlimited.
|
||||
"max_tabs": null,
|
||||
// Settings related to the editor's tab bar.
|
||||
"tab_bar": {
|
||||
// Whether or not to show the tab bar in the editor
|
||||
|
||||
@@ -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`.
|
||||
"allow_concurrent_runs": false,
|
||||
// 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)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"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:
|
||||
// * `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
|
||||
|
||||
@@ -716,7 +716,7 @@ impl ContextStore {
|
||||
let candidates = metadata
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, &metadata.title))
|
||||
.map(|(id, metadata)| StringMatchCandidate::new(id, metadata.title.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
|
||||
@@ -47,7 +47,6 @@ use std::{
|
||||
iter, mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
@@ -175,7 +174,7 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
Arc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
@@ -1442,15 +1441,6 @@ impl Render for PromptEditor {
|
||||
]
|
||||
}
|
||||
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![
|
||||
IconButton::new("cancel", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
@@ -1460,22 +1450,23 @@ impl Render for PromptEditor {
|
||||
cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
|
||||
)
|
||||
.into_any_element(),
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Muted)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Regenerate Transformation",
|
||||
Some(restart_key),
|
||||
"Current change will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element(),
|
||||
if !must_rerun {
|
||||
if self.edited_since_done || matches!(status, CodegenStatus::Error(_)) {
|
||||
IconButton::new("restart", IconName::RotateCw)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(|cx| {
|
||||
Tooltip::with_meta(
|
||||
"Restart Transformation",
|
||||
Some(&menu::Confirm),
|
||||
"Changes will be discarded",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(cx.listener(|_, _, cx| {
|
||||
cx.emit(PromptEditorEvent::StartRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
IconButton::new("confirm", IconName::Check)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
@@ -1484,8 +1475,6 @@ impl Render for PromptEditor {
|
||||
cx.emit(PromptEditorEvent::ConfirmRequested);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -1502,7 +1491,6 @@ impl Render for PromptEditor {
|
||||
.py(cx.line_height() / 2.5)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.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_down))
|
||||
.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>) {
|
||||
match self.codegen.read(cx).status(cx) {
|
||||
CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
|
||||
|
||||
@@ -1439,7 +1439,10 @@ impl PromptStore {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ix, metadata)| {
|
||||
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?))
|
||||
Some(StringMatchCandidate::new(
|
||||
ix,
|
||||
metadata.title.as_ref()?.to_string(),
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = fuzzy::match_strings(
|
||||
|
||||
@@ -7,13 +7,11 @@ use editor::{CompletionProvider, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{AppContext, Model, Task, ViewContext, WeakView, WindowContext};
|
||||
use language::{Anchor, Buffer, CodeLabel, Documentation, HighlightId, LanguageServerId, ToPoint};
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use project::CompletionIntent;
|
||||
use rope::Point;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
@@ -80,7 +78,11 @@ impl SlashCommandCompletionProvider {
|
||||
.command_names(cx)
|
||||
.into_iter()
|
||||
.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<_>>();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
@@ -324,7 +326,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
|
||||
&self,
|
||||
_: Model<Buffer>,
|
||||
_: Vec<usize>,
|
||||
_: Rc<RefCell<Box<[project::Completion]>>>,
|
||||
_: Arc<RwLock<Box<[project::Completion]>>>,
|
||||
_: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
Task::ready(Ok(true))
|
||||
|
||||
@@ -218,7 +218,10 @@ impl Options {
|
||||
}
|
||||
|
||||
fn match_candidates_for_args() -> [StringMatchCandidate; 1] {
|
||||
[StringMatchCandidate::new(0, INCLUDE_WARNINGS_ARGUMENT)]
|
||||
[StringMatchCandidate::new(
|
||||
0,
|
||||
INCLUDE_WARNINGS_ARGUMENT.to_string(),
|
||||
)]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +249,11 @@ fn tab_items_for_queries(
|
||||
.enumerate()
|
||||
.filter_map(|(id, (full_path, ..))| {
|
||||
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<_>>();
|
||||
let mut processed_matches = HashSet::default();
|
||||
|
||||
@@ -3,7 +3,6 @@ mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod context;
|
||||
mod context_picker;
|
||||
mod context_strip;
|
||||
mod inline_assistant;
|
||||
mod message_editor;
|
||||
mod prompts;
|
||||
|
||||
@@ -94,9 +94,7 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| {
|
||||
MessageEditor::new(workspace, thread_store.downgrade(), thread.clone(), cx)
|
||||
}),
|
||||
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
|
||||
tools,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
@@ -125,14 +123,8 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
@@ -154,14 +146,8 @@ impl AssistantPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor = cx.new_view(|cx| {
|
||||
MessageEditor::new(
|
||||
self.workspace.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
thread,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.message_editor =
|
||||
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
|
||||
self.message_editor.focus_handle(cx).focus(cx);
|
||||
}
|
||||
|
||||
@@ -323,11 +309,10 @@ impl AssistantPanel {
|
||||
.when(!recent_threads.is_empty(), |parent| {
|
||||
parent
|
||||
.child(
|
||||
h_flex().w_full().justify_center().child(
|
||||
Label::new("Recent Threads:")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_center()
|
||||
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
|
||||
)
|
||||
.child(
|
||||
v_flex().gap_2().children(
|
||||
|
||||
@@ -24,5 +24,4 @@ pub struct Context {
|
||||
pub enum ContextKind {
|
||||
File,
|
||||
FetchedUrl,
|
||||
Thread,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
mod fetch_context_picker;
|
||||
mod file_context_picker;
|
||||
mod thread_context_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
|
||||
WeakModel, WeakView,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
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::file_context_picker::FileContextPicker;
|
||||
use crate::context_picker::thread_context_picker::ThreadContextPicker;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ContextPickerMode {
|
||||
Default,
|
||||
File(View<FileContextPicker>),
|
||||
Fetch(View<FetchContextPicker>),
|
||||
Thread(View<ThreadContextPicker>),
|
||||
}
|
||||
|
||||
pub(super) struct ContextPicker {
|
||||
@@ -35,15 +31,13 @@ pub(super) struct ContextPicker {
|
||||
impl ContextPicker {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let delegate = ContextPickerDelegate {
|
||||
context_picker: cx.view().downgrade(),
|
||||
workspace,
|
||||
thread_store,
|
||||
context_strip,
|
||||
workspace: workspace.clone(),
|
||||
message_editor: message_editor.clone(),
|
||||
entries: vec![
|
||||
ContextPickerEntry {
|
||||
name: "directory".into(),
|
||||
@@ -60,11 +54,6 @@ impl ContextPicker {
|
||||
description: "Fetch content from URL".into(),
|
||||
icon: IconName::Globe,
|
||||
},
|
||||
ContextPickerEntry {
|
||||
name: "thread".into(),
|
||||
description: "Insert any thread".into(),
|
||||
icon: IconName::MessageBubbles,
|
||||
},
|
||||
],
|
||||
selected_ix: 0,
|
||||
};
|
||||
@@ -92,7 +81,6 @@ impl FocusableView for ContextPicker {
|
||||
ContextPickerMode::Default => self.picker.focus_handle(cx),
|
||||
ContextPickerMode::File(file_picker) => file_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::File(file_picker) => parent.child(file_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 {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
entries: Vec<ContextPickerEntry>,
|
||||
selected_ix: usize,
|
||||
}
|
||||
@@ -161,7 +147,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
FileContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_strip.clone(),
|
||||
self.message_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
@@ -171,17 +157,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
FetchContextPicker::new(
|
||||
self.context_picker.clone(),
|
||||
self.workspace.clone(),
|
||||
self.context_strip.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(),
|
||||
self.message_editor.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
@@ -199,9 +175,7 @@ impl PickerDelegate for ContextPickerDelegate {
|
||||
self.context_picker
|
||||
.update(cx, |this, cx| match this.mode {
|
||||
ContextPickerMode::Default => cx.emit(DismissEvent),
|
||||
ContextPickerMode::File(_)
|
||||
| ContextPickerMode::Fetch(_)
|
||||
| ContextPickerMode::Thread(_) => {}
|
||||
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, Wea
|
||||
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
|
||||
use http_client::{AsyncBody, HttpClientWithUrl};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{prelude::*, ListItem, ViewContext};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
pub struct FetchContextPicker {
|
||||
picker: View<Picker<FetchContextPickerDelegate>>,
|
||||
@@ -23,10 +23,10 @@ impl FetchContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<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));
|
||||
|
||||
Self { picker }
|
||||
@@ -55,7 +55,7 @@ enum ContentType {
|
||||
pub struct FetchContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
url: String,
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ impl FetchContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
) -> Self {
|
||||
FetchContextPickerDelegate {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_strip,
|
||||
message_editor,
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
@@ -150,15 +150,7 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
if self.url.is_empty() {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
|
||||
"Enter the URL that you would like to fetch".into()
|
||||
1
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
@@ -189,9 +181,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate
|
||||
.context_strip
|
||||
.update(cx, |context_strip, _cx| {
|
||||
context_strip.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
.message_editor
|
||||
.update(cx, |message_editor, _cx| {
|
||||
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
|
||||
})
|
||||
})??;
|
||||
|
||||
@@ -218,8 +210,9 @@ impl PickerDelegate for FetchContextPickerDelegate {
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(Label::new(self.url.clone())),
|
||||
.child(self.url.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ use fuzzy::PathMatch;
|
||||
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{PathMatchCandidateSet, WorktreeId};
|
||||
use ui::{prelude::*, ListItem};
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::context::ContextKind;
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::context_strip::ContextStrip;
|
||||
use crate::message_editor::MessageEditor;
|
||||
|
||||
pub struct FileContextPicker {
|
||||
picker: View<Picker<FileContextPickerDelegate>>,
|
||||
@@ -24,10 +24,10 @@ impl FileContextPicker {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
cx: &mut ViewContext<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));
|
||||
|
||||
Self { picker }
|
||||
@@ -49,7 +49,7 @@ impl Render for FileContextPicker {
|
||||
pub struct FileContextPickerDelegate {
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
matches: Vec<PathMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
@@ -58,12 +58,12 @@ impl FileContextPickerDelegate {
|
||||
pub fn new(
|
||||
context_picker: WeakView<ContextPicker>,
|
||||
workspace: WeakView<Workspace>,
|
||||
context_strip: WeakView<ContextStrip>,
|
||||
message_editor: WeakView<MessageEditor>,
|
||||
) -> Self {
|
||||
Self {
|
||||
context_picker,
|
||||
workspace,
|
||||
context_strip,
|
||||
message_editor,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
@@ -214,22 +214,24 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
let buffer = open_buffer_task.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate.context_strip.update(cx, |context_strip, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
text.push_str(&buffer.read(cx).text());
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
this.delegate
|
||||
.message_editor
|
||||
.update(cx, |message_editor, cx| {
|
||||
let mut text = String::new();
|
||||
text.push_str(&codeblock_fence_for_path(Some(&path), None));
|
||||
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(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
message_editor.insert_context(
|
||||
ContextKind::File,
|
||||
path.to_string_lossy().to_string(),
|
||||
text,
|
||||
);
|
||||
})
|
||||
})??;
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -252,29 +254,14 @@ impl PickerDelegate for FileContextPickerDelegate {
|
||||
selected: bool,
|
||||
_cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let path_match = &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()));
|
||||
let mat = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).toggle_state(selected).child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new(file_name))
|
||||
.children(directory.map(|directory| {
|
||||
Label::new(directory)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
})),
|
||||
),
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(mat.path.to_string_lossy().to_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ use std::{
|
||||
iter, mem,
|
||||
ops::{Range, RangeInclusive},
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
task::{self, Poll},
|
||||
time::Instant,
|
||||
@@ -179,7 +178,7 @@ impl InlineAssistant {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.push_code_action_provider(
|
||||
Rc::new(AssistantCodeActionProvider {
|
||||
Arc::new(AssistantCodeActionProvider {
|
||||
editor: cx.view().downgrade(),
|
||||
workspace: workspace.downgrade(),
|
||||
}),
|
||||
@@ -212,12 +211,12 @@ impl InlineAssistant {
|
||||
let handle_assist = |cx: &mut ViewContext<Workspace>| match inline_assist_target {
|
||||
InlineAssistTarget::Editor(active_editor) => {
|
||||
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) => {
|
||||
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(
|
||||
&mut self,
|
||||
editor: &View<Editor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let (snapshot, initial_selections) = editor.update(cx, |editor, cx| {
|
||||
@@ -429,7 +428,7 @@ impl InlineAssistant {
|
||||
initial_prompt: String,
|
||||
initial_transaction_id: Option<TransactionId>,
|
||||
focus: bool,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> InlineAssistId {
|
||||
let assist_group_id = self.next_assist_group_id.post_inc();
|
||||
@@ -2166,7 +2165,7 @@ pub struct InlineAssist {
|
||||
decorations: Option<InlineAssistDecorations>,
|
||||
codegen: Model<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
impl InlineAssist {
|
||||
@@ -2180,7 +2179,7 @@ impl InlineAssist {
|
||||
end_block_id: CustomBlockId,
|
||||
range: Range<Anchor>,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
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 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);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
struct InlineAssistantError;
|
||||
@@ -3383,7 +3386,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
|
||||
"Fix Diagnostics".into(),
|
||||
None,
|
||||
true,
|
||||
workspace,
|
||||
Some(workspace),
|
||||
cx,
|
||||
);
|
||||
assistant.start_assist(assist_id, cx);
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
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_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
|
||||
use settings::Settings;
|
||||
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 crate::context_strip::ContextStrip;
|
||||
use crate::context::{Context, ContextId, ContextKind};
|
||||
use crate::context_picker::ContextPicker;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::ContextPill;
|
||||
use crate::{Chat, ToggleModelSelector};
|
||||
|
||||
pub struct MessageEditor {
|
||||
thread: Model<Thread>,
|
||||
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>,
|
||||
use_tools: bool,
|
||||
}
|
||||
@@ -23,10 +32,10 @@ pub struct MessageEditor {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
workspace: WeakView<Workspace>,
|
||||
thread_store: WeakModel<ThreadStore>,
|
||||
thread: Model<Thread>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let weak_self = cx.view().downgrade();
|
||||
Self {
|
||||
thread,
|
||||
editor: cx.new_view(|cx| {
|
||||
@@ -35,8 +44,10 @@ impl MessageEditor {
|
||||
|
||||
editor
|
||||
}),
|
||||
context_strip: cx
|
||||
.new_view(|cx| ContextStrip::new(workspace.clone(), thread_store.clone(), cx)),
|
||||
context: Vec::new(),
|
||||
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| {
|
||||
LanguageModelSelector::new(
|
||||
|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>) {
|
||||
self.send_to_model(RequestKind::Chat, cx);
|
||||
}
|
||||
@@ -75,7 +100,7 @@ impl MessageEditor {
|
||||
editor.clear(cx);
|
||||
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| {
|
||||
thread.insert_user_message(user_message, context, cx);
|
||||
@@ -161,6 +186,7 @@ impl Render for MessageEditor {
|
||||
let font_size = TextSize::Default.rems(cx);
|
||||
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
|
||||
let focus_handle = self.editor.focus_handle(cx);
|
||||
let context_picker = self.context_picker.clone();
|
||||
|
||||
v_flex()
|
||||
.key_context("MessageEditor")
|
||||
@@ -169,7 +195,48 @@ impl Render for MessageEditor {
|
||||
.gap_2()
|
||||
.p_2()
|
||||
.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({
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
|
||||
@@ -82,7 +82,7 @@ impl TerminalInlineAssistant {
|
||||
pub fn assist(
|
||||
&mut self,
|
||||
terminal_view: &View<TerminalView>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let terminal = terminal_view.read(cx).terminal().clone();
|
||||
@@ -361,7 +361,7 @@ struct TerminalInlineAssist {
|
||||
terminal: WeakView<TerminalView>,
|
||||
prompt_editor: Option<View<PromptEditor>>,
|
||||
codegen: Model<Codegen>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ impl TerminalInlineAssist {
|
||||
assist_id: TerminalInlineAssistId,
|
||||
terminal: &View<TerminalView>,
|
||||
prompt_editor: View<PromptEditor>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let codegen = prompt_editor.read(cx).codegen.clone();
|
||||
@@ -396,7 +396,11 @@ impl TerminalInlineAssist {
|
||||
|
||||
if let CodegenStatus::Error(error) = &codegen.read(cx).status {
|
||||
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 =
|
||||
format!("Terminal inline assistant error: {}", error);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
|
||||
@@ -194,7 +194,6 @@ impl Thread {
|
||||
if let Some(context) = self.context_for_message(message.id) {
|
||||
let mut file_context = String::new();
|
||||
let mut fetch_context = String::new();
|
||||
let mut thread_context = String::new();
|
||||
|
||||
for context in context.iter() {
|
||||
match context.kind {
|
||||
@@ -208,12 +207,6 @@ impl Thread {
|
||||
fetch_context.push_str(&context.text);
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
.content
|
||||
.push(MessageContent::Text(context_text))
|
||||
|
||||
@@ -2,7 +2,7 @@ use gpui::{
|
||||
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
|
||||
};
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing, Tooltip};
|
||||
use ui::{prelude::*, IconButtonShape, ListItem};
|
||||
|
||||
use crate::thread::Thread;
|
||||
use crate::thread_store::ThreadStore;
|
||||
@@ -117,25 +117,17 @@ impl RenderOnce for PastThread {
|
||||
.unwrap_or(UtcOffset::UTC),
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
);
|
||||
|
||||
ListItem::new(("past-thread", self.thread.entity_id()))
|
||||
.outlined()
|
||||
.start_slot(Icon::new(IconName::MessageBubbles))
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.child(Label::new(summary).size(LabelSize::Small))
|
||||
.child(Label::new(summary))
|
||||
.end_slot(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Disabled)
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(Label::new(thread_timestamp).color(Color::Disabled))
|
||||
.child(
|
||||
IconButton::new("delete", IconName::TrashAlt)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(|cx| Tooltip::text("Delete Thread", cx))
|
||||
.on_click({
|
||||
let assistant_panel = self.assistant_panel.clone();
|
||||
let id = id.clone();
|
||||
|
||||
@@ -44,6 +44,7 @@ gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
notifications.workspace = true
|
||||
parking_lot.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
||||
@@ -12,15 +12,10 @@ use language::{
|
||||
language_settings::SoftWrap, Anchor, Buffer, BufferSnapshot, CodeLabel, LanguageRegistry,
|
||||
LanguageServerId, ToOffset,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{search::SearchQuery, Completion};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
rc::Rc,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{ops::Range, sync::Arc, sync::LazyLock, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, TextSize};
|
||||
|
||||
@@ -73,7 +68,7 @@ impl CompletionProvider for MessageEditorCompletionProvider {
|
||||
&self,
|
||||
_buffer: Model<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
_cx: &mut ViewContext<Editor>,
|
||||
) -> Task<anyhow::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
@@ -386,7 +381,11 @@ impl MessageEditor {
|
||||
|
||||
let candidates = names
|
||||
.into_iter()
|
||||
.map(|user| StringMatchCandidate::new(0, &user))
|
||||
.map(|user| StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.clone(),
|
||||
char_bag: user.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some((start_anchor, query, candidates))
|
||||
@@ -402,7 +401,11 @@ impl MessageEditor {
|
||||
LazyLock::new(|| {
|
||||
let emojis = emojis::iter()
|
||||
.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<_>>();
|
||||
emojis
|
||||
});
|
||||
|
||||
@@ -393,8 +393,11 @@ impl CollabPanel {
|
||||
// Populate the active user.
|
||||
if let Some(user) = user_store.current_user() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.push(StringMatchCandidate::new(0, &user.github_login));
|
||||
self.match_candidates.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: user.github_login.clone(),
|
||||
char_bag: user.github_login.chars().collect(),
|
||||
});
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -433,10 +436,11 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.remote_participants().values().map(|participant| {
|
||||
StringMatchCandidate::new(
|
||||
participant.user.id as usize,
|
||||
&participant.user.github_login,
|
||||
)
|
||||
StringMatchCandidate {
|
||||
id: participant.user.id as usize,
|
||||
string: participant.user.github_login.clone(),
|
||||
char_bag: participant.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
let mut matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
@@ -485,8 +489,10 @@ impl CollabPanel {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(room.pending_participants().iter().enumerate().map(
|
||||
|(id, participant)| {
|
||||
StringMatchCandidate::new(id, &participant.github_login)
|
||||
|(id, participant)| StringMatchCandidate {
|
||||
id,
|
||||
string: participant.github_login.clone(),
|
||||
char_bag: participant.github_login.chars().collect(),
|
||||
},
|
||||
));
|
||||
let matches = executor.block(match_strings(
|
||||
@@ -513,12 +519,17 @@ impl CollabPanel {
|
||||
|
||||
if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
channel_store
|
||||
.ordered_channels()
|
||||
.enumerate()
|
||||
.map(|(ix, (_, channel))| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}),
|
||||
);
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -589,12 +600,14 @@ impl CollabPanel {
|
||||
let channel_invites = channel_store.channel_invitations();
|
||||
if !channel_invites.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
channel_invites
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, channel)| StringMatchCandidate::new(ix, &channel.name)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
|
||||
StringMatchCandidate {
|
||||
id: ix,
|
||||
string: channel.name.clone().into(),
|
||||
char_bag: channel.name.chars().collect(),
|
||||
}
|
||||
}));
|
||||
let matches = executor.block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -624,12 +637,17 @@ impl CollabPanel {
|
||||
let incoming = user_store.incoming_contact_requests();
|
||||
if !incoming.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
incoming
|
||||
.iter()
|
||||
.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(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -648,12 +666,17 @@ impl CollabPanel {
|
||||
let outgoing = user_store.outgoing_contact_requests();
|
||||
if !outgoing.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, user)| StringMatchCandidate::new(ix, &user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
outgoing
|
||||
.iter()
|
||||
.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(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
@@ -680,12 +703,17 @@ impl CollabPanel {
|
||||
let contacts = user_store.contacts();
|
||||
if !contacts.is_empty() {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, contact)| StringMatchCandidate::new(ix, &contact.user.github_login)),
|
||||
);
|
||||
self.match_candidates
|
||||
.extend(
|
||||
contacts
|
||||
.iter()
|
||||
.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(
|
||||
&self.match_candidates,
|
||||
|
||||
@@ -272,7 +272,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.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(
|
||||
|
||||
@@ -283,7 +283,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let candidates = commands
|
||||
.iter()
|
||||
.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<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
|
||||
@@ -373,17 +373,6 @@ mod tests {
|
||||
|
||||
// Ensure existing inline completion is interpolated when inserting again.
|
||||
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();
|
||||
cx.update_editor(|editor, cx| {
|
||||
assert!(!editor.context_menu_visible());
|
||||
|
||||
@@ -1050,7 +1050,6 @@ fn editor_blocks(
|
||||
.ok()?
|
||||
}
|
||||
|
||||
Block::FoldedBuffer { .. } => FILE_HEADER.into(),
|
||||
Block::ExcerptBoundary {
|
||||
starts_new_buffer, ..
|
||||
} => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::{cell::Cell, cmp::Reverse, ops::Range, rc::Rc};
|
||||
use std::{cell::Cell, cmp::Reverse, ops::Range, sync::Arc};
|
||||
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@@ -12,6 +11,7 @@ use language::{CodeLabel, Documentation};
|
||||
use lsp::LanguageServerId;
|
||||
use multi_buffer::{Anchor, ExcerptId};
|
||||
use ordered_float::OrderedFloat;
|
||||
use parking_lot::RwLock;
|
||||
use project::{CodeAction, Completion, TaskSourceKind};
|
||||
use task::ResolvedTask;
|
||||
use ui::{
|
||||
@@ -137,9 +137,9 @@ pub struct CompletionsMenu {
|
||||
sort_completions: bool,
|
||||
pub initial_position: Anchor,
|
||||
pub buffer: Model<Buffer>,
|
||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
match_candidates: Rc<[StringMatchCandidate]>,
|
||||
pub matches: Rc<[StringMatch]>,
|
||||
pub completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
match_candidates: Arc<[StringMatchCandidate]>,
|
||||
pub matches: Arc<[StringMatch]>,
|
||||
pub selected_item: usize,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
resolve_completions: bool,
|
||||
@@ -160,7 +160,12 @@ impl CompletionsMenu {
|
||||
let match_candidates = completions
|
||||
.iter()
|
||||
.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();
|
||||
|
||||
Self {
|
||||
@@ -169,7 +174,7 @@ impl CompletionsMenu {
|
||||
initial_position,
|
||||
buffer,
|
||||
show_completion_documentation,
|
||||
completions: RefCell::new(completions).into(),
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches: Vec::new().into(),
|
||||
selected_item: 0,
|
||||
@@ -206,7 +211,7 @@ impl CompletionsMenu {
|
||||
let match_candidates = choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, &completion))
|
||||
.map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string()))
|
||||
.collect();
|
||||
let matches = choices
|
||||
.iter()
|
||||
@@ -223,7 +228,7 @@ impl CompletionsMenu {
|
||||
sort_completions,
|
||||
initial_position: selection.start,
|
||||
buffer,
|
||||
completions: RefCell::new(completions).into(),
|
||||
completions: Arc::new(RwLock::new(completions)),
|
||||
match_candidates,
|
||||
matches,
|
||||
selected_item: 0,
|
||||
@@ -329,13 +334,13 @@ impl CompletionsMenu {
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
let completions = self.completions.borrow_mut();
|
||||
let show_completion_documentation = self.show_completion_documentation;
|
||||
let widest_completion_ix = self
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, mat)| {
|
||||
let completions = self.completions.read();
|
||||
let completion = &completions[mat.candidate_id];
|
||||
let documentation = &completion.documentation;
|
||||
|
||||
@@ -350,12 +355,14 @@ impl CompletionsMenu {
|
||||
})
|
||||
.map(|(ix, _)| ix);
|
||||
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.matches.clone();
|
||||
let selected_item = self.selected_item;
|
||||
let style = style.clone();
|
||||
|
||||
let multiline_docs = if show_completion_documentation {
|
||||
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(div().child(SharedString::from(text.clone())))
|
||||
}
|
||||
@@ -399,16 +406,13 @@ impl CompletionsMenu {
|
||||
.occlude()
|
||||
});
|
||||
|
||||
drop(completions);
|
||||
let completions = self.completions.clone();
|
||||
let matches = self.matches.clone();
|
||||
let list = uniform_list(
|
||||
cx.view().clone(),
|
||||
"completions",
|
||||
matches.len(),
|
||||
move |_editor, range, cx| {
|
||||
let start_ix = range.start;
|
||||
let completions_guard = completions.borrow_mut();
|
||||
let completions_guard = completions.read();
|
||||
|
||||
matches[range]
|
||||
.iter()
|
||||
@@ -424,14 +428,8 @@ impl CompletionsMenu {
|
||||
&None
|
||||
};
|
||||
|
||||
let filter_start = completion.label.filter_range.start;
|
||||
let highlights = gpui::combine_highlights(
|
||||
mat.ranges().map(|range| {
|
||||
(
|
||||
filter_start + range.start..filter_start + range.end,
|
||||
FontWeight::BOLD.into(),
|
||||
)
|
||||
}),
|
||||
mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
|
||||
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|
||||
|(range, mut highlight)| {
|
||||
// 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 {
|
||||
matches.sort_unstable_by_key(|mat| {
|
||||
// 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);
|
||||
|
||||
self.matches = matches.into();
|
||||
@@ -612,13 +618,13 @@ impl CompletionsMenu {
|
||||
pub struct AvailableCodeAction {
|
||||
pub excerpt_id: ExcerptId,
|
||||
pub action: CodeAction,
|
||||
pub provider: Rc<dyn CodeActionProvider>,
|
||||
pub provider: Arc<dyn CodeActionProvider>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodeActionContents {
|
||||
pub tasks: Option<Rc<ResolvedTasks>>,
|
||||
pub actions: Option<Rc<[AvailableCodeAction]>>,
|
||||
pub tasks: Option<Arc<ResolvedTasks>>,
|
||||
pub actions: Option<Arc<[AvailableCodeAction]>>,
|
||||
}
|
||||
|
||||
impl CodeActionContents {
|
||||
@@ -703,7 +709,7 @@ pub enum CodeActionsItem {
|
||||
CodeAction {
|
||||
excerpt_id: ExcerptId,
|
||||
action: CodeAction,
|
||||
provider: Rc<dyn CodeActionProvider>,
|
||||
provider: Arc<dyn CodeActionProvider>,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ impl DisplayMap {
|
||||
let start = buffer_snapshot.anchor_before(range.start);
|
||||
let end = buffer_snapshot.anchor_after(range.end);
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Replace(start..=end),
|
||||
placement: BlockPlacement::Replace(start..end),
|
||||
render,
|
||||
height,
|
||||
style,
|
||||
@@ -336,38 +336,6 @@ impl DisplayMap {
|
||||
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(
|
||||
&mut self,
|
||||
creases: impl IntoIterator<Item = Crease<Anchor>>,
|
||||
@@ -744,11 +712,7 @@ impl DisplaySnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_line_boundary(
|
||||
&self,
|
||||
mut point: MultiBufferPoint,
|
||||
) -> (MultiBufferPoint, DisplayPoint) {
|
||||
let original_point = point;
|
||||
pub fn next_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
|
||||
loop {
|
||||
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);
|
||||
@@ -759,7 +723,7 @@ impl DisplaySnapshot {
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Right);
|
||||
*display_point.column_mut() = self.line_len(display_point.row());
|
||||
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);
|
||||
}
|
||||
point = next_point;
|
||||
@@ -1117,6 +1081,10 @@ impl DisplaySnapshot {
|
||||
|| 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 {
|
||||
self.block_snapshot.is_block_line(BlockRow(display_row.0))
|
||||
}
|
||||
@@ -2263,7 +2231,7 @@ pub mod tests {
|
||||
[BlockProperties {
|
||||
placement: BlockPlacement::Replace(
|
||||
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,
|
||||
style: BlockStyle::Fixed,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,6 +127,7 @@ pub use multi_buffer::{
|
||||
use multi_buffer::{
|
||||
ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use project::{
|
||||
lsp_store::{FormatTarget, FormatTrigger, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
@@ -605,7 +606,7 @@ pub struct Editor {
|
||||
scrollbar_marker_state: ScrollbarMarkerState,
|
||||
active_indent_guides_state: ActiveIndentGuidesState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
context_menu: RefCell<Option<CodeContextMenu>>,
|
||||
context_menu: RwLock<Option<CodeContextMenu>>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
hunk_controls_menu_handle: PopoverMenuHandle<ui::ContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
@@ -613,7 +614,7 @@ pub struct Editor {
|
||||
auto_signature_help: Option<bool>,
|
||||
find_all_references_task_sources: Vec<Anchor>,
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
|
||||
available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>,
|
||||
code_actions_task: Option<Task<Result<()>>>,
|
||||
document_highlights_task: Option<Task<()>>,
|
||||
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||
@@ -634,7 +635,7 @@ pub struct Editor {
|
||||
gutter_hovered: bool,
|
||||
hovered_link_state: Option<HoveredLinkState>,
|
||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
||||
code_action_providers: Vec<Rc<dyn CodeActionProvider>>,
|
||||
code_action_providers: Vec<Arc<dyn CodeActionProvider>>,
|
||||
active_inline_completion: Option<InlineCompletionState>,
|
||||
// enable_inline_completions is a switch that Vim can use to disable
|
||||
// inline completions based on its mode.
|
||||
@@ -677,7 +678,6 @@ pub struct Editor {
|
||||
next_scroll_position: NextScrollCursorCenterTopBottom,
|
||||
addons: HashMap<TypeId, Box<dyn Addon>>,
|
||||
registered_buffers: HashMap<BufferId, OpenLspBufferHandle>,
|
||||
toggle_fold_multiple_buffers: Task<()>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
}
|
||||
|
||||
@@ -1191,7 +1191,7 @@ impl Editor {
|
||||
let mut code_action_providers = Vec::new();
|
||||
if let Some(project) = project.clone() {
|
||||
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 {
|
||||
@@ -1237,7 +1237,7 @@ impl Editor {
|
||||
scrollbar_marker_state: ScrollbarMarkerState::default(),
|
||||
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
||||
nav_history: None,
|
||||
context_menu: RefCell::new(None),
|
||||
context_menu: RwLock::new(None),
|
||||
mouse_context_menu: None,
|
||||
hunk_controls_menu_handle: PopoverMenuHandle::default(),
|
||||
completion_tasks: Default::default(),
|
||||
@@ -1325,7 +1325,6 @@ impl Editor {
|
||||
addons: HashMap::default(),
|
||||
registered_buffers: HashMap::default(),
|
||||
_scroll_cursor_center_top_bottom_task: Task::ready(()),
|
||||
toggle_fold_multiple_buffers: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
};
|
||||
this.tasks_update_task = Some(this.refresh_runnables(cx));
|
||||
@@ -1383,7 +1382,7 @@ impl Editor {
|
||||
key_context.add("renaming");
|
||||
}
|
||||
if self.context_menu_visible() {
|
||||
match self.context_menu.borrow().as_ref() {
|
||||
match self.context_menu.read().as_ref() {
|
||||
Some(CodeContextMenu::Completions(_)) => {
|
||||
key_context.add("menu");
|
||||
key_context.add("showing_completions")
|
||||
@@ -1885,9 +1884,10 @@ impl Editor {
|
||||
|
||||
if local {
|
||||
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() {
|
||||
Some(CodeContextMenu::Completions(menu)) => Some(menu),
|
||||
|
||||
_ => {
|
||||
*context_menu = None;
|
||||
None
|
||||
@@ -1911,7 +1911,7 @@ impl Editor {
|
||||
.await;
|
||||
|
||||
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()
|
||||
else {
|
||||
return;
|
||||
@@ -2429,7 +2429,7 @@ impl Editor {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
if self.dismiss_menus_and_popups(false, true, cx) {
|
||||
if self.dismiss_menus_and_popups(true, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2444,7 +2444,6 @@ impl Editor {
|
||||
|
||||
pub fn dismiss_menus_and_popups(
|
||||
&mut self,
|
||||
keep_inline_completion: bool,
|
||||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
@@ -2468,9 +2467,7 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !keep_inline_completion
|
||||
&& self.discard_inline_completion(should_report_inline_completion_event, cx)
|
||||
{
|
||||
if self.discard_inline_completion(should_report_inline_completion_event, cx) {
|
||||
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| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
@@ -2863,7 +2861,8 @@ impl Editor {
|
||||
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);
|
||||
this.refresh_inline_completion(true, false, cx);
|
||||
});
|
||||
@@ -3650,7 +3649,7 @@ impl Editor {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3669,7 +3668,7 @@ impl Editor {
|
||||
|
||||
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(),
|
||||
_ => false,
|
||||
};
|
||||
@@ -3722,14 +3721,16 @@ impl Editor {
|
||||
};
|
||||
|
||||
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() {
|
||||
None => {}
|
||||
|
||||
Some(CodeContextMenu::Completions(prev_menu)) => {
|
||||
if prev_menu.id > id {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_ => return,
|
||||
}
|
||||
|
||||
@@ -3792,7 +3793,7 @@ impl Editor {
|
||||
.matches
|
||||
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
||||
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)?;
|
||||
cx.stop_propagation();
|
||||
|
||||
@@ -3958,7 +3959,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
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 code_actions.deployed_from_indicator == action.deployed_from_indicator {
|
||||
// Toggle if we're selecting the same one
|
||||
@@ -4038,7 +4039,7 @@ impl Editor {
|
||||
};
|
||||
let resolved_tasks =
|
||||
tasks.zip(task_context).map(|(tasks, task_context)| {
|
||||
Rc::new(ResolvedTasks {
|
||||
Arc::new(ResolvedTasks {
|
||||
templates: tasks.resolve(&task_context).collect(),
|
||||
position: snapshot.buffer_snapshot.anchor_before(Point::new(
|
||||
multibuffer_point.row,
|
||||
@@ -4053,7 +4054,7 @@ impl Editor {
|
||||
.as_ref()
|
||||
.map_or(true, |actions| actions.is_empty());
|
||||
if let Ok(task) = editor.update(&mut cx, |editor, cx| {
|
||||
*editor.context_menu.borrow_mut() =
|
||||
*editor.context_menu.write() =
|
||||
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
|
||||
buffer,
|
||||
actions: CodeActionContents {
|
||||
@@ -4238,7 +4239,7 @@ impl Editor {
|
||||
|
||||
pub fn push_code_action_provider(
|
||||
&mut self,
|
||||
provider: Rc<dyn CodeActionProvider>,
|
||||
provider: Arc<dyn CodeActionProvider>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.code_action_providers.push(provider);
|
||||
@@ -5001,7 +5002,7 @@ impl Editor {
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
self.context_menu
|
||||
.borrow()
|
||||
.read()
|
||||
.as_ref()
|
||||
.map_or(false, |menu| menu.visible())
|
||||
}
|
||||
@@ -5013,7 +5014,7 @@ impl Editor {
|
||||
max_height: Pixels,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(ContextMenuOrigin, AnyElement)> {
|
||||
self.context_menu.borrow().as_ref().map(|menu| {
|
||||
self.context_menu.read().as_ref().map(|menu| {
|
||||
menu.render(
|
||||
cursor_position,
|
||||
style,
|
||||
@@ -5027,7 +5028,7 @@ impl Editor {
|
||||
fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<CodeContextMenu> {
|
||||
cx.notify();
|
||||
self.completion_tasks.clear();
|
||||
self.context_menu.borrow_mut().take()
|
||||
self.context_menu.write().take()
|
||||
}
|
||||
|
||||
fn show_snippet_choices(
|
||||
@@ -5044,7 +5045,7 @@ impl Editor {
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
|
||||
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),
|
||||
));
|
||||
}
|
||||
@@ -7098,7 +7099,7 @@ impl Editor {
|
||||
|
||||
if self
|
||||
.context_menu
|
||||
.borrow_mut()
|
||||
.write()
|
||||
.as_mut()
|
||||
.map(|menu| menu.select_first(self.completion_provider.as_deref(), cx))
|
||||
.unwrap_or(false)
|
||||
@@ -7207,7 +7208,7 @@ impl Editor {
|
||||
|
||||
if self
|
||||
.context_menu
|
||||
.borrow_mut()
|
||||
.write()
|
||||
.as_mut()
|
||||
.map(|menu| menu.select_last(self.completion_provider.as_deref(), cx))
|
||||
.unwrap_or(false)
|
||||
@@ -7260,25 +7261,25 @@ impl Editor {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -9501,7 +9502,7 @@ impl Editor {
|
||||
let location_tasks = definitions
|
||||
.into_iter()
|
||||
.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) => {
|
||||
editor.compute_target_location(lsp_location, server_id, cx)
|
||||
}
|
||||
@@ -9543,7 +9544,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<Option<Location>>> {
|
||||
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 {
|
||||
@@ -10310,53 +10311,22 @@ impl Editor {
|
||||
}
|
||||
|
||||
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 range = if selection.is_empty() {
|
||||
let point = selection.head().to_display_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()))
|
||||
.to_point(&display_map);
|
||||
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)
|
||||
}
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let range = if selection.is_empty() {
|
||||
let point = selection.head().to_display_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()))
|
||||
.to_point(&display_map);
|
||||
start..end
|
||||
} else {
|
||||
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
|
||||
let mut toggled_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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
selection.range()
|
||||
};
|
||||
if display_map.folds_in_range(range).next().is_some() {
|
||||
self.unfold_lines(&Default::default(), cx)
|
||||
} else {
|
||||
self.fold(&Default::default(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10385,68 +10355,44 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext<Self>) {
|
||||
if self.is_singleton(cx) {
|
||||
let mut to_fold = Vec::new();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut to_fold = Vec::new();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
|
||||
for selection in selections {
|
||||
let range = selection.range().sorted();
|
||||
let buffer_start_row = range.start.row;
|
||||
for selection in selections {
|
||||
let range = selection.range().sorted();
|
||||
let buffer_start_row = range.start.row;
|
||||
|
||||
if range.start.row != range.end.row {
|
||||
let mut found = false;
|
||||
let mut row = range.start.row;
|
||||
while row <= range.end.row {
|
||||
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row))
|
||||
{
|
||||
found = true;
|
||||
row = crease.range().end.row + 1;
|
||||
to_fold.push(crease);
|
||||
} else {
|
||||
row += 1
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue;
|
||||
if range.start.row != range.end.row {
|
||||
let mut found = false;
|
||||
let mut row = range.start.row;
|
||||
while row <= range.end.row {
|
||||
if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) {
|
||||
found = true;
|
||||
row = crease.range().end.row + 1;
|
||||
to_fold.push(crease);
|
||||
} else {
|
||||
row += 1
|
||||
}
|
||||
}
|
||||
|
||||
for row in (0..=range.start.row).rev() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.fold_creases(to_fold, true, cx);
|
||||
} else {
|
||||
let (display_snapshot, selections) = self.selections.all_adjusted_display(cx);
|
||||
let mut folded_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 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);
|
||||
for row in (0..=range.start.row).rev() {
|
||||
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);
|
||||
}
|
||||
|
||||
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>) {
|
||||
if self.buffer.read(cx).is_singleton() {
|
||||
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);
|
||||
} 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();
|
||||
});
|
||||
if !self.buffer.read(cx).is_singleton() {
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -10581,45 +10519,22 @@ impl Editor {
|
||||
}
|
||||
|
||||
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 buffer = &display_map.buffer_snapshot;
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let ranges = selections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let range = s.display_range(&display_map).sorted();
|
||||
let mut start = range.start.to_point(&display_map);
|
||||
let mut end = range.end.to_point(&display_map);
|
||||
start.column = 0;
|
||||
end.column = buffer.line_len(MultiBufferRow(end.row));
|
||||
start..end
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let ranges = selections
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let range = s.display_range(&display_map).sorted();
|
||||
let mut start = range.start.to_point(&display_map);
|
||||
let mut end = range.end.to_point(&display_map);
|
||||
start.column = 0;
|
||||
end.column = buffer.line_len(MultiBufferRow(end.row));
|
||||
start..end
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.unfold_ranges(&ranges, true, true, cx);
|
||||
}
|
||||
|
||||
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>) {
|
||||
if self.buffer.read(cx).is_singleton() {
|
||||
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);
|
||||
} 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();
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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.
|
||||
pub fn remove_folds_with_type<T: ToOffset + Clone>(
|
||||
&mut self,
|
||||
@@ -12815,7 +12679,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
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(_))
|
||||
})
|
||||
}
|
||||
@@ -13311,7 +13175,7 @@ pub trait CompletionProvider {
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>>;
|
||||
|
||||
@@ -13429,7 +13293,7 @@ fn snippet_completions(
|
||||
snippet
|
||||
.prefix
|
||||
.iter()
|
||||
.map(move |prefix| StringMatchCandidate::new(ix, &prefix))
|
||||
.map(move |prefix| StringMatchCandidate::new(ix, prefix.clone()))
|
||||
})
|
||||
.collect::<Vec<StringMatchCandidate>>();
|
||||
|
||||
@@ -13541,7 +13405,7 @@ impl CompletionProvider for Model<Project> {
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Task<Result<bool>> {
|
||||
self.update(cx, |project, cx| {
|
||||
@@ -13956,10 +13820,6 @@ pub enum EditorEvent {
|
||||
ExcerptsRemoved {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
BufferFoldToggled {
|
||||
ids: Vec<ExcerptId>,
|
||||
folded: bool,
|
||||
},
|
||||
ExcerptsEdited {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
|
||||
@@ -11,7 +11,6 @@ pub struct EditorSettings {
|
||||
pub current_line_highlight: CurrentLineHighlight,
|
||||
pub lsp_highlight_debounce: u64,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub hover_popover_delay: u64,
|
||||
pub toolbar: Toolbar,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub gutter: Gutter,
|
||||
@@ -197,10 +196,7 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
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
|
||||
pub toolbar: Option<ToolbarContent>,
|
||||
/// Scrollbar related settings
|
||||
|
||||
@@ -4064,7 +4064,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
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(
|
||||
[BlockProperties {
|
||||
@@ -8342,9 +8342,9 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
additional edit
|
||||
"});
|
||||
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");
|
||||
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! {"
|
||||
one.second_completion
|
||||
@@ -8406,12 +8406,12 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
cx.set_state("editorˇ");
|
||||
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("l");
|
||||
cx.simulate_keystroke("o");
|
||||
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| {
|
||||
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.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!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["first", "last"]
|
||||
@@ -8481,8 +8480,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
cx.update_editor(|editor, 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!(
|
||||
menu.selected_item == 1,
|
||||
"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| {
|
||||
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!(
|
||||
menu.selected_item == 0,
|
||||
"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.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!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["r", "ret", "Range", "return"]
|
||||
@@ -10652,9 +10648,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
async fn test_completions_resolve_updates_labels(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
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.simulate_keystroke(".");
|
||||
|
||||
let item1 = lsp::CompletionItem {
|
||||
label: "id".to_string(),
|
||||
filter_text: Some("id".to_string()),
|
||||
let completion_item = lsp::CompletionItem {
|
||||
label: "unresolved".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: ".id".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(),
|
||||
new_text: ".unresolved".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
};
|
||||
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
|
||||
let item1 = item1.clone();
|
||||
let item2 = item2.clone();
|
||||
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
|
||||
let item = completion_item.clone();
|
||||
async move { Ok(Some(lsp::CompletionResponse::Array(vec![item]))) }
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
@@ -10708,20 +10688,15 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.update_editor(|editor, _| {
|
||||
let context_menu = editor.context_menu.borrow_mut();
|
||||
let context_menu = editor.context_menu.read();
|
||||
let context_menu = context_menu
|
||||
.as_ref()
|
||||
.expect("Should have the context menu deployed");
|
||||
match context_menu {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
let completions = completions_menu.completions.borrow_mut();
|
||||
assert_eq!(
|
||||
completions
|
||||
.iter()
|
||||
.map(|completion| &completion.label.text)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["id", "other"]
|
||||
)
|
||||
let completions = completions_menu.completions.read();
|
||||
assert_eq!(completions.len(), 1, "Should have one completion");
|
||||
assert_eq!(completions.get(0).unwrap().label.text, "unresolved");
|
||||
}
|
||||
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 {
|
||||
Ok(lsp::CompletionItem {
|
||||
label: "method id()".to_string(),
|
||||
filter_text: Some("id".to_string()),
|
||||
label: "resolved".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()
|
||||
})
|
||||
})
|
||||
.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(),
|
||||
new_text: ".resolved".to_string(),
|
||||
})),
|
||||
..lsp::CompletionItem::default()
|
||||
})
|
||||
@@ -10765,20 +10719,18 @@ async fn test_completions_resolve_updates_labels_if_filter_text_matches(
|
||||
cx.run_until_parked();
|
||||
|
||||
cx.update_editor(|editor, _| {
|
||||
let context_menu = editor.context_menu.borrow_mut();
|
||||
let context_menu = editor.context_menu.read();
|
||||
let context_menu = context_menu
|
||||
.as_ref()
|
||||
.expect("Should have the context menu deployed");
|
||||
match context_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!(
|
||||
completions
|
||||
.iter()
|
||||
.map(|completion| &completion.label.text)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["method id()", "other"],
|
||||
"Should update first completion label, but not second as the filter text did not match."
|
||||
completions.get(0).unwrap().label.text,
|
||||
"resolved",
|
||||
"Should update the completion label after resolving"
|
||||
);
|
||||
}
|
||||
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;
|
||||
cx.run_until_parked();
|
||||
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") {
|
||||
CodeContextMenu::Completions(completions_menu) => {
|
||||
assert_eq!(
|
||||
@@ -11063,8 +11015,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||
cx.simulate_keystroke("-");
|
||||
cx.executor().run_until_parked();
|
||||
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!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["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.executor().run_until_parked();
|
||||
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!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["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.executor().run_until_parked();
|
||||
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!(
|
||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
||||
&["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> {
|
||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||
point..point
|
||||
|
||||
@@ -22,23 +22,23 @@ use crate::{
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
|
||||
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,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use file_icons::FileIcons;
|
||||
use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
|
||||
use gpui::{
|
||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClickEvent,
|
||||
ClipboardItem, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element,
|
||||
ElementInputHandler, Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla,
|
||||
InteractiveElement, IntoElement, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent,
|
||||
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription,
|
||||
TextRun, TextStyleRefinement, View, ViewContext, WeakView, WindowContext,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use gpui::{ClickEvent, Subscription};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
@@ -49,8 +49,8 @@ use language::{
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint,
|
||||
MultiBufferRow, MultiBufferSnapshot, ToOffset,
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
@@ -1686,7 +1686,7 @@ impl EditorElement {
|
||||
deployed_from_indicator,
|
||||
actions,
|
||||
..
|
||||
})) = editor.context_menu.borrow().as_ref()
|
||||
})) = editor.context_menu.read().as_ref()
|
||||
{
|
||||
actions
|
||||
.tasks
|
||||
@@ -1713,15 +1713,6 @@ impl EditorElement {
|
||||
}
|
||||
let multibuffer_point = tasks.offset.0.to_point(&snapshot.buffer_snapshot);
|
||||
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) {
|
||||
// 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 {
|
||||
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);
|
||||
};
|
||||
@@ -2096,7 +2087,6 @@ impl EditorElement {
|
||||
is_row_soft_wrapped: impl Copy + Fn(usize) -> bool,
|
||||
cx: &mut WindowContext,
|
||||
) -> (AnyElement, Size<Pixels>) {
|
||||
let header_padding = px(6.0);
|
||||
let mut element = match block {
|
||||
Block::Custom(block) => {
|
||||
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
|
||||
@@ -2146,58 +2136,21 @@ impl EditorElement {
|
||||
.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 {
|
||||
prev_excerpt,
|
||||
next_excerpt,
|
||||
show_excerpt_controls,
|
||||
height,
|
||||
starts_new_buffer,
|
||||
height,
|
||||
..
|
||||
} => {
|
||||
let icon_offset = gutter_dimensions.width
|
||||
- (gutter_dimensions.left_padding + gutter_dimensions.margin);
|
||||
|
||||
let header_padding = px(6.0);
|
||||
|
||||
let mut result = v_flex().id(block_id).w_full();
|
||||
|
||||
if let Some(prev_excerpt) = prev_excerpt {
|
||||
if *show_excerpt_controls {
|
||||
result = result.child(
|
||||
@@ -2217,15 +2170,115 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
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 {
|
||||
result = result.child(self.render_buffer_header(
|
||||
next_excerpt,
|
||||
header_padding,
|
||||
false,
|
||||
jump_data,
|
||||
cx,
|
||||
));
|
||||
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 = 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 {
|
||||
result = result.child(
|
||||
h_flex()
|
||||
@@ -2375,137 +2428,6 @@ impl EditorElement {
|
||||
(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(
|
||||
&self,
|
||||
excerpt_id: ExcerptId,
|
||||
@@ -2826,7 +2748,7 @@ impl EditorElement {
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
const PADDING_X: Pixels = Pixels(24.);
|
||||
const PADDING_X: Pixels = Pixels(25.);
|
||||
const PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
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 line_count = text.lines().count() + 1;
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
@@ -2947,8 +2868,7 @@ impl EditorElement {
|
||||
.width
|
||||
};
|
||||
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text).with_highlights(&style.text, highlights);
|
||||
let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
|
||||
|
||||
let mut element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
@@ -2956,38 +2876,15 @@ impl EditorElement {
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1()
|
||||
.child(styled_text)
|
||||
.child(text)
|
||||
.into_any();
|
||||
|
||||
let element_bounds = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let is_fully_visible =
|
||||
editor_width >= longest_line_width + PADDING_X + element_bounds.width;
|
||||
|
||||
let origin = if is_fully_visible {
|
||||
text_bounds.origin
|
||||
+ 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);
|
||||
let origin = text_bounds.origin
|
||||
+ point(
|
||||
longest_line_width + PADDING_X - scroll_pixel_position.x,
|
||||
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
|
||||
);
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
|
||||
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(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
@@ -5900,33 +5757,29 @@ impl Element for EditorElement {
|
||||
if !expanded_add_hunks_by_rows
|
||||
.contains_key(&newest_selection_display_row)
|
||||
{
|
||||
if !snapshot
|
||||
.is_line_folded(MultiBufferRow(newest_selection_point.row))
|
||||
{
|
||||
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
|
||||
MultiBufferRow(newest_selection_point.row),
|
||||
);
|
||||
if let Some((buffer, range)) = buffer {
|
||||
let buffer_id = buffer.remote_id();
|
||||
let row = range.start.row;
|
||||
let has_test_indicator = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.tasks
|
||||
.contains_key(&(buffer_id, row));
|
||||
let buffer = snapshot.buffer_snapshot.buffer_line_for_row(
|
||||
MultiBufferRow(newest_selection_point.row),
|
||||
);
|
||||
if let Some((buffer, range)) = buffer {
|
||||
let buffer_id = buffer.remote_id();
|
||||
let row = range.start.row;
|
||||
let has_test_indicator = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.tasks
|
||||
.contains_key(&(buffer_id, row));
|
||||
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self
|
||||
.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
if !has_test_indicator {
|
||||
code_actions_indicator = self
|
||||
.layout_code_actions_indicator(
|
||||
line_height,
|
||||
newest_selection_head,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, window_is_transparent, Scrollbar, ScrollbarState};
|
||||
use util::TryFutureExt;
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(hover_popover_delay))
|
||||
.timer(Duration::from_millis(HOVER_DELAY_MILLIS))
|
||||
.await;
|
||||
this.update(&mut cx, |this, _| {
|
||||
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| {
|
||||
async move {
|
||||
// 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
|
||||
let total_delay = Some(
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(hover_popover_delay)),
|
||||
.timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
|
||||
);
|
||||
|
||||
cx.background_executor()
|
||||
@@ -859,7 +856,6 @@ mod tests {
|
||||
InlayId, PointForPosition,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::AppContext;
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
@@ -869,10 +865,6 @@ mod tests {
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
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 {
|
||||
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
|
||||
let mut rendered_text = String::new();
|
||||
@@ -897,6 +889,7 @@ mod tests {
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
@@ -970,7 +963,7 @@ mod tests {
|
||||
}))
|
||||
});
|
||||
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;
|
||||
|
||||
cx.editor(|editor, cx| {
|
||||
@@ -1049,7 +1042,7 @@ mod tests {
|
||||
hover_at(editor, Some(anchor), cx)
|
||||
});
|
||||
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;
|
||||
|
||||
// verify that the information popover is no longer visible
|
||||
@@ -1103,7 +1096,7 @@ mod tests {
|
||||
}))
|
||||
});
|
||||
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;
|
||||
|
||||
cx.editor(|editor, cx| {
|
||||
@@ -1139,7 +1132,7 @@ mod tests {
|
||||
hover_at(editor, Some(anchor), cx)
|
||||
});
|
||||
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;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
@@ -1401,7 +1394,7 @@ mod tests {
|
||||
}))
|
||||
});
|
||||
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.editor(|Editor { hover_state, .. }, _| {
|
||||
@@ -1689,7 +1682,7 @@ mod tests {
|
||||
);
|
||||
});
|
||||
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.update_editor(|editor, cx| {
|
||||
let hover_state = &editor.hover_state;
|
||||
@@ -1743,7 +1736,7 @@ mod tests {
|
||||
);
|
||||
});
|
||||
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.update_editor(|editor, cx| {
|
||||
let hover_state = &editor.hover_state;
|
||||
|
||||
@@ -172,7 +172,13 @@ pub fn indent_guides_in_range(
|
||||
let start =
|
||||
MultiBufferRow(indent_guide.multibuffer_row_range.start.0.saturating_sub(1));
|
||||
// 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()
|
||||
}
|
||||
|
||||
@@ -113,7 +113,13 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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<_>>();
|
||||
|
||||
|
||||
@@ -328,7 +328,11 @@ impl ExtensionsPage {
|
||||
let match_candidates = dev_extensions
|
||||
.iter()
|
||||
.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<_>>();
|
||||
|
||||
let matches = match_strings(
|
||||
|
||||
@@ -131,7 +131,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, path)| {
|
||||
StringMatchCandidate::new(ix, &path.to_string_lossy())
|
||||
StringMatchCandidate::new(ix, path.to_string_lossy().into())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
||||
@@ -18,12 +18,22 @@ pub struct StringMatchCandidate {
|
||||
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 {
|
||||
pub fn new(id: usize, string: &str) -> Self {
|
||||
pub fn new(id: usize, string: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
string: string.into(),
|
||||
char_bag: string.into(),
|
||||
char_bag: CharBag::from(string.as_str()),
|
||||
string,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,39 +56,29 @@ pub struct StringMatch {
|
||||
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 {
|
||||
pub fn ranges(&self) -> impl '_ + Iterator<Item = Range<usize>> {
|
||||
let mut positions = self.positions.iter().peekable();
|
||||
iter::from_fn(move || {
|
||||
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!(
|
||||
"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
|
||||
);
|
||||
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() {
|
||||
if end == **next_start {
|
||||
let Some(char_len) = self.char_len_at_index(end) else {
|
||||
if end >= self.string.len() {
|
||||
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
|
||||
);
|
||||
return None;
|
||||
};
|
||||
end += char_len;
|
||||
}
|
||||
end += self.char_len_at_index(end);
|
||||
positions.next();
|
||||
} else {
|
||||
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
|
||||
/// or not on a utf-8 boundary then None is returned.
|
||||
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()))
|
||||
fn char_len_at_index(&self, ix: usize) -> usize {
|
||||
self.string[ix..].chars().next().unwrap().len_utf8()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,8 +46,7 @@ pub trait GitRepository: Send + Sync {
|
||||
|
||||
fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
|
||||
|
||||
/// Returns the path to the repository, typically the `.git` folder.
|
||||
fn dot_git_dir(&self) -> PathBuf;
|
||||
fn path(&self) -> PathBuf;
|
||||
}
|
||||
|
||||
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();
|
||||
repo.path().into()
|
||||
}
|
||||
@@ -234,7 +233,7 @@ pub struct FakeGitRepository {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FakeGitRepositoryState {
|
||||
pub dot_git_dir: PathBuf,
|
||||
pub path: PathBuf,
|
||||
pub event_emitter: smol::channel::Sender<PathBuf>,
|
||||
pub index_contents: HashMap<PathBuf, String>,
|
||||
pub blames: HashMap<PathBuf, Blame>,
|
||||
@@ -250,9 +249,9 @@ impl FakeGitRepository {
|
||||
}
|
||||
|
||||
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 {
|
||||
dot_git_dir,
|
||||
path,
|
||||
event_emitter,
|
||||
index_contents: Default::default(),
|
||||
blames: Default::default(),
|
||||
@@ -284,9 +283,9 @@ impl GitRepository for FakeGitRepository {
|
||||
None
|
||||
}
|
||||
|
||||
fn dot_git_dir(&self) -> PathBuf {
|
||||
fn path(&self) -> PathBuf {
|
||||
let state = self.state.lock();
|
||||
state.dot_git_dir.clone()
|
||||
state.path.clone()
|
||||
}
|
||||
|
||||
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
|
||||
.event_emitter
|
||||
.try_send(state.dot_git_dir.clone())
|
||||
.try_send(state.path.clone())
|
||||
.expect("Dropped repo change event");
|
||||
Ok(())
|
||||
}
|
||||
@@ -345,7 +344,7 @@ impl GitRepository for FakeGitRepository {
|
||||
state.branches.insert(name.to_owned());
|
||||
state
|
||||
.event_emitter
|
||||
.try_send(state.dot_git_dir.clone())
|
||||
.try_send(state.path.clone())
|
||||
.expect("Dropped repo change event");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,19 +14,22 @@ path = "src/git_ui.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
git.workspace = true
|
||||
collections.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use anyhow::Context;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use language::Buffer;
|
||||
use std::{
|
||||
cell::OnceCell,
|
||||
collections::HashSet,
|
||||
@@ -8,6 +11,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
use git::repository::GitFileStatus;
|
||||
|
||||
@@ -24,7 +28,7 @@ use ui::{
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
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};
|
||||
|
||||
actions!(git_panel, [ToggleFocus]);
|
||||
@@ -84,6 +88,8 @@ pub struct GitPanel {
|
||||
selected_item: Option<usize>,
|
||||
show_scrollbar: bool,
|
||||
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
|
||||
git_state: Model<GitState>,
|
||||
editor: View<Editor>,
|
||||
|
||||
// The entries that are currently shown in the panel, aka
|
||||
// not hidden by folding or such
|
||||
@@ -104,11 +110,17 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
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 weak_workspace = workspace.weak_handle();
|
||||
let project = workspace.project().clone();
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
|
||||
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();
|
||||
cx.on_focus(&focus_handle, Self::focus_in).detach();
|
||||
cx.on_focus_out(&focus_handle, |this, _, cx| {
|
||||
@@ -131,6 +143,55 @@ impl GitPanel {
|
||||
})
|
||||
.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 mut this = Self {
|
||||
@@ -142,6 +203,8 @@ impl GitPanel {
|
||||
visible_entries: Vec::new(),
|
||||
current_modifiers: cx.modifiers(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
git_state,
|
||||
editor,
|
||||
|
||||
width: Some(px(360.)),
|
||||
scrollbar_state: ScrollbarState::new(scroll_handle.clone()).parent_view(cx.view()),
|
||||
@@ -269,14 +332,24 @@ impl GitPanel {
|
||||
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
|
||||
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
|
||||
println!("Commit staged changes triggered");
|
||||
}
|
||||
|
||||
/// 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
|
||||
println!("Commit all changes triggered");
|
||||
}
|
||||
@@ -426,6 +499,23 @@ impl GitPanel {
|
||||
|
||||
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 {
|
||||
@@ -499,6 +589,13 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
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_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(
|
||||
v_flex()
|
||||
.id("commit-editor-container")
|
||||
.relative()
|
||||
.h_full()
|
||||
.py_2p5()
|
||||
.px_3()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.font_buffer(cx)
|
||||
.text_ui_sm(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child("Add a message")
|
||||
.gap_1()
|
||||
.child(div().flex_grow())
|
||||
.child(h_flex().child(div().gap_1().flex_grow()).child(
|
||||
if self.current_modifiers.alt {
|
||||
commit_all_button
|
||||
} else {
|
||||
commit_staged_button
|
||||
},
|
||||
))
|
||||
.cursor(CursorStyle::OperationNotAllowed)
|
||||
.opacity(0.5),
|
||||
.on_click(cx.listener(move |_, _: &ClickEvent, cx| cx.focus(&editor_focus_handle)))
|
||||
.child(self.editor.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.absolute()
|
||||
.bottom_2p5()
|
||||
.right_3()
|
||||
.child(div().gap_1().flex_grow())
|
||||
.child(if self.current_modifiers.alt {
|
||||
commit_all_button
|
||||
} else {
|
||||
commit_staged_button
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ::settings::Settings;
|
||||
use git::repository::GitFileStatus;
|
||||
use gpui::{actions, AppContext, Hsla};
|
||||
use gpui::{actions, prelude::*, AppContext, Global, Hsla, Model};
|
||||
use settings::GitPanelSettings;
|
||||
use ui::{Color, Icon, IconName, IntoElement};
|
||||
use ui::{Color, Icon, IconName, IntoElement, SharedString};
|
||||
|
||||
pub mod git_panel;
|
||||
mod settings;
|
||||
@@ -14,14 +14,50 @@ actions!(
|
||||
UnstageAll,
|
||||
DiscardAll,
|
||||
CommitStagedChanges,
|
||||
CommitAllChanges
|
||||
CommitAllChanges,
|
||||
ClearMessage
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
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 {
|
||||
h: 142. / 360.,
|
||||
s: 0.68,
|
||||
|
||||
@@ -50,10 +50,7 @@ pub struct ForegroundExecutor {
|
||||
/// the task to continue running, but with no way to return a value.
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct Task<T>(TaskState<T>);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TaskState<T> {
|
||||
pub enum Task<T> {
|
||||
/// A task that is ready to return a value
|
||||
Ready(Option<T>),
|
||||
|
||||
@@ -64,14 +61,14 @@ enum TaskState<T> {
|
||||
impl<T> Task<T> {
|
||||
/// Creates a new task that will resolve with the value
|
||||
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
|
||||
pub fn detach(self) {
|
||||
match self {
|
||||
Task(TaskState::Ready(_)) => {}
|
||||
Task(TaskState::Spawned(task)) => task.detach(),
|
||||
Task::Ready(_) => {}
|
||||
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> {
|
||||
match unsafe { self.get_unchecked_mut() } {
|
||||
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
|
||||
Task(TaskState::Spawned(task)) => task.poll(cx),
|
||||
Task::Ready(val) => Poll::Ready(val.take().unwrap()),
|
||||
Task::Spawned(task) => task.poll(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +163,7 @@ impl BackgroundExecutor {
|
||||
let (runnable, task) =
|
||||
async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
});
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
Task::Spawned(task)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
});
|
||||
runnable.schedule();
|
||||
Task(TaskState::Spawned(task))
|
||||
Task::Spawned(task)
|
||||
}
|
||||
inner::<R>(dispatcher, Box::pin(future))
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ impl IndexedDocsStore {
|
||||
let candidates = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item_path)| StringMatchCandidate::new(ix, &item_path))
|
||||
.map(|(ix, item_path)| StringMatchCandidate::new(ix, item_path.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
|
||||
@@ -1678,10 +1678,6 @@ impl CodeLabel {
|
||||
pub fn text(&self) -> &str {
|
||||
self.text.as_str()
|
||||
}
|
||||
|
||||
pub fn filter_text(&self) -> &str {
|
||||
&self.text[self.filter_range.clone()]
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CodeLabel {
|
||||
|
||||
@@ -73,8 +73,8 @@ impl<T> Outline<T> {
|
||||
.map(|range| &item.text[range.start..range.end])
|
||||
.collect::<String>();
|
||||
|
||||
path_candidates.push(StringMatchCandidate::new(id, &path_text));
|
||||
candidates.push(StringMatchCandidate::new(id, &candidate_text));
|
||||
path_candidates.push(StringMatchCandidate::new(id, path_text.clone()));
|
||||
candidates.push(StringMatchCandidate::new(id, candidate_text));
|
||||
}
|
||||
|
||||
Self {
|
||||
|
||||
@@ -112,7 +112,7 @@ impl LanguageSelectorDelegate {
|
||||
.then_some(name)
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
|
||||
@@ -375,7 +375,7 @@ impl ContextProvider for PythonContextProvider {
|
||||
args: vec![
|
||||
"-m".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![
|
||||
"python-unittest-class".to_owned(),
|
||||
|
||||
@@ -443,7 +443,7 @@ impl LanguageServer {
|
||||
let stderr_captures = stderr_capture.clone();
|
||||
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 (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
|
||||
stdout.or(stderr)
|
||||
|
||||
@@ -19,6 +19,5 @@ actions!(
|
||||
SelectNext,
|
||||
SelectFirst,
|
||||
SelectLast,
|
||||
Restart
|
||||
]
|
||||
);
|
||||
|
||||
@@ -195,7 +195,6 @@ pub struct ExcerptInfo {
|
||||
pub buffer: BufferSnapshot,
|
||||
pub buffer_id: BufferId,
|
||||
pub range: ExcerptRange<text::Anchor>,
|
||||
pub text_summary: TextSummary,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ExcerptInfo {
|
||||
@@ -1547,33 +1546,6 @@ impl MultiBuffer {
|
||||
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> {
|
||||
self.snapshot
|
||||
.borrow()
|
||||
@@ -3587,7 +3559,6 @@ impl MultiBufferSnapshot {
|
||||
buffer: excerpt.buffer.clone(),
|
||||
buffer_id: excerpt.buffer_id,
|
||||
range: excerpt.range.clone(),
|
||||
text_summary: excerpt.text_summary.clone(),
|
||||
});
|
||||
|
||||
if next.is_none() {
|
||||
@@ -3603,7 +3574,6 @@ impl MultiBufferSnapshot {
|
||||
buffer: prev_excerpt.buffer.clone(),
|
||||
buffer_id: prev_excerpt.buffer_id,
|
||||
range: prev_excerpt.range.clone(),
|
||||
text_summary: prev_excerpt.text_summary.clone(),
|
||||
});
|
||||
let row = MultiBufferRow(cursor.start().1.row);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1211,7 +1211,7 @@ impl BufferStore {
|
||||
return Task::ready(Err(anyhow!("buffer has no file")));
|
||||
};
|
||||
|
||||
match file.worktree.read(cx) {
|
||||
match file.worktree.clone().read(cx) {
|
||||
Worktree::Local(worktree) => {
|
||||
let Some(repo) = worktree.local_git_repo(file.path()) else {
|
||||
return Task::ready(Err(anyhow!("no repository for buffer found")));
|
||||
|
||||
@@ -52,7 +52,7 @@ use lsp::{
|
||||
WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder,
|
||||
};
|
||||
use node_runtime::read_package_installed_version;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
|
||||
@@ -65,14 +65,12 @@ use smol::channel::Sender;
|
||||
use snippet::Snippet;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
cmp::Ordering,
|
||||
convert::TryInto,
|
||||
ffi::OsStr,
|
||||
iter, mem,
|
||||
ops::{ControlFlow, Range},
|
||||
path::{self, Path, PathBuf},
|
||||
rc::Rc,
|
||||
str,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -4139,7 +4137,7 @@ impl LspStore {
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let client = self.upstream_client();
|
||||
@@ -4153,8 +4151,8 @@ impl LspStore {
|
||||
if let Some((client, project_id)) = client {
|
||||
for completion_index in completion_indices {
|
||||
let (server_id, completion) = {
|
||||
let completions = completions.borrow_mut();
|
||||
let completion = &completions[completion_index];
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
did_resolve = true;
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
@@ -4177,8 +4175,8 @@ impl LspStore {
|
||||
} else {
|
||||
for completion_index in completion_indices {
|
||||
let (server_id, completion) = {
|
||||
let completions = completions.borrow_mut();
|
||||
let completion = &completions[completion_index];
|
||||
let completions_guard = completions.read();
|
||||
let completion = &completions_guard[completion_index];
|
||||
let server_id = completion.server_id;
|
||||
let completion = completion.lsp_completion.clone();
|
||||
|
||||
@@ -4220,7 +4218,7 @@ impl LspStore {
|
||||
server: Arc<lsp::LanguageServer>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
snapshot: &BufferSnapshot,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -4248,11 +4246,11 @@ impl LspStore {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
} else {
|
||||
let mut completions = completions.borrow_mut();
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(Documentation::Undocumented);
|
||||
}
|
||||
@@ -4267,7 +4265,7 @@ impl LspStore {
|
||||
if let Some((old_range, mut new_text)) = edit {
|
||||
LineEnding::normalize(&mut new_text);
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
|
||||
completion.new_text = new_text;
|
||||
@@ -4276,7 +4274,7 @@ impl LspStore {
|
||||
}
|
||||
if completion_item.insert_text_format == Some(InsertTextFormat::SNIPPET) {
|
||||
// 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];
|
||||
if completion_item.insert_text_format != completion.lsp_completion.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];
|
||||
completion.lsp_completion = completion_item;
|
||||
if completion.label.filter_text() == new_label.filter_text() {
|
||||
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()
|
||||
);
|
||||
}
|
||||
completion.label = new_label;
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -4324,7 +4311,7 @@ impl LspStore {
|
||||
project_id: u64,
|
||||
server_id: LanguageServerId,
|
||||
buffer_id: BufferId,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
completion_index: usize,
|
||||
completion: lsp::CompletionItem,
|
||||
client: AnyProtoClient,
|
||||
@@ -4362,7 +4349,7 @@ impl LspStore {
|
||||
Documentation::MultiLinePlainText(response.documentation)
|
||||
};
|
||||
|
||||
let mut completions = completions.borrow_mut();
|
||||
let mut completions = completions.write();
|
||||
let completion = &mut completions[completion_index];
|
||||
completion.documentation = Some(documentation);
|
||||
completion.lsp_completion = lsp_completion;
|
||||
|
||||
@@ -57,7 +57,7 @@ use lsp::{
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
pub use prettier_store::PrettierStore;
|
||||
use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
|
||||
use remote::{SshConnectionOptions, SshRemoteClient};
|
||||
@@ -73,10 +73,8 @@ use snippet::Snippet;
|
||||
use snippet_provider::SnippetProvider;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
ops::Range,
|
||||
path::{Component, Path, PathBuf},
|
||||
rc::Rc,
|
||||
str,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -2542,7 +2540,7 @@ impl Project {
|
||||
.read(cx)
|
||||
.list_toolchains(worktree_id, language_name, cx)
|
||||
})
|
||||
.ok()?
|
||||
.unwrap_or(Task::Ready(None))
|
||||
.await
|
||||
})
|
||||
} else {
|
||||
@@ -2874,7 +2872,7 @@ impl Project {
|
||||
&self,
|
||||
buffer: Model<Buffer>,
|
||||
completion_indices: Vec<usize>,
|
||||
completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
completions: Arc<RwLock<Box<[Completion]>>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
|
||||
@@ -81,8 +81,7 @@ impl Inventory {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// and global tasks last. No specific order inside source kinds groups.
|
||||
/// returns all task templates with their source kinds, in no specific order.
|
||||
pub fn list_tasks(
|
||||
&self,
|
||||
file: Option<Arc<dyn File>>,
|
||||
@@ -93,15 +92,13 @@ impl Inventory {
|
||||
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
|
||||
name: language.name().0,
|
||||
});
|
||||
let global_tasks = self.global_templates_from_settings();
|
||||
let language_tasks = language
|
||||
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
|
||||
.into_iter()
|
||||
.flat_map(|tasks| tasks.0.into_iter())
|
||||
.flat_map(|task| Some((task_source_kind.clone()?, task)))
|
||||
.chain(global_tasks);
|
||||
.flat_map(|task| Some((task_source_kind.clone()?, task)));
|
||||
|
||||
self.worktree_templates_from_settings(worktree)
|
||||
self.templates_from_settings(worktree)
|
||||
.chain(language_tasks)
|
||||
.collect()
|
||||
}
|
||||
@@ -168,18 +165,14 @@ impl Inventory {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let not_used_score = post_inc(&mut lru_score);
|
||||
let global_tasks = self.global_templates_from_settings();
|
||||
let language_tasks = language
|
||||
.and_then(|language| language.context_provider()?.associated_tasks(file, cx))
|
||||
.into_iter()
|
||||
.flat_map(|tasks| tasks.0.into_iter())
|
||||
.flat_map(|task| Some((task_source_kind.clone()?, task)))
|
||||
.chain(global_tasks);
|
||||
let worktree_tasks = self
|
||||
.worktree_templates_from_settings(worktree)
|
||||
.chain(language_tasks);
|
||||
|
||||
let new_resolved_tasks = worktree_tasks
|
||||
.flat_map(|task| Some((task_source_kind.clone()?, task)));
|
||||
let new_resolved_tasks = self
|
||||
.templates_from_settings(worktree)
|
||||
.chain(language_tasks)
|
||||
.filter_map(|(kind, task)| {
|
||||
let id_base = kind.to_id_base();
|
||||
Some((
|
||||
@@ -242,8 +235,9 @@ impl Inventory {
|
||||
self.last_scheduled_tasks.retain(|(_, task)| &task.id != id);
|
||||
}
|
||||
|
||||
fn global_templates_from_settings(
|
||||
fn templates_from_settings(
|
||||
&self,
|
||||
worktree: Option<WorktreeId>,
|
||||
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
|
||||
self.templates_from_settings
|
||||
.global
|
||||
@@ -258,34 +252,28 @@ impl Inventory {
|
||||
template,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn worktree_templates_from_settings(
|
||||
&self,
|
||||
worktree: Option<WorktreeId>,
|
||||
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
|
||||
worktree.into_iter().flat_map(|worktree| {
|
||||
self.templates_from_settings
|
||||
.worktree
|
||||
.get(&worktree)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|(directory, templates)| {
|
||||
templates.iter().map(move |template| (directory, template))
|
||||
})
|
||||
.map(move |(directory, template)| {
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree,
|
||||
directory_in_worktree: directory.to_path_buf(),
|
||||
id_base: Cow::Owned(format!(
|
||||
"local worktree tasks from directory {directory:?}"
|
||||
)),
|
||||
},
|
||||
template.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.chain(worktree.into_iter().flat_map(|worktree| {
|
||||
self.templates_from_settings
|
||||
.worktree
|
||||
.get(&worktree)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|(directory, templates)| {
|
||||
templates.iter().map(move |template| (directory, template))
|
||||
})
|
||||
.map(move |(directory, template)| {
|
||||
(
|
||||
TaskSourceKind::Worktree {
|
||||
id: worktree,
|
||||
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.
|
||||
@@ -378,7 +366,7 @@ mod test_inventory {
|
||||
|
||||
use crate::Inventory;
|
||||
|
||||
use super::TaskSourceKind;
|
||||
use super::{task_source_kind_preference, TaskSourceKind};
|
||||
|
||||
pub(super) fn task_template_names(
|
||||
inventory: &Model<Inventory>,
|
||||
@@ -420,18 +408,15 @@ mod test_inventory {
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<(TaskSourceKind, String)> {
|
||||
inventory.update(cx, |inventory, cx| {
|
||||
let task_context = &TaskContext::default();
|
||||
inventory
|
||||
.list_tasks(None, None, worktree, cx)
|
||||
.into_iter()
|
||||
.filter_map(|(source_kind, task)| {
|
||||
let id_base = source_kind.to_id_base();
|
||||
Some((source_kind, task.resolve_task(&id_base, task_context)?))
|
||||
})
|
||||
.map(|(source_kind, resolved_task)| (source_kind, resolved_task.resolved_label))
|
||||
.collect()
|
||||
})
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -804,30 +789,6 @@ mod tests {
|
||||
.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!(
|
||||
list_tasks(&inventory, None, cx).await,
|
||||
worktree_independent_tasks,
|
||||
@@ -839,6 +800,7 @@ mod tests {
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -847,6 +809,7 @@ mod tests {
|
||||
.iter()
|
||||
.chain(worktree_independent_tasks.iter())
|
||||
.cloned()
|
||||
.sorted_by_key(|(kind, label)| (task_source_kind_preference(kind), label.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
@@ -858,7 +821,7 @@ mod tests {
|
||||
TaskStore::init(None);
|
||||
}
|
||||
|
||||
async fn resolved_task_names(
|
||||
pub(super) async fn resolved_task_names(
|
||||
inventory: &Model<Inventory>,
|
||||
worktree: Option<WorktreeId>,
|
||||
cx: &mut TestAppContext,
|
||||
@@ -886,20 +849,4 @@ mod tests {
|
||||
))
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_abs_path = worktree_id
|
||||
.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 {
|
||||
let worktree_abs_path = worktree_abs_path.clone();
|
||||
|
||||
@@ -50,7 +50,13 @@ impl Project {
|
||||
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
|
||||
.into_iter()
|
||||
.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
|
||||
}
|
||||
|
||||
@@ -94,6 +100,7 @@ impl Project {
|
||||
}
|
||||
}
|
||||
};
|
||||
let ssh_details = self.ssh_details(cx);
|
||||
|
||||
let mut settings_location = None;
|
||||
if let Some(path) = path.as_ref() {
|
||||
@@ -106,57 +113,10 @@ impl Project {
|
||||
}
|
||||
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);
|
||||
|
||||
// Start with the environment that we might have inherited from the Zed CLI.
|
||||
let mut env = this
|
||||
let mut env = self
|
||||
.environment
|
||||
.read(cx)
|
||||
.get_cli_environment()
|
||||
@@ -171,141 +131,165 @@ impl Project {
|
||||
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 {
|
||||
TerminalKind::Shell(_) => {
|
||||
if let Some(python_venv_directory) = &python_venv_directory {
|
||||
python_venv_activate_command =
|
||||
this.python_activate_command(python_venv_directory, &settings.detect_venv);
|
||||
}
|
||||
|
||||
match &ssh_details {
|
||||
Some((host, ssh_command)) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// 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()),
|
||||
},
|
||||
)
|
||||
let (spawn_task, shell) = match kind {
|
||||
TerminalKind::Shell(_) => {
|
||||
if let Some(python_venv_directory) = python_venv_directory {
|
||||
python_venv_activate_command = this
|
||||
.update(&mut cx, |this, _| {
|
||||
this.python_activate_command(
|
||||
&python_venv_directory,
|
||||
&settings.detect_venv,
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
}
|
||||
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 {
|
||||
env.insert(
|
||||
"VIRTUAL_ENV".to_string(),
|
||||
venv_path.to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
// Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
|
||||
// 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());
|
||||
|
||||
match &ssh_details {
|
||||
Some((host, ssh_command)) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
let (program, args) = wrap_for_ssh(
|
||||
&ssh_command,
|
||||
Some((&spawn_task.command, &spawn_task.args)),
|
||||
path.as_deref(),
|
||||
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();
|
||||
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()),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
(
|
||||
task_state,
|
||||
Shell::WithArguments {
|
||||
program: spawn_task.command,
|
||||
args: spawn_task.args,
|
||||
title_override: None,
|
||||
},
|
||||
)
|
||||
None => (None, settings.shell.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
TerminalBuilder::new(
|
||||
local_path.map(|path| path.to_path_buf()),
|
||||
python_venv_directory,
|
||||
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));
|
||||
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,
|
||||
});
|
||||
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
env.extend(spawn_task.env);
|
||||
|
||||
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(venv_path) = &python_venv_directory {
|
||||
env.insert(
|
||||
"VIRTUAL_ENV".to_string(),
|
||||
venv_path.to_string_lossy().to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(index) = handles
|
||||
.iter()
|
||||
.position(|terminal| terminal.entity_id() == id)
|
||||
{
|
||||
handles.remove(index);
|
||||
cx.notify();
|
||||
match &ssh_details {
|
||||
Some((host, ssh_command)) => {
|
||||
log::debug!("Connecting to a remote server: {ssh_command:?}");
|
||||
env.entry("TERM".to_string())
|
||||
.or_insert_with(|| "xterm-256color".to_string());
|
||||
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.activate_python_virtual_environment(activate_command, &terminal_handle, cx);
|
||||
}
|
||||
terminal_handle
|
||||
this.terminals
|
||||
.local_handles
|
||||
.push(terminal_handle.downgrade());
|
||||
|
||||
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,
|
||||
command: String,
|
||||
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>> {
|
||||
@@ -455,7 +439,7 @@ pub fn wrap_for_ssh(
|
||||
command: Option<(&String, &Vec<String>)>,
|
||||
path: Option<&Path>,
|
||||
env: HashMap<String, String>,
|
||||
venv_directory: Option<&Path>,
|
||||
venv_directory: Option<PathBuf>,
|
||||
) -> (String, Vec<String>) {
|
||||
let to_run = if let Some((command, args)) = command {
|
||||
let command = Cow::Borrowed(command.as_str());
|
||||
|
||||
@@ -3224,9 +3224,7 @@ impl ProjectPanel {
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.when(!is_marked && !is_active, |div| {
|
||||
div.hover(|style| style.bg(bg_hover_color))
|
||||
})
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.when(is_local, |div| {
|
||||
div.on_drag_move::<ExternalPaths>(cx.listener(
|
||||
move |this, event: &DragMoveEvent<ExternalPaths>, cx| {
|
||||
@@ -3899,11 +3897,6 @@ impl Render for ProjectPanel {
|
||||
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))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
|
||||
@@ -78,7 +78,10 @@ impl ProjectSymbolsDelegate {
|
||||
));
|
||||
let sort_key_for_match = |mat: &StringMatch| {
|
||||
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);
|
||||
@@ -174,7 +177,10 @@ impl PickerDelegate for ProjectSymbolsDelegate {
|
||||
.iter()
|
||||
.enumerate()
|
||||
.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| {
|
||||
project
|
||||
@@ -307,7 +313,7 @@ mod tests {
|
||||
let candidates = fake_symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.name))
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if params.query.is_empty() {
|
||||
Vec::new()
|
||||
|
||||
@@ -228,7 +228,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
.join(""),
|
||||
};
|
||||
|
||||
StringMatchCandidate::new(id, &combined_string)
|
||||
StringMatchCandidate::new(id, combined_string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.matches = smol::block_on(fuzzy::match_strings(
|
||||
|
||||
@@ -98,7 +98,7 @@ impl PickerDelegate for KernelPickerDelegate {
|
||||
|
||||
if query.is_empty() {
|
||||
self.filtered_kernels = all_kernels;
|
||||
return Task::ready(());
|
||||
return Task::Ready(Some(()));
|
||||
}
|
||||
|
||||
self.filtered_kernels = if query.is_empty() {
|
||||
@@ -110,7 +110,7 @@ impl PickerDelegate for KernelPickerDelegate {
|
||||
.collect()
|
||||
};
|
||||
|
||||
return Task::ready(());
|
||||
return Task::Ready(Some(()));
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
|
||||
@@ -96,7 +96,7 @@ impl ScopeSelectorDelegate {
|
||||
let candidates = candidates
|
||||
.chain(languages)
|
||||
.enumerate()
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, &name))
|
||||
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
|
||||
@@ -22,7 +22,11 @@ impl Delegate {
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(id, string)| StringMatchCandidate::new(id, string))
|
||||
.map(|(id, string)| StringMatchCandidate {
|
||||
id,
|
||||
char_bag: string.into(),
|
||||
string: string.into(),
|
||||
})
|
||||
.collect(),
|
||||
matches: vec![],
|
||||
selected_ix: 0,
|
||||
|
||||
@@ -21,7 +21,6 @@ serde_json_lenient.workspace = true
|
||||
sha2.workspace = true
|
||||
shellexpand.workspace = true
|
||||
util.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -15,7 +15,6 @@ use std::str::FromStr;
|
||||
|
||||
pub use task_template::{HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates};
|
||||
pub use vscode_format::VsCodeTaskFile;
|
||||
pub use zed_actions::RevealTarget;
|
||||
|
||||
/// Task identifier, unique within the application.
|
||||
/// Based on it, task reruns and terminal tabs are managed.
|
||||
@@ -48,8 +47,6 @@ pub struct SpawnInTerminal {
|
||||
pub allow_concurrent_runs: bool,
|
||||
/// What to do with the terminal pane and tab, after the command was started.
|
||||
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.
|
||||
pub hide: HideStrategy,
|
||||
/// Which shell to use when spawning the task.
|
||||
|
||||
@@ -9,7 +9,7 @@ use sha2::{Digest, Sha256};
|
||||
use util::{truncate_and_remove_front, ResultExt};
|
||||
|
||||
use crate::{
|
||||
ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
|
||||
ResolvedTask, Shell, SpawnInTerminal, TaskContext, TaskId, VariableName,
|
||||
ZED_VARIABLE_NAME_PREFIX,
|
||||
};
|
||||
|
||||
@@ -42,16 +42,10 @@ pub struct TaskTemplate {
|
||||
#[serde(default)]
|
||||
pub allow_concurrent_runs: bool,
|
||||
/// 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)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
/// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
/// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
#[serde(default)]
|
||||
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:
|
||||
/// * `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
|
||||
@@ -76,12 +70,12 @@ pub struct TaskTemplate {
|
||||
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
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]
|
||||
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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -241,7 +235,6 @@ impl TaskTemplate {
|
||||
use_new_terminal: self.use_new_terminal,
|
||||
allow_concurrent_runs: self.allow_concurrent_runs,
|
||||
reveal: self.reveal,
|
||||
reveal_target: self.reveal_target,
|
||||
hide: self.hide,
|
||||
shell: self.shell.clone(),
|
||||
show_summary: self.show_summary,
|
||||
@@ -637,7 +630,7 @@ mod tests {
|
||||
label: "My task".into(),
|
||||
command: "echo".into(),
|
||||
args: vec!["$PATH".into()],
|
||||
..TaskTemplate::default()
|
||||
..Default::default()
|
||||
};
|
||||
let resolved_task = task
|
||||
.resolve_task(TEST_ID_BASE, &TaskContext::default())
|
||||
@@ -655,7 +648,7 @@ mod tests {
|
||||
label: "My task".into(),
|
||||
command: "echo".into(),
|
||||
args: vec!["$ZED_VARIABLE".into()],
|
||||
..TaskTemplate::default()
|
||||
..Default::default()
|
||||
};
|
||||
assert!(task
|
||||
.resolve_task(TEST_ID_BASE, &TaskContext::default())
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use ::settings::Settings;
|
||||
use editor::{tasks::task_context, Editor};
|
||||
use gpui::{AppContext, Task as AsyncTask, ViewContext, WindowContext};
|
||||
use modal::{TaskOverrides, TasksModal};
|
||||
use modal::TasksModal;
|
||||
use project::{Location, WorktreeId};
|
||||
use task::{RevealTarget, TaskId};
|
||||
use task::TaskId;
|
||||
use workspace::tasks::schedule_task;
|
||||
use workspace::{tasks::schedule_resolved_task, Workspace};
|
||||
|
||||
@@ -79,7 +79,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
}
|
||||
} 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>) {
|
||||
match action {
|
||||
Spawn::ByName {
|
||||
task_name,
|
||||
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(),
|
||||
match &action.task_name {
|
||||
Some(name) => spawn_task_with_name(name.clone(), cx).detach_and_log_err(cx),
|
||||
None => toggle_modal(workspace, cx).detach(),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_modal(
|
||||
workspace: &mut Workspace,
|
||||
reveal_target: Option<RevealTarget>,
|
||||
cx: &mut ViewContext<'_, Workspace>,
|
||||
) -> AsyncTask<()> {
|
||||
fn toggle_modal(workspace: &mut Workspace, cx: &mut ViewContext<'_, Workspace>) -> AsyncTask<()> {
|
||||
let task_store = workspace.project().read(cx).task_store().clone();
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let can_open_modal = workspace.project().update(cx, |project, cx| {
|
||||
@@ -119,15 +107,7 @@ fn toggle_modal(
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx| {
|
||||
TasksModal::new(
|
||||
task_store.clone(),
|
||||
task_context,
|
||||
reveal_target.map(|target| TaskOverrides {
|
||||
reveal_target: Some(target),
|
||||
}),
|
||||
workspace_handle,
|
||||
cx,
|
||||
)
|
||||
TasksModal::new(task_store.clone(), task_context, workspace_handle, cx)
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
@@ -139,7 +119,6 @@ fn toggle_modal(
|
||||
|
||||
fn spawn_task_with_name(
|
||||
name: String,
|
||||
overrides: Option<TaskOverrides>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> AsyncTask<anyhow::Result<()>> {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
@@ -174,13 +153,8 @@ fn spawn_task_with_name(
|
||||
|
||||
let did_spawn = workspace
|
||||
.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)?;
|
||||
if let Some(overrides) = &overrides {
|
||||
if let Some(target_override) = overrides.reveal_target {
|
||||
target_task.reveal_target = target_override;
|
||||
}
|
||||
}
|
||||
schedule_task(
|
||||
workspace,
|
||||
task_source_kind,
|
||||
@@ -195,13 +169,7 @@ fn spawn_task_with_name(
|
||||
if !did_spawn {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
spawn_task_or_modal(
|
||||
workspace,
|
||||
&Spawn::ViaModal {
|
||||
reveal_target: overrides.and_then(|overrides| overrides.reveal_target),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
spawn_task_or_modal(workspace, &Spawn::default(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
};
|
||||
use picker::{highlighted_match_with_paths::HighlightedText, Picker, PickerDelegate};
|
||||
use project::{task_store::TaskStore, TaskSourceKind};
|
||||
use task::{ResolvedTask, RevealTarget, TaskContext, TaskTemplate};
|
||||
use task::{ResolvedTask, TaskContext, TaskTemplate};
|
||||
use ui::{
|
||||
div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
|
||||
FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement,
|
||||
@@ -24,7 +24,6 @@ pub use zed_actions::{Rerun, Spawn};
|
||||
pub(crate) struct TasksModalDelegate {
|
||||
task_store: Model<TaskStore>,
|
||||
candidates: Option<Vec<(TaskSourceKind, ResolvedTask)>>,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
last_used_candidate_index: Option<usize>,
|
||||
divider_index: Option<usize>,
|
||||
matches: Vec<StringMatch>,
|
||||
@@ -35,28 +34,12 @@ pub(crate) struct TasksModalDelegate {
|
||||
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 {
|
||||
fn new(
|
||||
task_store: Model<TaskStore>,
|
||||
task_context: TaskContext,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
workspace: WeakView<Workspace>,
|
||||
) -> 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 {
|
||||
task_store,
|
||||
workspace,
|
||||
@@ -67,8 +50,7 @@ impl TasksModalDelegate {
|
||||
selected_index: 0,
|
||||
prompt: String::default(),
|
||||
task_context,
|
||||
task_overrides,
|
||||
placeholder_text,
|
||||
placeholder_text: Arc::from("Find a task, or run a command"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,17 +61,11 @@ impl TasksModalDelegate {
|
||||
|
||||
let source_kind = TaskSourceKind::UserInput;
|
||||
let id_base = source_kind.to_id_base();
|
||||
let mut new_oneshot = TaskTemplate {
|
||||
let new_oneshot = TaskTemplate {
|
||||
label: self.prompt.clone(),
|
||||
command: self.prompt.clone(),
|
||||
..TaskTemplate::default()
|
||||
};
|
||||
if let Some(TaskOverrides {
|
||||
reveal_target: Some(reveal_target),
|
||||
}) = &self.task_overrides
|
||||
{
|
||||
new_oneshot.reveal_target = *reveal_target;
|
||||
}
|
||||
Some((
|
||||
source_kind,
|
||||
new_oneshot.resolve_task(&id_base, &self.task_context)?,
|
||||
@@ -124,13 +100,12 @@ impl TasksModal {
|
||||
pub(crate) fn new(
|
||||
task_store: Model<TaskStore>,
|
||||
task_context: TaskContext,
|
||||
task_overrides: Option<TaskOverrides>,
|
||||
workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new_view(|cx| {
|
||||
Picker::uniform_list(
|
||||
TasksModalDelegate::new(task_store, task_context, task_overrides, workspace),
|
||||
TasksModalDelegate::new(task_store, task_context, workspace),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -282,17 +257,9 @@ impl PickerDelegate for TasksModalDelegate {
|
||||
.as_ref()
|
||||
.map(|candidates| candidates[ix].clone())
|
||||
});
|
||||
let Some((task_source_kind, mut task)) = task else {
|
||||
let Some((task_source_kind, task)) = task else {
|
||||
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
|
||||
.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>>) {
|
||||
let Some((task_source_kind, mut task)) = self.spawn_oneshot() else {
|
||||
let Some((task_source_kind, task)) = self.spawn_oneshot() else {
|
||||
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
|
||||
.update(cx, |workspace, 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 {
|
||||
id: index,
|
||||
char_bag: candidate.resolved_label.chars().collect(),
|
||||
string: candidate.display_label().into(),
|
||||
string: candidate.display_label().to_owned(),
|
||||
})
|
||||
.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)"
|
||||
);
|
||||
|
||||
cx.dispatch_action(Spawn::ByName {
|
||||
task_name: "example task".to_string(),
|
||||
reveal_target: None,
|
||||
cx.dispatch_action(Spawn {
|
||||
task_name: Some("example task".to_string()),
|
||||
});
|
||||
let tasks_picker = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
@@ -1037,7 +994,7 @@ mod tests {
|
||||
workspace: &View<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> View<Picker<TasksModalDelegate>> {
|
||||
cx.dispatch_action(Spawn::modal());
|
||||
cx.dispatch_action(Spawn::default());
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<TasksModal>(cx)
|
||||
|
||||
@@ -324,7 +324,6 @@ impl TerminalBuilder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
working_directory: Option<PathBuf>,
|
||||
python_venv_directory: Option<PathBuf>,
|
||||
task: Option<TaskState>,
|
||||
shell: Shell,
|
||||
mut env: HashMap<String, String>,
|
||||
@@ -472,7 +471,6 @@ impl TerminalBuilder {
|
||||
word_regex: RegexSearch::new(WORD_REGEX).unwrap(),
|
||||
vi_mode_enabled: false,
|
||||
is_ssh_terminal,
|
||||
python_venv_directory,
|
||||
};
|
||||
|
||||
Ok(TerminalBuilder {
|
||||
@@ -621,7 +619,6 @@ pub struct Terminal {
|
||||
pub breadcrumb_text: String,
|
||||
pub pty_info: PtyProcessInfo,
|
||||
title_override: Option<SharedString>,
|
||||
pub python_venv_directory: Option<PathBuf>,
|
||||
scroll_px: Pixels,
|
||||
next_link_id: usize,
|
||||
selection_phase: SelectionPhase,
|
||||
|
||||
@@ -251,13 +251,7 @@ async fn deserialize_pane_group(
|
||||
let terminal = terminal.await.ok()?;
|
||||
pane.update(cx, |pane, cx| {
|
||||
let terminal_view = Box::new(cx.new_view(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
workspace.clone(),
|
||||
Some(workspace_id),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
TerminalView::new(terminal, workspace.clone(), Some(workspace_id), cx)
|
||||
}));
|
||||
pane.add_item(terminal_view, true, false, None, cx);
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use futures::future::join_all;
|
||||
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,
|
||||
Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
@@ -20,7 +20,7 @@ use itertools::Itertools;
|
||||
use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};
|
||||
use search::{buffer_search::DivRegistrar, BufferSearchBar};
|
||||
use settings::Settings;
|
||||
use task::{RevealStrategy, RevealTarget, Shell, SpawnInTerminal, TaskId};
|
||||
use task::{RevealStrategy, Shell, SpawnInTerminal, TaskId};
|
||||
use terminal::{
|
||||
terminal_settings::{TerminalDockPosition, TerminalSettings},
|
||||
Terminal,
|
||||
@@ -40,7 +40,7 @@ use workspace::{
|
||||
SplitUp, SwapPaneInDirection, ToggleZoom, Workspace,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::Result;
|
||||
use zed_actions::InlineAssist;
|
||||
|
||||
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::open_terminal);
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -72,6 +76,7 @@ pub struct TerminalPanel {
|
||||
pending_serialization: Task<Option<()>>,
|
||||
pending_terminals_to_add: usize,
|
||||
deferred_tasks: HashMap<TaskId, Task<()>>,
|
||||
enabled: bool,
|
||||
assistant_enabled: bool,
|
||||
assistant_tab_bar_button: Option<AnyView>,
|
||||
}
|
||||
@@ -81,6 +86,7 @@ impl TerminalPanel {
|
||||
let project = workspace.project();
|
||||
let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, cx);
|
||||
let center = PaneGroup::new(pane.clone());
|
||||
let enabled = project.read(cx).supports_terminal(cx);
|
||||
cx.focus_view(&pane);
|
||||
let terminal_panel = Self {
|
||||
center,
|
||||
@@ -92,6 +98,7 @@ impl TerminalPanel {
|
||||
height: None,
|
||||
pending_terminals_to_add: 0,
|
||||
deferred_tasks: HashMap::default(),
|
||||
enabled,
|
||||
assistant_enabled: false,
|
||||
assistant_tab_bar_button: None,
|
||||
};
|
||||
@@ -248,10 +255,7 @@ impl TerminalPanel {
|
||||
terminal_panel
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.subscribe(&workspace, |terminal_panel, _, e, cx| {
|
||||
if let workspace::Event::SpawnTask {
|
||||
action: spawn_in_terminal,
|
||||
} = e
|
||||
{
|
||||
if let workspace::Event::SpawnTask(spawn_in_terminal) = e {
|
||||
terminal_panel.spawn_task(spawn_in_terminal, cx);
|
||||
};
|
||||
})
|
||||
@@ -335,13 +339,24 @@ impl TerminalPanel {
|
||||
self.serialize(cx);
|
||||
}
|
||||
pane::Event::Split(direction) => {
|
||||
let Some(new_pane) = self.new_pane_with_cloned_active_terminal(cx) else {
|
||||
return;
|
||||
};
|
||||
let new_pane = self.new_pane_with_cloned_active_terminal(cx);
|
||||
let pane = pane.clone();
|
||||
let direction = *direction;
|
||||
self.center.split(&pane, &new_pane, direction).log_err();
|
||||
cx.focus_view(&new_pane);
|
||||
cx.spawn(move |terminal_panel, mut cx| async move {
|
||||
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 => {
|
||||
self.active_pane = pane.clone();
|
||||
@@ -354,56 +369,63 @@ impl TerminalPanel {
|
||||
fn new_pane_with_cloned_active_terminal(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Pane>> {
|
||||
let workspace = self.workspace.clone().upgrade()?;
|
||||
let workspace = workspace.read(cx);
|
||||
let database_id = workspace.database_id();
|
||||
) -> Task<Option<View<Pane>>> {
|
||||
let Some(workspace) = self.workspace.clone().upgrade() else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
let database_id = workspace.read(cx).database_id();
|
||||
let weak_workspace = self.workspace.clone();
|
||||
let project = workspace.project().clone();
|
||||
let (working_directory, python_venv_directory) = self
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let working_directory = self
|
||||
.active_pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<TerminalView>())
|
||||
.map(|terminal_view| {
|
||||
let terminal = terminal_view.read(cx).terminal().read(cx);
|
||||
(
|
||||
terminal
|
||||
.working_directory()
|
||||
.or_else(|| default_working_directory(workspace, cx)),
|
||||
terminal.python_venv_directory.clone(),
|
||||
)
|
||||
.and_then(|terminal_view| {
|
||||
terminal_view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
})
|
||||
.unwrap_or((None, None));
|
||||
.or_else(|| default_working_directory(workspace.read(cx), cx));
|
||||
let kind = TerminalKind::Shell(working_directory);
|
||||
let window = cx.window_handle();
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_with_venv(kind, python_venv_directory, window, cx)
|
||||
cx.spawn(move |terminal_panel, mut cx| async move {
|
||||
let terminal = project
|
||||
.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()?;
|
||||
|
||||
let terminal_view = Box::new(cx.new_view(|cx| {
|
||||
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)
|
||||
Some(pane)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_terminal(
|
||||
@@ -428,17 +450,83 @@ impl TerminalPanel {
|
||||
|
||||
fn spawn_task(&mut self, spawn_in_terminal: &SpawnInTerminal, cx: &mut ViewContext<Self>) {
|
||||
let mut spawn_task = spawn_in_terminal.clone();
|
||||
let Ok(is_local) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| workspace.project().read(cx).is_local())
|
||||
else {
|
||||
// Set up shell args unconditionally, as tasks are always spawned inside of a shell.
|
||||
let Some((shell, mut user_args)) = (match spawn_in_terminal.shell.clone() {
|
||||
Shell::System => {
|
||||
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;
|
||||
};
|
||||
if let ControlFlow::Break(_) =
|
||||
Self::fill_command(is_local, spawn_in_terminal, &mut spawn_task)
|
||||
#[cfg(target_os = "windows")]
|
||||
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 allow_concurrent_runs = spawn_in_terminal.allow_concurrent_runs;
|
||||
@@ -467,8 +555,8 @@ impl TerminalPanel {
|
||||
!use_new_terminal,
|
||||
"Should have handled 'allow_concurrent_runs && use_new_terminal' case above"
|
||||
);
|
||||
this.update(&mut cx, |terminal_panel, cx| {
|
||||
terminal_panel.replace_terminal(
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.replace_terminal(
|
||||
spawn_task,
|
||||
task_pane,
|
||||
existing_item_index,
|
||||
@@ -514,98 +602,13 @@ impl TerminalPanel {
|
||||
.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(
|
||||
&mut self,
|
||||
spawn_task: SpawnInTerminal,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Model<Terminal>>> {
|
||||
let reveal = spawn_task.reveal;
|
||||
let reveal_target = spawn_task.reveal_target;
|
||||
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),
|
||||
}
|
||||
self.add_terminal(TerminalKind::Task(spawn_task), reveal, cx)
|
||||
}
|
||||
|
||||
/// Create a new Terminal in the current working directory or the user's home directory
|
||||
@@ -632,40 +635,24 @@ impl TerminalPanel {
|
||||
label: &str,
|
||||
cx: &mut AppContext,
|
||||
) -> 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
|
||||
.panes()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.flat_map(pane_terminal_views)
|
||||
.chain(
|
||||
workspace
|
||||
.read(cx)
|
||||
.panes()
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.flat_map(pane_terminal_views),
|
||||
)
|
||||
.sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id())
|
||||
.flat_map(|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(|(index, terminal_view)| (index, pane.clone(), terminal_view))
|
||||
})
|
||||
.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(
|
||||
&mut self,
|
||||
kind: TerminalKind,
|
||||
reveal_strategy: RevealStrategy,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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();
|
||||
self.pending_terminals_to_add += 1;
|
||||
|
||||
cx.spawn(|terminal_panel, mut cx| async move {
|
||||
if workspace.update(&mut cx, |workspace, cx| {
|
||||
!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 pane = terminal_panel.update(&mut cx, |this, _| this.active_pane.clone())?;
|
||||
let project = workspace.update(&mut cx, |workspace, _| workspace.project().clone())?;
|
||||
let window = cx.window_handle();
|
||||
let terminal = project
|
||||
@@ -748,7 +698,6 @@ impl TerminalPanel {
|
||||
terminal.clone(),
|
||||
workspace.weak_handle(),
|
||||
workspace.database_id(),
|
||||
workspace.project().downgrade(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
@@ -825,11 +774,10 @@ impl TerminalPanel {
|
||||
cx: &mut ViewContext<'_, Self>,
|
||||
) -> Task<Option<()>> {
|
||||
let reveal = spawn_task.reveal;
|
||||
let reveal_target = spawn_task.reveal_target;
|
||||
let window = cx.window_handle();
|
||||
let task_workspace = self.workspace.clone();
|
||||
cx.spawn(move |terminal_panel, mut cx| async move {
|
||||
let project = terminal_panel
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let project = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
@@ -851,68 +799,32 @@ impl TerminalPanel {
|
||||
.ok()?;
|
||||
|
||||
match reveal {
|
||||
RevealStrategy::Always => match reveal_target {
|
||||
RevealTarget::Center => {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace
|
||||
.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()?;
|
||||
RevealStrategy::Always => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.activate_terminal_view(&task_pane, terminal_item_index, true, cx)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
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 => {
|
||||
cx.spawn(|mut cx| async move {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.active_pane().focus_handle(cx).focus(cx);
|
||||
})
|
||||
.ok()?;
|
||||
}
|
||||
RevealTarget::Dock => {
|
||||
terminal_panel
|
||||
.update(&mut cx, |terminal_panel, cx| {
|
||||
terminal_panel.activate_terminal_view(
|
||||
&task_pane,
|
||||
terminal_item_index,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
.update(&mut cx, |workspace, cx| workspace.focus_panel::<Self>(cx))
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::NoFocus => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.activate_terminal_view(&task_pane, terminal_item_index, false, cx)
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
cx.spawn(|mut cx| async move {
|
||||
task_workspace
|
||||
.update(&mut cx, |workspace, cx| workspace.open_panel::<Self>(cx))
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
RevealStrategy::Never => {}
|
||||
}
|
||||
|
||||
@@ -927,16 +839,6 @@ impl TerminalPanel {
|
||||
pub fn assistant_enabled(&self) -> bool {
|
||||
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(
|
||||
@@ -997,7 +899,8 @@ pub fn new_terminal_pane(
|
||||
pane.set_custom_drop_handle(cx, move |pane, dropped_item, cx| {
|
||||
if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>() {
|
||||
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)
|
||||
} else {
|
||||
tab.pane.read(cx).item_for_index(tab.ix)
|
||||
@@ -1007,57 +910,53 @@ pub fn new_terminal_pane(
|
||||
let source = tab.pane.clone();
|
||||
let item_id_to_move = item.item_id();
|
||||
|
||||
let new_split_pane = pane
|
||||
.drag_split_direction()
|
||||
.map(|split_direction| {
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel.center.split(
|
||||
&this_pane,
|
||||
&new_pane,
|
||||
split_direction,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
let new_pane = pane.drag_split_direction().and_then(|split_direction| {
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let is_zoomed = if terminal_panel.active_pane == this_pane {
|
||||
pane.is_zoomed()
|
||||
} else {
|
||||
terminal_panel.active_pane.read(cx).is_zoomed()
|
||||
};
|
||||
let new_pane = new_terminal_pane(
|
||||
workspace.clone(),
|
||||
project.clone(),
|
||||
is_zoomed,
|
||||
cx,
|
||||
);
|
||||
terminal_panel.apply_tab_bar_buttons(&new_pane, cx);
|
||||
terminal_panel
|
||||
.center
|
||||
.split(&this_pane, &new_pane, split_direction)
|
||||
.log_err()?;
|
||||
Some(new_pane)
|
||||
})
|
||||
.transpose();
|
||||
});
|
||||
|
||||
match new_split_pane {
|
||||
// Source pane may be the one currently updated, so defer the move.
|
||||
Ok(Some(new_pane)) => cx
|
||||
.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| {
|
||||
move_item(
|
||||
&source,
|
||||
&new_pane,
|
||||
item_id_to_move,
|
||||
new_pane.read(cx).active_item_index(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach(),
|
||||
// If we drop into existing pane or current pane,
|
||||
// regular pane drop handler will take care of it,
|
||||
// using the right tab index for the operation.
|
||||
Ok(None) => return ControlFlow::Continue(()),
|
||||
err @ Err(_) => {
|
||||
err.log_err();
|
||||
return ControlFlow::Break(());
|
||||
}
|
||||
};
|
||||
let destination;
|
||||
let destination_index;
|
||||
if let Some(new_pane) = new_pane {
|
||||
destination_index = new_pane.read(cx).active_item_index();
|
||||
destination = new_pane;
|
||||
} else if belongs_to_this_pane {
|
||||
return ControlFlow::Break(());
|
||||
} else {
|
||||
destination = cx.view().clone();
|
||||
destination_index = pane.active_item_index();
|
||||
}
|
||||
// Destination pane may be the one currently updated, so defer the move.
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
cx.update(|cx| {
|
||||
move_item(
|
||||
&source,
|
||||
&destination,
|
||||
item_id_to_move,
|
||||
destination_index,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
} else if let Some(project_path) = item.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()) {
|
||||
cx.focus_view(&pane);
|
||||
} else {
|
||||
if let Some(new_pane) =
|
||||
terminal_panel.new_pane_with_cloned_active_terminal(cx)
|
||||
{
|
||||
terminal_panel
|
||||
.center
|
||||
.split(
|
||||
&terminal_panel.active_pane,
|
||||
&new_pane,
|
||||
SplitDirection::Right,
|
||||
)
|
||||
.log_err();
|
||||
cx.focus_view(&new_pane);
|
||||
}
|
||||
let new_pane = 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
|
||||
.update(&mut cx, |terminal_panel, cx| {
|
||||
terminal_panel
|
||||
.center
|
||||
.split(
|
||||
&terminal_panel.active_pane,
|
||||
&new_pane,
|
||||
SplitDirection::Right,
|
||||
)
|
||||
.log_err();
|
||||
cx.focus_view(&new_pane);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(
|
||||
@@ -1315,7 +1220,7 @@ impl Panel for TerminalPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
this.add_terminal(kind, RevealStrategy::Always, cx)
|
||||
this.add_terminal(kind, RevealStrategy::Never, cx)
|
||||
.detach_and_log_err(cx)
|
||||
})
|
||||
}
|
||||
@@ -1339,9 +1244,7 @@ impl Panel for TerminalPanel {
|
||||
}
|
||||
|
||||
fn icon(&self, cx: &WindowContext) -> Option<IconName> {
|
||||
if (self.is_enabled(cx) || !self.has_no_terminals(cx))
|
||||
&& TerminalSettings::get_global(cx).button
|
||||
{
|
||||
if (self.enabled || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button {
|
||||
Some(IconName::Terminal)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
|
||||
FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
|
||||
MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View,
|
||||
VisualContext, WeakModel, WeakView,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use language::Bias;
|
||||
use persistence::TERMINAL_DB;
|
||||
@@ -30,6 +30,7 @@ use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
|
||||
use util::{paths::PathWithPosition, ResultExt};
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
|
||||
notifications::NotifyResultExt,
|
||||
register_serializable_item,
|
||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||
CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
|
||||
@@ -77,7 +78,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
|
||||
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);
|
||||
})
|
||||
.detach();
|
||||
@@ -97,7 +98,6 @@ pub struct BlockContext<'a, 'b> {
|
||||
pub struct TerminalView {
|
||||
terminal: Model<Terminal>,
|
||||
workspace: WeakView<Workspace>,
|
||||
project: WeakModel<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
//Currently using iTerm bell, show bell emoji in tab until input is received
|
||||
has_bell: bool,
|
||||
@@ -134,15 +134,44 @@ impl TerminalView {
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
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(
|
||||
terminal: Model<Terminal>,
|
||||
workspace: WeakView<Workspace>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
project: WeakModel<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let workspace_handle = workspace.clone();
|
||||
@@ -162,7 +191,6 @@ impl TerminalView {
|
||||
Self {
|
||||
terminal,
|
||||
workspace: workspace_handle,
|
||||
project,
|
||||
has_bell: false,
|
||||
focus_handle,
|
||||
context_menu: None,
|
||||
@@ -1078,37 +1106,21 @@ impl Item for TerminalView {
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
let window = cx.window_handle();
|
||||
let terminal = self
|
||||
.project
|
||||
.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()?;
|
||||
//From what I can tell, there's no way to tell the current working
|
||||
//Directory of the terminal from outside the shell. There might be
|
||||
//solutions to this, but they are non-trivial and require more IPC
|
||||
|
||||
Some(cx.new_view(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
self.workspace.clone(),
|
||||
workspace_id,
|
||||
self.project.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
// Some(TerminalContainer::new(
|
||||
// Err(anyhow::anyhow!("failed to instantiate terminal")),
|
||||
// workspace_id,
|
||||
// cx,
|
||||
// ))
|
||||
|
||||
// TODO
|
||||
None
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
|
||||
@@ -1237,15 +1249,7 @@ impl SerializableItem for TerminalView {
|
||||
})?
|
||||
.await?;
|
||||
cx.update(|cx| {
|
||||
cx.new_view(|cx| {
|
||||
TerminalView::new(
|
||||
terminal,
|
||||
workspace,
|
||||
Some(workspace_id),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,7 +230,11 @@ impl PickerDelegate for ThemeSelectorDelegate {
|
||||
.themes
|
||||
.iter()
|
||||
.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<_>>();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -296,7 +296,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
|
||||
.map(|(candidate_id, toolchain)| {
|
||||
let path = Self::relativize_path(toolchain.path, &worktree_root_path);
|
||||
let string = format!("{}{}", toolchain.name, path);
|
||||
StringMatchCandidate::new(candidate_id, &string)
|
||||
StringMatchCandidate::new(candidate_id, string)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
match_strings(
|
||||
|
||||
@@ -38,7 +38,6 @@ pub struct ListItem {
|
||||
on_secondary_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
selectable: bool,
|
||||
outlined: bool,
|
||||
overflow_x: bool,
|
||||
focused: Option<bool>,
|
||||
}
|
||||
@@ -63,7 +62,6 @@ impl ListItem {
|
||||
tooltip: None,
|
||||
children: SmallVec::new(),
|
||||
selectable: true,
|
||||
outlined: false,
|
||||
overflow_x: false,
|
||||
focused: None,
|
||||
}
|
||||
@@ -140,11 +138,6 @@ impl ListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn outlined(mut self) -> Self {
|
||||
self.outlined = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn overflow_x(mut self) -> Self {
|
||||
self.overflow_x = true;
|
||||
self
|
||||
@@ -210,7 +203,6 @@ impl RenderOnce for ListItem {
|
||||
.child(
|
||||
h_flex()
|
||||
.id("inner_list_item")
|
||||
.group("list_item")
|
||||
.w_full()
|
||||
.relative()
|
||||
.items_center()
|
||||
@@ -220,6 +212,7 @@ impl RenderOnce for ListItem {
|
||||
ListItemSpacing::Dense => this,
|
||||
ListItemSpacing::Sparse => this.py_1(),
|
||||
})
|
||||
.group("list_item")
|
||||
.when(self.inset && !self.disabled, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
@@ -245,12 +238,6 @@ impl RenderOnce for ListItem {
|
||||
.when_some(self.on_click, |this, 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| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
|
||||
@@ -172,7 +172,11 @@ impl PickerDelegate for BranchListDelegate {
|
||||
branches
|
||||
.into_iter()
|
||||
.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>>()
|
||||
});
|
||||
let Some(candidates) = candidates.log_err() else {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl Vim {
|
||||
if count <= 1 || Vim::globals(cx).dot_replaying {
|
||||
self.create_mark("^".into(), false, 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| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
|
||||
@@ -626,23 +626,10 @@ impl Vim {
|
||||
pub fn cursor_shape(&self) -> CursorShape {
|
||||
match self.mode {
|
||||
Mode::Normal => {
|
||||
if let Some(operator) = self.operator_stack.last() {
|
||||
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
|
||||
if self.operator_stack.is_empty() {
|
||||
CursorShape::Block
|
||||
} else {
|
||||
CursorShape::Underline
|
||||
}
|
||||
}
|
||||
Mode::Replace => CursorShape::Underline,
|
||||
|
||||
@@ -127,7 +127,11 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = BaseKeymap::names()
|
||||
.enumerate()
|
||||
.map(|(id, name)| StringMatchCandidate::new(id, name))
|
||||
.map(|(id, name)| StringMatchCandidate {
|
||||
id,
|
||||
char_bag: name.into(),
|
||||
string: name.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -896,8 +896,6 @@ impl Pane {
|
||||
destination_index: Option<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.close_items_over_max_tabs(cx);
|
||||
|
||||
if item.is_singleton(cx) {
|
||||
if let Some(&entry_id) = item.project_entry_ids(cx).first() {
|
||||
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(
|
||||
items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
|
||||
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.
|
||||
|| 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.project_path(cx).is_none()
|
||||
})
|
||||
@@ -3321,8 +3282,6 @@ impl Render for DraggedTab {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::num::NonZero;
|
||||
|
||||
use super::*;
|
||||
use crate::item::test::{TestItem, TestProjectItem};
|
||||
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]
|
||||
async fn test_add_item_with_new_item(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -4025,8 +3936,11 @@ mod tests {
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
cx.simulate_prompt_answer(2);
|
||||
cx.executor().run_until_parked();
|
||||
cx.simulate_prompt_answer(2);
|
||||
cx.executor().run_until_parked();
|
||||
save.await.unwrap();
|
||||
assert_item_labels(&pane, [], cx);
|
||||
assert_item_labels(&pane, ["A*^", "B^", "C^"], cx);
|
||||
}
|
||||
|
||||
#[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(
|
||||
pane: &View<Pane>,
|
||||
label: &str,
|
||||
|
||||
@@ -6,7 +6,7 @@ use ui::ViewContext;
|
||||
use crate::Workspace;
|
||||
|
||||
pub fn schedule_task(
|
||||
workspace: &mut Workspace,
|
||||
workspace: &Workspace,
|
||||
task_source_kind: TaskSourceKind,
|
||||
task_to_resolve: &TaskTemplate,
|
||||
task_cx: &TaskContext,
|
||||
@@ -40,7 +40,7 @@ pub fn schedule_task(
|
||||
}
|
||||
|
||||
pub fn schedule_resolved_task(
|
||||
workspace: &mut Workspace,
|
||||
workspace: &Workspace,
|
||||
task_source_kind: TaskSourceKind,
|
||||
mut resolved_task: ResolvedTask,
|
||||
omit_history: bool,
|
||||
@@ -59,9 +59,6 @@ pub fn schedule_resolved_task(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(crate::Event::SpawnTask {
|
||||
action: Box::new(spawn_in_terminal),
|
||||
});
|
||||
cx.emit(crate::Event::SpawnTask(Box::new(spawn_in_terminal)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -686,9 +686,7 @@ pub enum Event {
|
||||
},
|
||||
ContactRequestedJoin(u64),
|
||||
WorkspaceCreated(WeakView<Workspace>),
|
||||
SpawnTask {
|
||||
action: Box<SpawnInTerminal>,
|
||||
},
|
||||
SpawnTask(Box<SpawnInTerminal>),
|
||||
OpenBundledFile {
|
||||
text: Cow<'static, str>,
|
||||
title: &'static str,
|
||||
@@ -1651,7 +1649,7 @@ impl Workspace {
|
||||
F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
if self.project.read(cx).is_local() {
|
||||
Task::ready(Ok(callback(self, cx)))
|
||||
Task::Ready(Some(Ok(callback(self, cx))))
|
||||
} else {
|
||||
let env = self.project.read(cx).cli_environment(cx);
|
||||
let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::AppContext;
|
||||
@@ -22,7 +20,6 @@ pub struct WorkspaceSettings {
|
||||
pub use_system_path_prompts: bool,
|
||||
pub command_aliases: HashMap<String, String>,
|
||||
pub show_user_picture: bool,
|
||||
pub max_tabs: Option<NonZeroUsize>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -87,15 +84,15 @@ pub enum RestoreOnStartupBehavior {
|
||||
pub struct WorkspaceSettingsContent {
|
||||
/// Active pane styling settings.
|
||||
pub active_pane_modifiers: Option<ActivePanelModifiers>,
|
||||
/// Direction to split horizontally.
|
||||
///
|
||||
/// Default: "up"
|
||||
// Direction to split horizontally.
|
||||
//
|
||||
// Default: "up"
|
||||
pub pane_split_direction_horizontal: Option<PaneSplitDirectionHorizontal>,
|
||||
/// Direction to split vertically.
|
||||
///
|
||||
/// Default: "left"
|
||||
// Direction to split vertically.
|
||||
//
|
||||
// Default: "left"
|
||||
pub pane_split_direction_vertical: Option<PaneSplitDirectionVertical>,
|
||||
/// Centered layout related settings.
|
||||
// Centered layout related settings.
|
||||
pub centered_layout: Option<CenteredLayoutSettings>,
|
||||
/// Whether or not to prompt the user to confirm before closing the application.
|
||||
///
|
||||
@@ -136,11 +133,6 @@ pub struct WorkspaceSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
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)]
|
||||
|
||||
@@ -2533,12 +2533,6 @@ impl Snapshot {
|
||||
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 {
|
||||
&self.root_name
|
||||
}
|
||||
@@ -3118,7 +3112,7 @@ impl BackgroundScannerState {
|
||||
let t0 = Instant::now();
|
||||
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))?;
|
||||
watcher.add(&actual_repo_path).log_err()?;
|
||||
|
||||
@@ -10,5 +10,4 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use gpui::{actions, impl_actions};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
|
||||
// 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
|
||||
@@ -91,40 +90,19 @@ pub struct OpenRecent {
|
||||
gpui::impl_actions!(projects, [OpenRecent]);
|
||||
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
|
||||
#[derive(Debug, PartialEq, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Spawn {
|
||||
/// Spawns a task by the name given.
|
||||
ByName {
|
||||
task_name: String,
|
||||
#[serde(default)]
|
||||
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>,
|
||||
},
|
||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||
pub struct Spawn {
|
||||
#[serde(default)]
|
||||
/// Name of the task to spawn.
|
||||
/// If it is not set, a modal with a list of available tasks is opened instead.
|
||||
/// Defaults to None.
|
||||
pub task_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Spawn {
|
||||
pub fn modal() -> Self {
|
||||
Self::ViaModal {
|
||||
reveal_target: None,
|
||||
}
|
||||
Self { task_name: None }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`:
|
||||
|
||||
```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:
|
||||
|
||||
@@ -13,18 +13,6 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp
|
||||
|
||||
`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](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.
|
||||
|
||||
@@ -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`.
|
||||
"allow_concurrent_runs": false,
|
||||
// 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)
|
||||
// * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it
|
||||
// * `never` — do not alter focus, but still add/reuse the task's tab in its pane
|
||||
// * `always` — always show the terminal pane, add and focus the corresponding task's tab in it (default)
|
||||
// * `no_focus` — always show the terminal pane, add/reuse the task's tab there, but don't focus it
|
||||
// * `never` — avoid changing current terminal pane focus, but still add/reuse the task's tab there
|
||||
"reveal": "always",
|
||||
// What to do with the terminal pane and tab, after the command had finished:
|
||||
// * `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
|
||||
|
||||
Zed supports overriding default action for inline runnable indicators via workspace-local and global `tasks.json` file with the following precedence hierarchy:
|
||||
|
||||
Reference in New Issue
Block a user