Compare commits
24 Commits
v0.180.1-p
...
html_trees
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8494a33b98 | ||
|
|
2b5095ac91 | ||
|
|
9e02fee98d | ||
|
|
999ad77a59 | ||
|
|
780d0eb427 | ||
|
|
7b40ab30d7 | ||
|
|
0a3c8a6790 | ||
|
|
1463b4d201 | ||
|
|
77856bf017 | ||
|
|
848a99c605 | ||
|
|
435a36b9f9 | ||
|
|
8b3ddcd545 | ||
|
|
13bf179aae | ||
|
|
cdaad2655a | ||
|
|
7e4320f587 | ||
|
|
130abc8998 | ||
|
|
9db4c8b710 | ||
|
|
e67ad1a1b6 | ||
|
|
82536f5243 | ||
|
|
9eacac62a9 | ||
|
|
82b0881dcb | ||
|
|
0a49ccbebf | ||
|
|
d232150d67 | ||
|
|
9a2dfa687d |
86
Cargo.lock
generated
86
Cargo.lock
generated
@@ -491,6 +491,7 @@ dependencies = [
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"serde",
|
||||
@@ -5228,7 +5229,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"libc",
|
||||
"log",
|
||||
"notify 6.1.1",
|
||||
"notify 8.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
|
||||
"objc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -6833,17 +6834,6 @@ dependencies = [
|
||||
"zeta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
@@ -6949,7 +6939,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio 1.0.3",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -8151,7 +8141,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"log",
|
||||
"memchr",
|
||||
"notify 8.0.0",
|
||||
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify-debouncer-mini",
|
||||
"once_cell",
|
||||
"opener",
|
||||
@@ -8300,18 +8290,6 @@ version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
@@ -8589,25 +8567,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify 0.9.6",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
@@ -8617,12 +8576,30 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify 0.11.0",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 1.0.3",
|
||||
"notify-types",
|
||||
"mio",
|
||||
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types 2.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -8634,8 +8611,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify 8.0.0",
|
||||
"notify-types",
|
||||
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@@ -8645,6 +8622,11 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
@@ -14241,7 +14223,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.10.1",
|
||||
"libc",
|
||||
"mio 1.0.3",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@@ -17272,7 +17254,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.180.0"
|
||||
version = "0.181.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -754,8 +754,11 @@
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"shift-delete": "git::RestoreFile",
|
||||
"ctrl-delete": "git::RestoreFile"
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -803,7 +803,10 @@
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-backspace": "git::RestoreFile"
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
|
||||
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ It will be up to you to decide which of these you are doing based on what the us
|
||||
You should only perform actions that modify the user’s system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user’s system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
|
||||
Be concise and direct in your responses.
|
||||
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
|
||||
"hide_mouse_while_typing": true,
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
@@ -427,6 +429,8 @@
|
||||
"project_panel": {
|
||||
// Whether to show the project panel button in the status bar
|
||||
"button": true,
|
||||
// Whether to hide the gitignore entries in the project panel.
|
||||
"hide_gitignore": false,
|
||||
// Default width of the project panel.
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
@@ -620,6 +624,7 @@
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
},
|
||||
"default_profile": "code-writer",
|
||||
"profiles": {
|
||||
"read-only": {
|
||||
"name": "Read-only",
|
||||
|
||||
@@ -62,6 +62,7 @@ prompt_library.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -5,16 +5,16 @@ use crate::thread::{
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, ScrollHandle,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle,
|
||||
WeakEntity, WindowHandle,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, MouseButton,
|
||||
ScrollHandle, Stateful, StyleRefinement, Subscription, Task, TextStyleRefinement,
|
||||
Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
@@ -23,7 +23,7 @@ use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct ActiveThread {
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
scrollbar_state: ScrollbarState,
|
||||
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
|
||||
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
@@ -226,6 +227,14 @@ impl ActiveThread {
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
language_registry,
|
||||
thread_store,
|
||||
@@ -238,13 +247,8 @@ impl ActiveThread {
|
||||
rendered_tool_use_labels: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
list_state: list_state.clone(),
|
||||
scrollbar_state: ScrollbarState::new(list_state),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
pop_ups: Vec::new(),
|
||||
@@ -550,11 +554,22 @@ impl ActiveThread {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
|
||||
let workspace_handle = this.workspace.clone();
|
||||
|
||||
// If there are multiple Zed windows, activate the correct one.
|
||||
cx.defer(move |cx| {
|
||||
handle
|
||||
.update(cx, |_view, window, _cx| {
|
||||
window.activate_window();
|
||||
|
||||
if let Some(workspace) = workspace_handle.upgrade()
|
||||
{
|
||||
workspace.update(_cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(
|
||||
window, cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
@@ -1737,13 +1752,48 @@ impl ActiveThread {
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("active-thread-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
.children(self.render_confirmations(cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ mod history_store;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod profile_selector;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_selector;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
|
||||
@@ -544,6 +544,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
fn sort_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn filter_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_completion_callback(
|
||||
|
||||
@@ -282,7 +282,10 @@ pub fn render_file_context_entry(
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = if path == Path::new("") {
|
||||
(SharedString::from(path_prefix.clone()), None)
|
||||
(
|
||||
SharedString::from(path_prefix.trim_end_matches('/').to_string()),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@@ -291,8 +294,10 @@ pub fn render_file_context_entry(
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let mut directory = format!("{}/", path_prefix);
|
||||
|
||||
let mut directory = path_prefix.to_string();
|
||||
if !directory.ends_with('/') {
|
||||
directory.push('/');
|
||||
}
|
||||
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
|
||||
directory.push_str(&parent.to_string_lossy());
|
||||
directory.push('/');
|
||||
|
||||
@@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_selector::ToolSelector;
|
||||
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -43,7 +43,7 @@ pub struct MessageEditor {
|
||||
inline_context_picker: Entity<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
tool_selector: Entity<ToolSelector>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let tools = thread.read(cx).tools().clone();
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
@@ -129,14 +128,14 @@ impl MessageEditor {
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
|
||||
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -624,7 +623,7 @@ impl Render for MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().child(self.tool_selector.clone()))
|
||||
.child(h_flex().gap_2().child(self.profile_selector.clone()))
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
|
||||
202
crates/assistant2/src/profile_selector.rs
Normal file
202
crates/assistant2/src/profile_selector.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
use anyhow::Result;
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use editor::scroll::Autoscroll;
|
||||
use editor::Editor;
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, AsyncWindowContext, Entity, Subscription, WeakEntity};
|
||||
use indexmap::IndexMap;
|
||||
use regex::Regex;
|
||||
use settings::{update_settings_file, Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{create_and_open_local_file, Workspace};
|
||||
|
||||
use crate::ThreadStore;
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
fs,
|
||||
thread_store,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let icon_position = IconPosition::Start;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(&profile_id, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Configure Profiles")
|
||||
.icon(IconName::Pencil)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
if let Some(workspace) = window.root().flatten() {
|
||||
let workspace = workspace.downgrade();
|
||||
window
|
||||
.spawn(cx, async |cx| {
|
||||
Self::open_profiles_setting_in_editor(workspace, cx).await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_profiles_setting_in_editor(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update_in(cx, |_, window, cx| {
|
||||
create_and_open_local_file(paths::settings_file(), window, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor
|
||||
.downgrade()
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
let text = editor.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
|
||||
let edits =
|
||||
settings.edits_for_update::<AssistantSettings>(
|
||||
&text,
|
||||
|settings| match settings {
|
||||
assistant_settings::AssistantSettingsContent::Versioned(settings) => {
|
||||
match settings {
|
||||
assistant_settings::VersionedAssistantSettingsContent::V2(
|
||||
settings,
|
||||
) => {
|
||||
settings.profiles.get_or_insert_with(IndexMap::default);
|
||||
}
|
||||
assistant_settings::VersionedAssistantSettingsContent::V1(
|
||||
_,
|
||||
) => {}
|
||||
}
|
||||
}
|
||||
assistant_settings::AssistantSettingsContent::Legacy(_) => {}
|
||||
},
|
||||
);
|
||||
|
||||
if !edits.is_empty() {
|
||||
editor.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
|
||||
let text = editor.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
static PROFILES_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r#"(?P<key>"profiles":)\s*\{"#).unwrap());
|
||||
let range = PROFILES_REGEX.captures(&text).and_then(|captures| {
|
||||
captures
|
||||
.name("key")
|
||||
.map(|inner_match| inner_match.start()..inner_match.end())
|
||||
});
|
||||
if let Some(range) = range {
|
||||
editor.change_selections(
|
||||
Some(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges(vec![range]);
|
||||
},
|
||||
);
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = settings
|
||||
.profiles
|
||||
.get(&settings.default_profile)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
Button::new("profile-selector-button", profile)
|
||||
.style(ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small),
|
||||
Tooltip::text("Change Profile"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -873,17 +873,23 @@ impl Thread {
|
||||
request.messages.push(context_message);
|
||||
}
|
||||
|
||||
self.attach_stale_files(&mut request.messages, cx);
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
fn attach_stale_files(&self, messages: &mut Vec<LanguageModelRequestMessage>, cx: &App) {
|
||||
fn attached_tracked_files_state(
|
||||
&self,
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
cx: &App,
|
||||
) {
|
||||
const STALE_FILES_HEADER: &str = "These files changed since last read:";
|
||||
|
||||
let mut stale_message = String::new();
|
||||
|
||||
for stale_file in self.action_log.read(cx).stale_buffers(cx) {
|
||||
let action_log = self.action_log.read(cx);
|
||||
|
||||
for stale_file in action_log.stale_buffers(cx) {
|
||||
let Some(file) = stale_file.read(cx).file() else {
|
||||
continue;
|
||||
};
|
||||
@@ -895,10 +901,22 @@ impl Thread {
|
||||
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
|
||||
}
|
||||
|
||||
let mut content = Vec::with_capacity(2);
|
||||
|
||||
if !stale_message.is_empty() {
|
||||
content.push(stale_message.into());
|
||||
}
|
||||
|
||||
if action_log.has_edited_files_since_project_diagnostics_check() {
|
||||
content.push(
|
||||
"When you're done making changes, make sure to check project diagnostics and fix all errors AND warnings you introduced!".into(),
|
||||
);
|
||||
}
|
||||
|
||||
if !content.is_empty() {
|
||||
let context_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![stale_message.into()],
|
||||
content,
|
||||
cache: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
@@ -19,6 +20,7 @@ use language_model::{LanguageModelToolUseId, Role};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
|
||||
@@ -57,6 +59,7 @@ impl ThreadStore {
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
|
||||
@@ -184,6 +187,45 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn load_default_profile(&self, cx: &Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.load_profile_by_id(&assistant_settings.default_profile, cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
|
||||
self.load_profile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_profile(&self, profile: &AgentProfile) {
|
||||
self.tools.disable_all_tools();
|
||||
self.tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use gpui::{Entity, Subscription};
|
||||
use indexmap::IndexMap;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
|
||||
pub struct ToolSelector {
|
||||
profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ToolSelector {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
tools,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
let profiles = self.profiles.clone();
|
||||
let tool_set = self.tools.clone();
|
||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||
let icon_position = IconPosition::End;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (_id, profile) in profiles.clone() {
|
||||
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, cx| {
|
||||
tools.disable_all_tools(cx);
|
||||
|
||||
tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
|
||||
let tools_by_source = tool_set.tools_by_source(cx);
|
||||
|
||||
let all_tools_enabled = tool_set.are_all_tools_enabled();
|
||||
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_enabled {
|
||||
tools.disable_all_tools(cx);
|
||||
} else {
|
||||
tools.enable_all_tools();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (source, tools) in tools_by_source {
|
||||
let mut tools = tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let source = tool.source();
|
||||
let name = tool.name().into();
|
||||
let is_enabled = tool_set.is_enabled(&source, &name);
|
||||
|
||||
(source, name, is_enabled)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if ToolSource::Native == source {
|
||||
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
|
||||
}
|
||||
|
||||
menu = match &source {
|
||||
ToolSource::Native => menu.separator().header("Zed Tools"),
|
||||
ToolSource::ContextServer { id } => {
|
||||
let all_tools_from_source_enabled =
|
||||
tool_set.are_all_tools_from_source_enabled(&source);
|
||||
|
||||
menu.separator().header(id).toggleable_entry(
|
||||
"All Tools",
|
||||
all_tools_from_source_enabled,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let tools = tool_set.clone();
|
||||
let source = source.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_from_source_enabled {
|
||||
tools.disable_source(source.clone(), cx);
|
||||
} else {
|
||||
tools.enable_source(&source);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for (source, name, is_enabled) in tools {
|
||||
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, _cx| {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ToolSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("tool-selector-button", IconName::SettingsAlt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
Tooltip::text("Customize Tools"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,7 @@ pub struct AssistantSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub default_profile: Arc<str>,
|
||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: bool,
|
||||
@@ -174,6 +175,7 @@ impl AssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -307,6 +310,18 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: Arc<str>) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V1(_) => {}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -330,6 +345,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 {
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
#[schemars(skip)]
|
||||
profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
||||
default_profile: Option<Arc<str>>,
|
||||
#[schemars(skip)]
|
||||
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
/// that you allow it, always choose to allow it.
|
||||
///
|
||||
@@ -531,6 +549,7 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.notify_when_agent_waiting,
|
||||
value.notify_when_agent_waiting,
|
||||
);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
settings
|
||||
@@ -621,6 +640,7 @@ mod tests {
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
|
||||
@@ -80,6 +80,8 @@ pub struct ActionLog {
|
||||
stale_buffers_in_context: HashSet<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: HashMap<Entity<Buffer>, TrackedBuffer>,
|
||||
/// Has the model edited a file since it last checked diagnostics?
|
||||
edited_since_project_diagnostics_check: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -93,6 +95,7 @@ impl ActionLog {
|
||||
Self {
|
||||
stale_buffers_in_context: HashSet::default(),
|
||||
tracked_buffers: HashMap::default(),
|
||||
edited_since_project_diagnostics_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +113,12 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
self.stale_buffers_in_context.extend(buffers);
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
}
|
||||
|
||||
/// Notifies a diagnostics check
|
||||
pub fn checked_project_diagnostics(&mut self) {
|
||||
self.edited_since_project_diagnostics_check = false;
|
||||
}
|
||||
|
||||
/// Iterate over buffers changed since last read or edited by the model
|
||||
@@ -120,6 +129,11 @@ impl ActionLog {
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
/// Returns true if any files have been edited since the last project diagnostics check
|
||||
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
/// Takes and returns the set of buffers pending refresh, clearing internal state.
|
||||
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
|
||||
std::mem::take(&mut self.stale_buffers_in_context)
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct ToolWorkingSet {
|
||||
struct WorkingSetState {
|
||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
next_tool_id: ToolId,
|
||||
}
|
||||
|
||||
@@ -41,38 +41,23 @@ impl ToolWorkingSet {
|
||||
self.state.lock().tools_by_source(cx)
|
||||
}
|
||||
|
||||
pub fn are_all_tools_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
state.disabled_tools_by_source.is_empty()
|
||||
}
|
||||
|
||||
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.disabled_tools_by_source.contains_key(source)
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
self.state.lock().enabled_tools(cx)
|
||||
}
|
||||
|
||||
pub fn enable_all_tools(&self) {
|
||||
pub fn disable_all_tools(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disabled_tools_by_source.clear();
|
||||
state.disable_all_tools();
|
||||
}
|
||||
|
||||
pub fn disable_all_tools(&self, cx: &App) {
|
||||
pub fn enable_source(&self, source: ToolSource, cx: &App) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_all_tools(cx);
|
||||
state.enable_source(source, cx);
|
||||
}
|
||||
|
||||
pub fn enable_source(&self, source: &ToolSource) {
|
||||
pub fn disable_source(&self, source: &ToolSource) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_source(source);
|
||||
}
|
||||
|
||||
pub fn disable_source(&self, source: ToolSource, cx: &App) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_source(source, cx);
|
||||
state.disable_source(source);
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
@@ -159,40 +144,36 @@ impl WorkingSetState {
|
||||
}
|
||||
|
||||
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
!self.is_disabled(source, name)
|
||||
self.enabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |enabled_tools| enabled_tools.contains(name))
|
||||
}
|
||||
|
||||
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.disabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||
!self.is_enabled(source, name)
|
||||
}
|
||||
|
||||
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
self.enabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.retain(|name| !tools_to_enable.contains(name));
|
||||
.extend(tools_to_enable.into_iter().cloned());
|
||||
}
|
||||
|
||||
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
self.enabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.extend(tools_to_disable.into_iter().cloned());
|
||||
.retain(|name| !tools_to_disable.contains(name));
|
||||
}
|
||||
|
||||
fn enable_source(&mut self, source: &ToolSource) {
|
||||
self.disabled_tools_by_source.remove(source);
|
||||
}
|
||||
|
||||
fn disable_source(&mut self, source: ToolSource, cx: &App) {
|
||||
fn enable_source(&mut self, source: ToolSource, cx: &App) {
|
||||
let tools_by_source = self.tools_by_source(cx);
|
||||
let Some(tools) = tools_by_source.get(&source) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.disabled_tools_by_source.insert(
|
||||
self.enabled_tools_by_source.insert(
|
||||
source,
|
||||
tools
|
||||
.into_iter()
|
||||
@@ -201,16 +182,11 @@ impl WorkingSetState {
|
||||
);
|
||||
}
|
||||
|
||||
fn disable_all_tools(&mut self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
fn disable_source(&mut self, source: &ToolSource) {
|
||||
self.enabled_tools_by_source.remove(source);
|
||||
}
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
fn disable_all_tools(&mut self) {
|
||||
self.enabled_tools_by_source.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod bash_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
@@ -24,6 +25,7 @@ use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
@@ -43,6 +45,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
|
||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::command::new_smol_command;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
@@ -43,7 +44,14 @@ impl Tool for BashTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<BashToolInput>(input.clone()) {
|
||||
Ok(input) => format!("`{}`", input.command),
|
||||
Ok(input) => {
|
||||
let cmd = MarkdownString::escape(&input.command);
|
||||
if input.command.contains('\n') {
|
||||
format!("```bash\n{cmd}\n```")
|
||||
} else {
|
||||
format!("`{cmd}`")
|
||||
}
|
||||
}
|
||||
Err(_) => "Run bash command".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
@@ -60,8 +61,8 @@ impl Tool for CopyPathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = input.source_path.as_str();
|
||||
let dest = input.destination_path.as_str();
|
||||
let src = MarkdownString::escape(&input.source_path);
|
||||
let dest = MarkdownString::escape(&input.destination_path);
|
||||
format!("Copy `{src}` to `{dest}`")
|
||||
}
|
||||
Err(_) => "Copy path".to_string(),
|
||||
|
||||
89
crates/assistant_tools/src/create_directory_tool.rs
Normal file
89
crates/assistant_tools/src/create_directory_tool.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDirectoryToolInput {
|
||||
/// The path of the new directory.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following structure:
|
||||
///
|
||||
/// - directory1/
|
||||
/// - directory2/
|
||||
///
|
||||
/// You can create a new directory by providing a path of "directory1/new_directory"
|
||||
/// </example>
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct CreateDirectoryTool;
|
||||
|
||||
impl Tool for CreateDirectoryTool {
|
||||
fn name(&self) -> String {
|
||||
"create-directory".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./create_directory_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CreateDirectoryToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
format!("Create directory `{}`", MarkdownString::escape(&input.path))
|
||||
}
|
||||
Err(_) => "Create directory".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
|
||||
};
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_entry(project_path.clone(), true, cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created directory {destination_path}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
|
||||
|
||||
This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateFileToolInput {
|
||||
@@ -57,7 +58,7 @@ impl Tool for CreateFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = input.path.as_str();
|
||||
let path = MarkdownString::escape(&input.path);
|
||||
format!("Create file `{path}`")
|
||||
}
|
||||
Err(_) => "Create file".to_string(),
|
||||
|
||||
@@ -6,12 +6,9 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
@@ -28,7 +25,17 @@ pub struct DiagnosticsToolInput {
|
||||
///
|
||||
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// </example>
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(deserialize_with = "deserialize_path")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(deserializer)?;
|
||||
// The model passes an empty string sometimes
|
||||
Ok(opt.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
pub struct DiagnosticsTool;
|
||||
@@ -58,9 +65,12 @@ impl Tool for DiagnosticsTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
.and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(MarkdownString::escape(&path)),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
format!("Check diagnostics for “`{}`”", path.display())
|
||||
format!("Check diagnostics for `{path}`")
|
||||
} else {
|
||||
"Check project diagnostics".to_string()
|
||||
}
|
||||
@@ -71,78 +81,84 @@ impl Tool for DiagnosticsTool {
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
{
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Could not find path {} in project",
|
||||
path.display()
|
||||
)));
|
||||
};
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
Some(path) if !path.is_empty() => {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let buffer =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
action_log.update(cx, |action_log, _cx| {
|
||||
action_log.checked_project_diagnostics();
|
||||
});
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,5 @@ To get diagnostics for a specific file:
|
||||
To get a project-wide diagnostic summary:
|
||||
{}
|
||||
</example>
|
||||
|
||||
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!
|
||||
|
||||
@@ -13,6 +13,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
@@ -133,7 +134,7 @@ impl Tool for FetchTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FetchToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Fetch `{}`", input.url),
|
||||
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
|
||||
Err(_) => "Fetch URL".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ pub struct FindReplaceFileToolInput {
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly description of what's being replaced. This will be shown in the UI.
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The unique string to find in the file. This string cannot be empty;
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
@@ -61,7 +62,10 @@ impl Tool for ListDirectoryTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => format!("List the `{}` directory's contents", input.path),
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::escape(&input.path);
|
||||
format!("List the `{path}` directory's contents")
|
||||
}
|
||||
Err(_) => "List directory".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MovePathToolInput {
|
||||
@@ -60,16 +61,17 @@ impl Tool for MovePathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = input.source_path.as_str();
|
||||
let dest = input.destination_path.as_str();
|
||||
let src_path = Path::new(src);
|
||||
let dest_path = Path::new(dest);
|
||||
let src = MarkdownString::escape(&input.source_path);
|
||||
let dest = MarkdownString::escape(&input.destination_path);
|
||||
let src_path = Path::new(&input.source_path);
|
||||
let dest_path = Path::new(&input.destination_path);
|
||||
|
||||
match dest_path
|
||||
.file_name()
|
||||
.and_then(|os_str| os_str.to_os_string().into_string().ok())
|
||||
{
|
||||
Some(filename) if src_path.parent() == dest_path.parent() => {
|
||||
let filename = MarkdownString::escape(&filename);
|
||||
format!("Rename `{src}` to `{filename}`")
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -10,6 +10,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
@@ -64,7 +65,10 @@ impl Tool for ReadFileTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Read file `{}`", input.path.display()),
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::escape(&input.path.display().to_string());
|
||||
format!("Read file `{path}`")
|
||||
}
|
||||
Err(_) => "Read file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -63,14 +64,12 @@ impl Tool for RegexSearchTool {
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex = MarkdownString::escape(&input.regex);
|
||||
|
||||
if page > 1 {
|
||||
format!(
|
||||
"Get page {page} of search results for regex “`{}`”",
|
||||
input.regex
|
||||
)
|
||||
format!("Get page {page} of search results for regex “`{regex}`”")
|
||||
} else {
|
||||
format!("Search files for regex “`{}`”", input.regex)
|
||||
format!("Search files for regex “`{regex}`”")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
|
||||
@@ -790,6 +790,8 @@ pub struct Editor {
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
serialize_selections: Task<()>,
|
||||
serialize_folds: Task<()>,
|
||||
mouse_cursor_hidden: bool,
|
||||
hide_mouse_while_typing: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -1407,6 +1409,14 @@ impl Editor {
|
||||
code_action_providers.push(Rc::new(project) as Rc<_>);
|
||||
}
|
||||
|
||||
let hide_mouse_while_typing = if !matches!(mode, EditorMode::SingleLine { .. }) {
|
||||
EditorSettings::get_global(cx)
|
||||
.hide_mouse_while_typing
|
||||
.unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut this = Self {
|
||||
focus_handle,
|
||||
show_cursor_when_unfocused: false,
|
||||
@@ -1568,6 +1578,8 @@ impl Editor {
|
||||
serialize_folds: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
load_diff_task: load_uncommitted_diff,
|
||||
mouse_cursor_hidden: false,
|
||||
hide_mouse_while_typing,
|
||||
};
|
||||
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
|
||||
this._subscriptions
|
||||
@@ -2999,6 +3011,8 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
@@ -3403,6 +3417,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
@@ -3518,6 +3533,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -3575,6 +3592,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -4307,6 +4326,10 @@ impl Editor {
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.sort_completions());
|
||||
|
||||
let filter_completions = provider
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn_in(window, async move |editor, cx| {
|
||||
async move {
|
||||
@@ -4355,8 +4378,15 @@ impl Editor {
|
||||
completions.into(),
|
||||
);
|
||||
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
.await;
|
||||
menu.filter(
|
||||
if filter_completions {
|
||||
query.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
};
|
||||
@@ -7765,6 +7795,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.select_autoclose_pair(window, cx);
|
||||
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
|
||||
@@ -7863,6 +7894,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
@@ -7884,7 +7916,7 @@ impl Editor {
|
||||
if self.move_to_prev_snippet_tabstop(window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.outdent(&Outdent, window, cx);
|
||||
}
|
||||
|
||||
@@ -7892,7 +7924,7 @@ impl Editor {
|
||||
if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
let mut selections = self.selections.all_adjusted(cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -16669,6 +16701,15 @@ impl Editor {
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default();
|
||||
self.hide_mouse_while_typing = if !matches!(self.mode, EditorMode::SingleLine { .. }) {
|
||||
editor_settings.hide_mouse_while_typing.unwrap_or(true)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !self.hide_mouse_while_typing {
|
||||
self.mouse_cursor_hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
if old_cursor_shape != self.cursor_shape {
|
||||
@@ -18011,6 +18052,10 @@ pub trait CompletionProvider {
|
||||
fn sort_completions(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn filter_completions(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct EditorSettings {
|
||||
#[serde(default)]
|
||||
pub go_to_definition_fallback: GoToDefinitionFallback,
|
||||
pub jupyter: Jupyter,
|
||||
pub hide_mouse_while_typing: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -235,6 +236,10 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: None
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
/// Determines whether the mouse cursor should be hidden while typing in an editor or input box.
|
||||
///
|
||||
/// Default: true
|
||||
pub hide_mouse_while_typing: Option<bool>,
|
||||
/// How to highlight the current line in the editor.
|
||||
///
|
||||
/// Default: all
|
||||
|
||||
@@ -894,6 +894,7 @@ impl EditorElement {
|
||||
let gutter_hovered = gutter_hitbox.is_hovered(window);
|
||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||
editor.gutter_breakpoint_indicator = None;
|
||||
editor.mouse_cursor_hidden = false;
|
||||
|
||||
if gutter_hovered {
|
||||
let new_point = position_map
|
||||
@@ -4307,7 +4308,7 @@ impl EditorElement {
|
||||
let is_singleton = self.editor.read(cx).is_singleton(cx);
|
||||
|
||||
let line_height = layout.position_map.line_height;
|
||||
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
|
||||
|
||||
for LineNumberLayout {
|
||||
shaped_line,
|
||||
@@ -4340,9 +4341,9 @@ impl EditorElement {
|
||||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||||
if is_singleton {
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4564,7 +4565,7 @@ impl EditorElement {
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
{
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4636,18 +4637,24 @@ impl EditorElement {
|
||||
bounds: layout.position_map.text_hitbox.bounds,
|
||||
}),
|
||||
|window| {
|
||||
let cursor_style = if self
|
||||
.editor
|
||||
.read(cx)
|
||||
let editor = self.editor.read(cx);
|
||||
if editor.mouse_cursor_hidden {
|
||||
window.set_cursor_style(CursorStyle::None, None);
|
||||
} else if editor
|
||||
.hovered_link_state
|
||||
.as_ref()
|
||||
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
|
||||
{
|
||||
CursorStyle::PointingHand
|
||||
window.set_cursor_style(
|
||||
CursorStyle::PointingHand,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
} else {
|
||||
CursorStyle::IBeam
|
||||
window.set_cursor_style(
|
||||
CursorStyle::IBeam,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||
|
||||
self.paint_lines_background(layout, window, cx);
|
||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||
@@ -4842,7 +4849,7 @@ impl EditorElement {
|
||||
));
|
||||
})
|
||||
}
|
||||
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
@@ -6598,6 +6605,7 @@ impl Element for EditorElement {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Offset the content_bounds from the text_bounds by the gutter margin (which
|
||||
// is roughly half a character wide) to make hit testing work more like how we want.
|
||||
let content_origin =
|
||||
|
||||
@@ -90,13 +90,13 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V040(since_v0_4_0::Extension),
|
||||
V030(since_v0_3_0::Extension),
|
||||
V020(since_v0_2_0::Extension),
|
||||
V010(since_v0_1_0::Extension),
|
||||
V006(since_v0_0_6::Extension),
|
||||
V004(since_v0_0_4::Extension),
|
||||
V001(since_v0_0_1::Extension),
|
||||
V0_4_0(since_v0_4_0::Extension),
|
||||
V0_3_0(since_v0_3_0::Extension),
|
||||
V0_2_0(since_v0_2_0::Extension),
|
||||
V0_1_0(since_v0_1_0::Extension),
|
||||
V0_0_6(since_v0_0_6::Extension),
|
||||
V0_0_4(since_v0_0_4::Extension),
|
||||
V0_0_1(since_v0_0_1::Extension),
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
@@ -116,7 +116,7 @@ impl Extension {
|
||||
latest::Extension::instantiate_async(store, component, latest::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V040(extension))
|
||||
Ok(Self::V0_4_0(extension))
|
||||
} else if version >= since_v0_3_0::MIN_VERSION {
|
||||
let extension = since_v0_3_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -125,7 +125,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V030(extension))
|
||||
Ok(Self::V0_3_0(extension))
|
||||
} else if version >= since_v0_2_0::MIN_VERSION {
|
||||
let extension = since_v0_2_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -134,7 +134,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V020(extension))
|
||||
Ok(Self::V0_2_0(extension))
|
||||
} else if version >= since_v0_1_0::MIN_VERSION {
|
||||
let extension = since_v0_1_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -143,7 +143,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V010(extension))
|
||||
Ok(Self::V0_1_0(extension))
|
||||
} else if version >= since_v0_0_6::MIN_VERSION {
|
||||
let extension = since_v0_0_6::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -152,7 +152,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V006(extension))
|
||||
Ok(Self::V0_0_6(extension))
|
||||
} else if version >= since_v0_0_4::MIN_VERSION {
|
||||
let extension = since_v0_0_4::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -161,7 +161,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V004(extension))
|
||||
Ok(Self::V0_0_4(extension))
|
||||
} else {
|
||||
let extension = since_v0_0_1::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -170,19 +170,19 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V001(extension))
|
||||
Ok(Self::V0_0_1(extension))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||
match self {
|
||||
Extension::V040(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V030(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V020(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V010(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V006(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V004(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_3_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_2_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_1_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_6(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_4(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_1(ext) => ext.call_init_extension(store).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,27 +194,27 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V004(ext) => Ok(ext
|
||||
Extension::V0_0_4(ext) => Ok(ext
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -225,7 +225,7 @@ impl Extension {
|
||||
)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V001(ext) => Ok(ext
|
||||
Extension::V0_0_1(ext) => Ok(ext
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -248,7 +248,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -256,7 +256,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -264,7 +264,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -272,7 +272,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -280,7 +280,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
Extension::V0_0_6(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -288,7 +288,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(ext) => {
|
||||
Extension::V0_0_4(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -299,7 +299,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V001(ext) => {
|
||||
Extension::V0_0_1(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -321,7 +321,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -329,7 +329,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -337,7 +337,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -345,7 +345,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -353,7 +353,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
Extension::V0_0_6(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -361,7 +361,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_0_4(_) | Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_additional_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -382,12 +382,12 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(_)
|
||||
| Extension::V020(_)
|
||||
| Extension::V010(_)
|
||||
| Extension::V006(_)
|
||||
| Extension::V004(_)
|
||||
| Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_additional_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -408,12 +408,12 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(_)
|
||||
| Extension::V020(_)
|
||||
| Extension::V010(_)
|
||||
| Extension::V006(_)
|
||||
| Extension::V004(_)
|
||||
| Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,11 +424,11 @@ impl Extension {
|
||||
completions: Vec<latest::Completion>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => Ok(ext
|
||||
Extension::V0_3_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -441,7 +441,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -454,7 +454,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -467,7 +467,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -480,7 +480,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,11 +491,11 @@ impl Extension {
|
||||
symbols: Vec<latest::Symbol>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => Ok(ext
|
||||
Extension::V0_3_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -508,7 +508,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -521,7 +521,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -534,7 +534,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -547,7 +547,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,23 +558,25 @@ impl Extension {
|
||||
arguments: &[String],
|
||||
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Ok(Ok(Vec::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,23 +588,23 @@ impl Extension {
|
||||
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
|
||||
) -> Result<Result<SlashCommandOutput, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Err(anyhow!("`run_slash_command` not available prior to v0.1.0"))
|
||||
}
|
||||
}
|
||||
@@ -615,23 +617,24 @@ impl Extension {
|
||||
project: Resource<ExtensionProject>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_context_server_command(store, &context_server_id, project)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_context_server_command(store, &context_server_id, project)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_context_server_command(store, &context_server_id, project)
|
||||
.await?
|
||||
.map(Into::into)),
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) | Extension::V010(_) => {
|
||||
Err(anyhow!(
|
||||
"`context_server_command` not available prior to v0.2.0"
|
||||
))
|
||||
}
|
||||
Extension::V0_0_1(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_1_0(_) => Err(anyhow!(
|
||||
"`context_server_command` not available prior to v0.2.0"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,11 +644,11 @@ impl Extension {
|
||||
provider: &str,
|
||||
) -> Result<Result<Vec<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V030(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V020(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V010(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Err(anyhow!(
|
||||
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_1_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => Err(anyhow!(
|
||||
"`suggest_docs_packages` not available prior to v0.1.0"
|
||||
)),
|
||||
}
|
||||
@@ -659,23 +662,23 @@ impl Extension {
|
||||
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
|
||||
) -> Result<Result<(), String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Err(anyhow!("`index_docs` not available prior to v0.1.0"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ objc = "0.2"
|
||||
cocoa = "0.26"
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -38,6 +38,9 @@ impl Watcher for FsWatcher {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
EventKind::Remove(_) => Some(PathEventKind::Removed),
|
||||
// Adding this fix a weird bug on Linux after upgrading notify
|
||||
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
|
||||
EventKind::Access(_) => return,
|
||||
_ => None,
|
||||
};
|
||||
let mut path_events = event
|
||||
|
||||
@@ -11,7 +11,9 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
pub use git2 as libgit;
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::actions;
|
||||
use gpui::impl_action_with_deprecated_aliases;
|
||||
pub use repository::WORK_DIRECTORY_REPO_PATH;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
@@ -54,7 +56,13 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct RestoreFile {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
|
||||
impl_action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
|
||||
action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);
|
||||
|
||||
|
||||
@@ -935,14 +935,49 @@ impl GitPanel {
|
||||
|
||||
fn revert_selected(
|
||||
&mut self,
|
||||
_: &git::RestoreFile,
|
||||
action: &git::RestoreFile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let skip_prompt = action.skip_prompt;
|
||||
let list_entry = self.entries.get(self.selected_entry?)?.clone();
|
||||
let entry = list_entry.status_entry()?;
|
||||
self.revert_entry(&entry, window, cx);
|
||||
let entry = list_entry.status_entry()?.to_owned();
|
||||
|
||||
let prompt = if skip_prompt {
|
||||
Task::ready(Ok(0))
|
||||
} else {
|
||||
let prompt = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!(
|
||||
"Are you sure you want to restore {}?",
|
||||
entry
|
||||
.worktree_path
|
||||
.file_name()
|
||||
.unwrap_or(entry.worktree_path.as_os_str())
|
||||
.to_string_lossy()
|
||||
),
|
||||
None,
|
||||
&["Restore", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
cx.background_spawn(prompt)
|
||||
};
|
||||
|
||||
let this = cx.weak_entity();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if prompt.await? != 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.revert_entry(&entry, window, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.detach();
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
@@ -2495,7 +2530,6 @@ impl GitPanel {
|
||||
{
|
||||
return; // Hide the cancelled by user message
|
||||
} else {
|
||||
let project = self.project.clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let toast =
|
||||
@@ -2503,13 +2537,10 @@ impl GitPanel {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.action("View Log", move |window, cx| {
|
||||
let message = message.clone();
|
||||
let project = project.clone();
|
||||
let action = action.clone();
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, action, workspace, &message, window, cx,
|
||||
)
|
||||
Self::open_output(action, workspace, &message, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2531,21 +2562,17 @@ impl GitPanel {
|
||||
|
||||
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
|
||||
use remote_output::SuccessStyle::*;
|
||||
let project = self.project.clone();
|
||||
match style {
|
||||
Toast { .. } => this,
|
||||
ToastWithLog { output } => this
|
||||
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action("View Log", move |window, cx| {
|
||||
let output = output.clone();
|
||||
let project = project.clone();
|
||||
let output =
|
||||
format!("stdout:\n{}\nstderr:\n{}", output.stdout, output.stderr);
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, operation, workspace, &output, window, cx,
|
||||
)
|
||||
Self::open_output(operation, workspace, &output, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
@@ -2559,7 +2586,6 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn open_output(
|
||||
project: Entity<Project>,
|
||||
operation: impl Into<SharedString>,
|
||||
workspace: &mut Workspace,
|
||||
output: &str,
|
||||
@@ -2568,8 +2594,11 @@ impl GitPanel {
|
||||
) {
|
||||
let operation = operation.into();
|
||||
let buffer = cx.new(|cx| Buffer::local(output, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_title(format!("Output from git {operation}"), cx);
|
||||
});
|
||||
@@ -3466,7 +3495,7 @@ impl GitPanel {
|
||||
context_menu
|
||||
.context(self.focus_handle.clone())
|
||||
.action(stage_title, ToggleStaged.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile::default().boxed_clone())
|
||||
.separator()
|
||||
.action("Open Diff", Confirm.boxed_clone())
|
||||
.action("Open File", SecondaryConfirm.boxed_clone())
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Render for WindowShadow {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -375,16 +375,50 @@ macro_rules! action_with_deprecated_aliases {
|
||||
$name,
|
||||
$name,
|
||||
fn build(
|
||||
_: gpui::private::serde_json::Value,
|
||||
value: gpui::private::serde_json::Value,
|
||||
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(Box::new(Self))
|
||||
},
|
||||
|
||||
fn action_json_schema(
|
||||
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
None
|
||||
|
||||
},
|
||||
|
||||
fn deprecated_aliases() -> &'static [&'static str] {
|
||||
&[
|
||||
$($alias),*
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
gpui::register_action!($name);
|
||||
};
|
||||
}
|
||||
|
||||
/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
|
||||
#[macro_export]
|
||||
macro_rules! impl_action_with_deprecated_aliases {
|
||||
($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
|
||||
gpui::__impl_action!(
|
||||
$namespace,
|
||||
$name,
|
||||
$name,
|
||||
fn build(
|
||||
value: gpui::private::serde_json::Value,
|
||||
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
|
||||
},
|
||||
|
||||
fn action_json_schema(
|
||||
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
|
||||
generator,
|
||||
))
|
||||
},
|
||||
|
||||
fn deprecated_aliases() -> &'static [&'static str] {
|
||||
&[
|
||||
$($alias),*
|
||||
|
||||
@@ -1617,7 +1617,7 @@ impl Interactivity {
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(mouse_cursor) = style.mouse_cursor {
|
||||
window.set_cursor_style(mouse_cursor, hitbox);
|
||||
window.set_cursor_style(mouse_cursor, Some(hitbox));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ impl List {
|
||||
#[derive(Clone)]
|
||||
pub struct ListState(Rc<RefCell<StateInner>>);
|
||||
|
||||
impl std::fmt::Debug for ListState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("ListState")
|
||||
}
|
||||
}
|
||||
|
||||
struct StateInner {
|
||||
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||
last_padding: Option<Edges<Pixels>>,
|
||||
@@ -57,6 +63,7 @@ struct StateInner {
|
||||
reset: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -198,6 +205,7 @@ impl ListState {
|
||||
overdraw,
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
@@ -211,6 +219,7 @@ impl ListState {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.logical_scroll_top = None;
|
||||
state.scrollbar_drag_start_height = None;
|
||||
state.items.summary().count
|
||||
};
|
||||
|
||||
@@ -355,6 +364,62 @@ impl ListState {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Call this method when the user starts dragging the scrollbar.
|
||||
///
|
||||
/// This will prevent the height reported to the scrollbar from changing during the drag
|
||||
/// as items in the overdraw get measured, and help offset scroll position changes accordingly.
|
||||
pub fn scrollbar_drag_started(&self) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.scrollbar_drag_start_height = Some(state.items.summary().height);
|
||||
}
|
||||
|
||||
/// Called when the user stops dragging the scrollbar.
|
||||
///
|
||||
/// See `scrollbar_drag_started`.
|
||||
pub fn scrollbar_drag_ended(&self) {
|
||||
self.0.borrow_mut().scrollbar_drag_start_height.take();
|
||||
}
|
||||
|
||||
/// Set the offset from the scrollbar
|
||||
pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
|
||||
self.0.borrow_mut().set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
/// Returns the size of items we have measured.
|
||||
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
||||
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
|
||||
let state = self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
|
||||
let height = state
|
||||
.scrollbar_drag_start_height
|
||||
.unwrap_or_else(|| state.items.summary().height);
|
||||
|
||||
Size::new(bounds.size.width, height)
|
||||
}
|
||||
|
||||
/// Returns the current scroll offset adjusted for the scrollbar
|
||||
pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
|
||||
let state = &self.0.borrow();
|
||||
let logical_scroll_top = state.logical_scroll_top();
|
||||
|
||||
let mut cursor = state.items.cursor::<ListItemSummary>(&());
|
||||
let summary: ListItemSummary =
|
||||
cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||
let content_height = state.items.summary().height;
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
|
||||
|
||||
Point::new(px(0.), -offset)
|
||||
}
|
||||
|
||||
/// Return the bounds of the viewport in pixels.
|
||||
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
@@ -695,6 +760,37 @@ impl StateInner {
|
||||
Ok(layout_response)
|
||||
})
|
||||
}
|
||||
|
||||
// Scrollbar support
|
||||
|
||||
fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
|
||||
let Some(bounds) = self.last_layout_bounds else {
|
||||
return;
|
||||
};
|
||||
let height = bounds.size.height;
|
||||
|
||||
let padding = self.last_padding.unwrap_or_default();
|
||||
let content_height = self.items.summary().height;
|
||||
let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
||||
|
||||
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
||||
self.logical_scroll_top = None;
|
||||
} else {
|
||||
let mut cursor = self.items.cursor::<ListItemSummary>(&());
|
||||
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
|
||||
|
||||
let item_ix = cursor.start().count;
|
||||
let offset_in_item = new_scroll_top - cursor.start().height;
|
||||
self.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ListItem {
|
||||
|
||||
@@ -700,7 +700,7 @@ impl Element for InteractiveText {
|
||||
.iter()
|
||||
.any(|range| range.contains(&ix))
|
||||
{
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,6 +1228,9 @@ pub enum CursorStyle {
|
||||
/// A cursor indicating that the operation will result in a context menu
|
||||
/// corresponds to the CSS cursor value `context-menu`
|
||||
ContextualMenu,
|
||||
|
||||
/// Hide the cursor
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Context as _;
|
||||
use blade_graphics as gpu;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(Clone))]
|
||||
pub struct BladeContext {
|
||||
@@ -8,12 +10,24 @@ pub struct BladeContext {
|
||||
|
||||
impl BladeContext {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let device_id_forced = match std::env::var("ZED_DEVICE_ID") {
|
||||
Ok(val) => val
|
||||
.parse()
|
||||
.context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
|
||||
.log_err(),
|
||||
Err(std::env::VarError::NotPresent) => None,
|
||||
err => {
|
||||
err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
|
||||
.log_err();
|
||||
None
|
||||
}
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init(gpu::ContextDesc {
|
||||
presentation: true,
|
||||
validation: false,
|
||||
device_id: 0, //TODO: hook up to user settings
|
||||
device_id: device_id_forced.unwrap_or(0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -532,6 +532,12 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
quad.border_widths.top,
|
||||
center_to_point.y < 0.0));
|
||||
|
||||
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
|
||||
// The purpose of this is to not draw antialiasing pixels in this case.
|
||||
let reduced_border =
|
||||
vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
|
||||
select(border.y, -antialias_threshold, border.y == 0.0));
|
||||
|
||||
// Vector from the corner of the quad bounds to the point, after mirroring
|
||||
// the point into the bottom right quadrant. Both components are <= 0.
|
||||
let corner_to_point = abs(center_to_point) - half_size;
|
||||
@@ -546,15 +552,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
corner_center_to_point.y >= 0;
|
||||
|
||||
// Vector from straight border inner corner to point.
|
||||
let straight_border_inner_corner_to_point = corner_to_point + border;
|
||||
let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
|
||||
|
||||
// Whether the point is beyond the inner edge of the straight border.
|
||||
let is_beyond_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x > 0 ||
|
||||
straight_border_inner_corner_to_point.y > 0;
|
||||
|
||||
// Whether the point is far enough inside the straight border such that
|
||||
// pixels are not affected by it.
|
||||
// Whether the point is far enough inside the quad, such that the pixels are
|
||||
// not affected by the straight border.
|
||||
let is_within_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x < -antialias_threshold &&
|
||||
straight_border_inner_corner_to_point.y < -antialias_threshold;
|
||||
@@ -589,11 +595,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
} else if (is_beyond_inner_straight_border) {
|
||||
// Fast path for points that must be outside the inner edge.
|
||||
inner_sdf = -1.0;
|
||||
} else if (border.x == border.y) {
|
||||
} else if (reduced_border.x == reduced_border.y) {
|
||||
// Fast path for circular inner edge.
|
||||
inner_sdf = -(outer_sdf + border.x);
|
||||
inner_sdf = -(outer_sdf + reduced_border.x);
|
||||
} else {
|
||||
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - border);
|
||||
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
|
||||
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
|
||||
}
|
||||
|
||||
|
||||
@@ -666,6 +666,12 @@ impl CursorStyle {
|
||||
CursorStyle::DragLink => "alias",
|
||||
CursorStyle::DragCopy => "copy",
|
||||
CursorStyle::ContextualMenu => "context-menu",
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
"default"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ impl CursorStyle {
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
Shape::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +667,13 @@ impl LinuxClient for WaylandClient {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
state.cursor_style = Some(style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
if let CursorStyle::None = style {
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
wl_pointer.set_cursor(serial, None, 0, 0);
|
||||
} else if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
} else if let Some(focused_window) = &state.mouse_focused_window {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
|
||||
@@ -1438,13 +1438,16 @@ impl LinuxClient for X11Client {
|
||||
let cursor = match state.cursor_cache.get(&style) {
|
||||
Some(cursor) => *cursor,
|
||||
None => {
|
||||
let Some(cursor) = state
|
||||
.cursor_handle
|
||||
.load_cursor(&state.xcb_connection, &style.to_icon_name())
|
||||
.log_err()
|
||||
else {
|
||||
let Some(cursor) = (match style {
|
||||
CursorStyle::None => create_invisible_cursor(&state.xcb_connection).log_err(),
|
||||
_ => state
|
||||
.cursor_handle
|
||||
.load_cursor(&state.xcb_connection, &style.to_icon_name())
|
||||
.log_err(),
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.cursor_cache.insert(style, cursor);
|
||||
cursor
|
||||
}
|
||||
@@ -1938,3 +1941,19 @@ fn make_scroll_wheel_event(
|
||||
touch_phase: TouchPhase::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_invisible_cursor(
|
||||
connection: &XCBConnection,
|
||||
) -> anyhow::Result<crate::platform::linux::x11::client::xproto::Cursor> {
|
||||
let empty_pixmap = connection.generate_id()?;
|
||||
let root = connection.setup().roots[0].root;
|
||||
connection.create_pixmap(1, empty_pixmap, root, 1, 1)?;
|
||||
|
||||
let cursor = connection.generate_id()?;
|
||||
connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?;
|
||||
|
||||
connection.free_pixmap(empty_pixmap)?;
|
||||
|
||||
connection.flush()?;
|
||||
Ok(cursor)
|
||||
}
|
||||
|
||||
@@ -891,6 +891,11 @@ impl Platform for MacPlatform {
|
||||
/// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
unsafe {
|
||||
if style == CursorStyle::None {
|
||||
let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves:YES];
|
||||
return;
|
||||
}
|
||||
|
||||
let new_cursor: id = match style {
|
||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
@@ -925,6 +930,7 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
|
||||
CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
|
||||
CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
|
||||
CursorStyle::None => unreachable!(),
|
||||
};
|
||||
|
||||
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
|
||||
|
||||
@@ -133,6 +133,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom
|
||||
);
|
||||
|
||||
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
|
||||
// The purpose of this is to not draw antialiasing pixels in this case.
|
||||
float2 reduced_border = float2(
|
||||
border.x == 0.0 ? -antialias_threshold : border.x,
|
||||
border.y == 0.0 ? -antialias_threshold : border.y);
|
||||
|
||||
// Vector from the corner of the quad bounds to the point, after mirroring
|
||||
// the point into the bottom right quadrant. Both components are <= 0.
|
||||
float2 corner_to_point = fabs(center_to_point) - half_size;
|
||||
@@ -146,16 +152,20 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
corner_center_to_point.x >= 0.0 &&
|
||||
corner_center_to_point.y >= 0.0;
|
||||
|
||||
// Vector from straight border inner corner to point
|
||||
float2 straight_border_inner_corner_to_point = corner_to_point + border;
|
||||
// Vector from straight border inner corner to point.
|
||||
//
|
||||
// 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near
|
||||
// the border. Without this, antialiasing pixels would be drawn.
|
||||
float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border;
|
||||
|
||||
// Whether the point is beyond the inner edge of the straight border
|
||||
bool is_beyond_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x > 0.0 ||
|
||||
straight_border_inner_corner_to_point.y > 0.0;
|
||||
|
||||
// Whether the point is far enough inside the straight border such that
|
||||
// pixels are not affected by it
|
||||
|
||||
// Whether the point is far enough inside the quad, such that the pixels are
|
||||
// not affected by the straight border.
|
||||
bool is_within_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x < -antialias_threshold &&
|
||||
straight_border_inner_corner_to_point.y < -antialias_threshold;
|
||||
@@ -184,11 +194,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
} else if (is_beyond_inner_straight_border) {
|
||||
// Fast path for points that must be outside the inner edge
|
||||
inner_sdf = -1.0;
|
||||
} else if (border.x == border.y) {
|
||||
} else if (reduced_border.x == reduced_border.y) {
|
||||
// Fast path for circular inner edge.
|
||||
inner_sdf = -(outer_sdf + border.x);
|
||||
inner_sdf = -(outer_sdf + reduced_border.x);
|
||||
} else {
|
||||
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - border);
|
||||
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border);
|
||||
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
|
||||
}
|
||||
|
||||
|
||||
@@ -1127,7 +1127,19 @@ fn handle_nc_mouse_up_msg(
|
||||
}
|
||||
|
||||
fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
state_ptr.state.borrow_mut().current_cursor = HCURSOR(lparam.0 as _);
|
||||
let mut state = state_ptr.state.borrow_mut();
|
||||
let had_cursor = state.current_cursor.is_some();
|
||||
|
||||
state.current_cursor = if lparam.0 == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(HCURSOR(lparam.0 as _))
|
||||
};
|
||||
|
||||
if had_cursor != state.current_cursor.is_some() {
|
||||
unsafe { SetCursor(state.current_cursor) };
|
||||
}
|
||||
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -1138,7 +1150,9 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
|
||||
unsafe {
|
||||
SetCursor(state_ptr.state.borrow().current_cursor);
|
||||
};
|
||||
Some(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(crate) struct WindowsPlatformState {
|
||||
menus: Vec<OwnedMenu>,
|
||||
dock_menu_actions: Vec<Box<dyn Action>>,
|
||||
// NOTE: standard cursor handles don't need to close.
|
||||
pub(crate) current_cursor: HCURSOR,
|
||||
pub(crate) current_cursor: Option<HCURSOR>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -558,11 +558,11 @@ impl Platform for WindowsPlatform {
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let hcursor = load_cursor(style);
|
||||
let mut lock = self.state.borrow_mut();
|
||||
if lock.current_cursor.0 != hcursor.0 {
|
||||
if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) {
|
||||
self.post_message(
|
||||
WM_GPUI_CURSOR_STYLE_CHANGED,
|
||||
WPARAM(0),
|
||||
LPARAM(hcursor.0 as isize),
|
||||
LPARAM(hcursor.map_or(0, |c| c.0 as isize)),
|
||||
);
|
||||
lock.current_cursor = hcursor;
|
||||
}
|
||||
@@ -683,7 +683,7 @@ impl Drop for WindowsPlatform {
|
||||
pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) icon: HICON,
|
||||
pub(crate) executor: ForegroundExecutor,
|
||||
pub(crate) current_cursor: HCURSOR,
|
||||
pub(crate) current_cursor: Option<HCURSOR>,
|
||||
pub(crate) windows_version: WindowsVersion,
|
||||
pub(crate) validation_number: usize,
|
||||
pub(crate) main_receiver: flume::Receiver<Runnable>,
|
||||
|
||||
@@ -106,7 +106,7 @@ pub(crate) fn windows_credentials_target_name(url: &str) -> String {
|
||||
format!("zed:url={}", url)
|
||||
}
|
||||
|
||||
pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
|
||||
pub(crate) fn load_cursor(style: CursorStyle) -> Option<HCURSOR> {
|
||||
static ARROW: OnceLock<SafeCursor> = OnceLock::new();
|
||||
static IBEAM: OnceLock<SafeCursor> = OnceLock::new();
|
||||
static CROSS: OnceLock<SafeCursor> = OnceLock::new();
|
||||
@@ -127,17 +127,20 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
|
||||
| CursorStyle::ResizeUpDown
|
||||
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
|
||||
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
|
||||
CursorStyle::None => return None,
|
||||
_ => (&ARROW, IDC_ARROW),
|
||||
};
|
||||
*(*lock.get_or_init(|| {
|
||||
HCURSOR(
|
||||
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
|
||||
.log_err()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
)
|
||||
.into()
|
||||
}))
|
||||
Some(
|
||||
*(*lock.get_or_init(|| {
|
||||
HCURSOR(
|
||||
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
|
||||
.log_err()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
)
|
||||
.into()
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// This function is used to configure the dark mode for the window built-in title bar.
|
||||
|
||||
@@ -48,7 +48,7 @@ pub struct WindowsWindowState {
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
pub current_cursor: HCURSOR,
|
||||
pub current_cursor: Option<HCURSOR>,
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
pub display: WindowsDisplay,
|
||||
@@ -76,7 +76,7 @@ impl WindowsWindowState {
|
||||
hwnd: HWND,
|
||||
transparent: bool,
|
||||
cs: &CREATESTRUCTW,
|
||||
current_cursor: HCURSOR,
|
||||
current_cursor: Option<HCURSOR>,
|
||||
display: WindowsDisplay,
|
||||
gpu_context: &BladeContext,
|
||||
) -> Result<Self> {
|
||||
@@ -351,7 +351,7 @@ struct WindowCreateContext<'a> {
|
||||
transparent: bool,
|
||||
is_movable: bool,
|
||||
executor: ForegroundExecutor,
|
||||
current_cursor: HCURSOR,
|
||||
current_cursor: Option<HCURSOR>,
|
||||
windows_version: WindowsVersion,
|
||||
validation_number: usize,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
|
||||
@@ -407,7 +407,7 @@ pub(crate) type AnyMouseListener =
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CursorStyleRequest {
|
||||
pub(crate) hitbox_id: HitboxId,
|
||||
pub(crate) hitbox_id: Option<HitboxId>, // None represents whole window
|
||||
pub(crate) style: CursorStyle,
|
||||
}
|
||||
|
||||
@@ -1928,10 +1928,10 @@ impl Window {
|
||||
|
||||
/// Updates the cursor style at the platform level. This method should only be called
|
||||
/// during the prepaint phase of element drawing.
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
self.next_frame.cursor_styles.push(CursorStyleRequest {
|
||||
hitbox_id: hitbox.id,
|
||||
hitbox_id: hitbox.map(|hitbox| hitbox.id),
|
||||
style,
|
||||
});
|
||||
}
|
||||
@@ -2984,7 +2984,11 @@ impl Window {
|
||||
.cursor_styles
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|request| request.hitbox_id.is_hovered(self))
|
||||
.find(|request| {
|
||||
request
|
||||
.hitbox_id
|
||||
.map_or(true, |hitbox_id| hitbox_id.is_hovered(self))
|
||||
})
|
||||
.map(|request| request.style)
|
||||
.unwrap_or(CursorStyle::Arrow);
|
||||
cx.platform.set_cursor_style(style);
|
||||
@@ -3241,6 +3245,7 @@ impl Window {
|
||||
keystroke,
|
||||
&dispatch_path,
|
||||
);
|
||||
|
||||
if !match_result.to_replay.is_empty() {
|
||||
self.replay_pending_input(match_result.to_replay, cx)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,13 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `none`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
#visibility fn cursor_none(mut self, cursor: CursorStyle) -> Self {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::None);
|
||||
self
|
||||
}
|
||||
};
|
||||
|
||||
output.into()
|
||||
|
||||
@@ -29,6 +29,7 @@ use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -588,6 +589,28 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
|
||||
match k {
|
||||
PythonEnvironmentKind::Conda => "Conda",
|
||||
PythonEnvironmentKind::Pixi => "pixi",
|
||||
PythonEnvironmentKind::Homebrew => "Homebrew",
|
||||
PythonEnvironmentKind::Pyenv => "global (Pyenv)",
|
||||
PythonEnvironmentKind::GlobalPaths => "global",
|
||||
PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
|
||||
PythonEnvironmentKind::Pipenv => "Pipenv",
|
||||
PythonEnvironmentKind::Poetry => "Poetry",
|
||||
PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
|
||||
PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
|
||||
PythonEnvironmentKind::LinuxGlobal => "global",
|
||||
PythonEnvironmentKind::MacXCode => "global (Xcode)",
|
||||
PythonEnvironmentKind::Venv => "venv",
|
||||
PythonEnvironmentKind::VirtualEnv => "virtualenv",
|
||||
PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
|
||||
PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
|
||||
PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PythonToolchainProvider {
|
||||
term: SharedString,
|
||||
}
|
||||
@@ -683,14 +706,26 @@ impl ToolchainLister for PythonToolchainProvider {
|
||||
let mut toolchains: Vec<_> = toolchains
|
||||
.into_iter()
|
||||
.filter_map(|toolchain| {
|
||||
let name = if let Some(version) = &toolchain.version {
|
||||
format!("Python {version} ({:?})", toolchain.kind?)
|
||||
} else {
|
||||
format!("{:?}", toolchain.kind?)
|
||||
let mut name = String::from("Python");
|
||||
if let Some(ref version) = toolchain.version {
|
||||
_ = write!(name, " {version}");
|
||||
}
|
||||
.into();
|
||||
|
||||
let name_and_kind = match (&toolchain.name, &toolchain.kind) {
|
||||
(Some(name), Some(kind)) => {
|
||||
Some(format!("({name}; {})", python_env_kind_display(kind)))
|
||||
}
|
||||
(Some(name), None) => Some(format!("({name})")),
|
||||
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
if let Some(nk) = name_and_kind {
|
||||
_ = write!(name, " {nk}");
|
||||
}
|
||||
|
||||
Some(Toolchain {
|
||||
name,
|
||||
name: name.into(),
|
||||
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
|
||||
language_name: LanguageName::new("Python"),
|
||||
as_json: serde_json::to_value(toolchain).ok()?,
|
||||
|
||||
@@ -411,9 +411,9 @@ impl MarkdownElement {
|
||||
.is_some();
|
||||
|
||||
if is_hovering_link {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
|
||||
}
|
||||
|
||||
self.on_mouse_event(window, cx, {
|
||||
|
||||
@@ -36,7 +36,7 @@ use project_panel_settings::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
use std::{
|
||||
@@ -197,6 +197,7 @@ actions!(
|
||||
Open,
|
||||
OpenPermanent,
|
||||
ToggleFocus,
|
||||
ToggleHideGitIgnore,
|
||||
NewSearchInDirectory,
|
||||
UnfoldDirectory,
|
||||
FoldDirectory,
|
||||
@@ -233,6 +234,13 @@ pub fn init(cx: &mut App) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectPanelSettings>(fs, cx, move |setting, _| {
|
||||
setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false));
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -414,6 +422,9 @@ impl ProjectPanel {
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
let new_settings = *ProjectPanelSettings::get_global(cx);
|
||||
if project_panel_settings != new_settings {
|
||||
if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
|
||||
this.update_visible_entries(None, cx);
|
||||
}
|
||||
project_panel_settings = new_settings;
|
||||
this.update_diagnostics(cx);
|
||||
cx.notify();
|
||||
@@ -1536,7 +1547,6 @@ impl ProjectPanel {
|
||||
if sanitized_entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let (worktree_id, worktree) = sanitized_entries
|
||||
.iter()
|
||||
@@ -1568,13 +1578,14 @@ impl ProjectPanel {
|
||||
|
||||
// Remove all siblings that are being deleted except the last marked entry
|
||||
let snapshot = worktree.snapshot();
|
||||
let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
|
||||
let mut siblings: Vec<_> = ChildEntriesGitIter::new(&snapshot, parent_path)
|
||||
.filter(|sibling| {
|
||||
sibling.id == latest_entry.id
|
||||
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
(sibling.id == latest_entry.id)
|
||||
|| (!marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: sibling.id,
|
||||
})
|
||||
}) && (!hide_gitignore || !sibling.is_ignored))
|
||||
})
|
||||
.map(|entry| entry.to_owned())
|
||||
.collect();
|
||||
@@ -2590,7 +2601,9 @@ impl ProjectPanel {
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let auto_collapse_dirs = settings.auto_fold_dirs;
|
||||
let hide_gitignore = settings.hide_gitignore;
|
||||
let project = self.project.read(cx);
|
||||
self.last_worktree_root_id = project
|
||||
.visible_worktrees(cx)
|
||||
@@ -2675,7 +2688,9 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
auto_folded_ancestors.clear();
|
||||
visible_worktree_entries.push(entry.to_owned());
|
||||
if !hide_gitignore || !entry.is_ignored {
|
||||
visible_worktree_entries.push(entry.to_owned());
|
||||
}
|
||||
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
|
||||
entry.id == new_entry_id || {
|
||||
self.ancestors.get(&entry.id).map_or(false, |entries| {
|
||||
@@ -2688,7 +2703,7 @@ impl ProjectPanel {
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if precedes_new_entry {
|
||||
if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
|
||||
visible_worktree_entries.push(GitEntry {
|
||||
entry: Entry {
|
||||
id: NEW_ENTRY_ID,
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum EntrySpacing {
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub button: bool,
|
||||
pub hide_gitignore: bool,
|
||||
pub default_width: Pixels,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub entry_spacing: EntrySpacing,
|
||||
@@ -93,6 +94,10 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Whether to hide gitignore files in the project panel.
|
||||
///
|
||||
/// Default: false
|
||||
pub hide_gitignore: Option<bool>,
|
||||
/// Customize default width (in pixels) taken by project panel
|
||||
///
|
||||
/// Default: 240
|
||||
|
||||
@@ -3735,6 +3735,172 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"aa": "// Testing 1",
|
||||
"bb": "// Testing 2",
|
||||
"cc": "// Testing 3",
|
||||
"dd": "// Testing 4",
|
||||
"ee": "// Testing 5",
|
||||
"ff": "// Testing 6",
|
||||
"gg": "// Testing 7",
|
||||
"hh": "// Testing 8",
|
||||
"ii": "// Testing 8",
|
||||
".gitignore": "bb\ndd\nee\nff\nii\n'",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Test 1: Auto selection with one gitignored file next to the deleted file
|
||||
cx.update(|_, cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
hide_gitignore: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
select_path(&panel, "root/aa", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" aa <== selected",
|
||||
" cc",
|
||||
" gg",
|
||||
" hh"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" cc <== selected",
|
||||
" gg",
|
||||
" hh"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
|
||||
// Test 2: Auto selection with many gitignored files next to the deleted file
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" gg <== selected",
|
||||
" hh"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
|
||||
// Test 3: Auto selection of entry before deleted file
|
||||
select_path(&panel, "root/hh", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" gg",
|
||||
" hh <== selected"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " .gitignore", " gg <== selected"],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir1": {
|
||||
"file1": "// Testing",
|
||||
"file2": "// Testing",
|
||||
"file3": "// Testing"
|
||||
},
|
||||
"aa": "// Testing",
|
||||
".gitignore": "file1\nfile3\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
hide_gitignore: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
// Test 1: Visible items should exclude files on gitignore
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
select_path(&panel, "root/dir1/file2", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" file2 <== selected",
|
||||
" .gitignore",
|
||||
" aa"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
submit_deletion(&panel, cx);
|
||||
|
||||
// Test 2: Auto selection should go to the parent
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1 <== selected",
|
||||
" .gitignore",
|
||||
" aa"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
@@ -1911,6 +1911,10 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vi_mode_enabled(&self) -> bool {
|
||||
self.vi_mode_enabled
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to convert a grid row to a string
|
||||
|
||||
@@ -886,9 +886,9 @@ impl Element for TerminalElement {
|
||||
&& bounds.contains(&window.mouse_position())
|
||||
&& self.terminal_view.read(cx).hover_target_tooltip.is_some()
|
||||
{
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(&layout.hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::IBeam, Some(&layout.hitbox));
|
||||
}
|
||||
|
||||
let cursor = layout.cursor.take();
|
||||
|
||||
@@ -590,6 +590,10 @@ impl TerminalView {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("Terminal");
|
||||
|
||||
if self.terminal.read(cx).vi_mode_enabled() {
|
||||
dispatch_context.add("vi_mode");
|
||||
}
|
||||
|
||||
let mode = self.terminal.read(cx).last_content.mode;
|
||||
dispatch_context.set(
|
||||
"screen",
|
||||
|
||||
@@ -311,7 +311,7 @@ mod uniform_list {
|
||||
});
|
||||
let mut hovered_hitbox_id = None;
|
||||
for (i, hitbox) in hitboxes.iter().enumerate() {
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(hitbox));
|
||||
let indent_guide = &self.indent_guides[i];
|
||||
let fill_color = if hitbox.is_hovered(window) {
|
||||
hovered_hitbox_id = Some(hitbox.id);
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::{prelude::*, px, relative, IntoElement};
|
||||
use gpui::{
|
||||
point, quad, Along, App, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Corners,
|
||||
Edges, Element, ElementId, Entity, EntityId, GlobalElementId, Hitbox, Hsla, LayoutId,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle, ScrollWheelEvent,
|
||||
Size, Style, UniformListScrollHandle, Window,
|
||||
ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, ScrollHandle,
|
||||
ScrollWheelEvent, Size, Style, UniformListScrollHandle, Window,
|
||||
};
|
||||
|
||||
pub struct Scrollbar {
|
||||
@@ -39,6 +39,39 @@ impl ScrollableHandle for UniformListScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for ListState {
|
||||
fn content_size(&self) -> Option<ContentSize> {
|
||||
Some(ContentSize {
|
||||
size: self.content_size_for_scrollbar(),
|
||||
scroll_adjustment: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_offset(&self, point: Point<Pixels>) {
|
||||
self.set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.scroll_px_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn drag_started(&self) {
|
||||
self.scrollbar_drag_started();
|
||||
}
|
||||
|
||||
fn drag_ended(&self) {
|
||||
self.scrollbar_drag_ended();
|
||||
}
|
||||
|
||||
fn viewport(&self) -> Bounds<Pixels> {
|
||||
self.viewport_bounds()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableHandle for ScrollHandle {
|
||||
fn content_size(&self) -> Option<ContentSize> {
|
||||
let last_children_index = self.children_count().checked_sub(1)?;
|
||||
@@ -92,6 +125,8 @@ pub trait ScrollableHandle: Debug + 'static {
|
||||
fn offset(&self) -> Point<Pixels>;
|
||||
fn viewport(&self) -> Bounds<Pixels>;
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
fn drag_started(&self) {}
|
||||
fn drag_ended(&self) {}
|
||||
}
|
||||
|
||||
/// A scrollbar state that should be persisted across frames.
|
||||
@@ -300,6 +335,8 @@ impl Element for Scrollbar {
|
||||
return;
|
||||
}
|
||||
|
||||
scroll.drag_started();
|
||||
|
||||
if thumb_bounds.contains(&event.position) {
|
||||
let offset = event.position.along(axis) - thumb_bounds.origin.along(axis);
|
||||
state.drag.set(Some(offset));
|
||||
@@ -349,7 +386,7 @@ impl Element for Scrollbar {
|
||||
});
|
||||
let state = self.state.clone();
|
||||
let axis = self.kind;
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, _, _, cx| {
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, _, window, cx| {
|
||||
if let Some(drag_state) = state.drag.get().filter(|_| event.dragging()) {
|
||||
if let Some(ContentSize {
|
||||
size: item_size, ..
|
||||
@@ -381,6 +418,7 @@ impl Element for Scrollbar {
|
||||
scroll.set_offset(point(scroll.offset().x, drag_offset));
|
||||
}
|
||||
};
|
||||
window.refresh();
|
||||
if let Some(id) = state.parent_id {
|
||||
cx.notify(id);
|
||||
}
|
||||
@@ -390,9 +428,11 @@ impl Element for Scrollbar {
|
||||
}
|
||||
});
|
||||
let state = self.state.clone();
|
||||
let scroll = self.state.scroll_handle.clone();
|
||||
window.on_mouse_event(move |_event: &MouseUpEvent, phase, _, cx| {
|
||||
if phase.bubble() {
|
||||
state.drag.take();
|
||||
scroll.drag_ended();
|
||||
if let Some(id) = state.parent_id {
|
||||
cx.notify(id);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ impl MarkdownString {
|
||||
/// * `$` for inline math
|
||||
/// * `~` for strikethrough
|
||||
///
|
||||
/// Escape of some character is unnecessary because while they are involved in markdown syntax,
|
||||
/// Escape of some characters is unnecessary, because while they are involved in markdown syntax,
|
||||
/// the other characters involved are escaped:
|
||||
///
|
||||
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as
|
||||
|
||||
@@ -3315,13 +3315,28 @@ impl Render for Pane {
|
||||
})
|
||||
.map(|div| {
|
||||
if let Some(item) = self.active_item() {
|
||||
div.v_flex()
|
||||
div.id("pane_placeholder")
|
||||
.v_flex()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(self.toolbar.clone())
|
||||
.child(item.to_any())
|
||||
} else {
|
||||
let placeholder = div.h_flex().size_full().justify_center();
|
||||
let placeholder = div
|
||||
.id("pane_placeholder")
|
||||
.h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.on_click(cx.listener(
|
||||
move |this, event: &ClickEvent, window, cx| {
|
||||
if event.up.click_count == 2 {
|
||||
window.dispatch_action(
|
||||
this.double_click_dispatch_action.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
));
|
||||
if has_worktrees {
|
||||
placeholder
|
||||
} else {
|
||||
|
||||
@@ -1176,7 +1176,7 @@ mod element {
|
||||
Axis::Vertical => CursorStyle::ResizeRow,
|
||||
Axis::Horizontal => CursorStyle::ResizeColumn,
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &handle.hitbox);
|
||||
window.set_cursor_style(cursor_style, Some(&handle.hitbox));
|
||||
window.paint_quad(gpui::fill(
|
||||
handle.divider_bounds,
|
||||
cx.theme().colors().pane_group_border,
|
||||
|
||||
@@ -6702,7 +6702,7 @@ pub fn client_side_decorations(
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.180.0"
|
||||
version = "0.181.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -553,6 +553,16 @@ List of `string` values
|
||||
"cursor_shape": "hollow"
|
||||
```
|
||||
|
||||
## Hide Mouse While Typing
|
||||
|
||||
- Description: Determines whether the mouse cursor should be hidden while typing in an editor or input box.
|
||||
- Setting: `hide_mouse_while_typing`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Editor Scrollbar
|
||||
|
||||
- Description: Whether or not to show the editor scrollbar and various elements in it.
|
||||
|
||||
@@ -17,6 +17,55 @@ If you do not want to use the HTML extension, you can add the following to your
|
||||
}
|
||||
```
|
||||
|
||||
## Formatting
|
||||
|
||||
By default Zed uses [Prettier](https://prettier.io/) for formatting HTML
|
||||
|
||||
You can disable `format_on_save` by adding the following to your Zed settings:
|
||||
|
||||
```json
|
||||
"languages": {
|
||||
"HTML": {
|
||||
"format_on_save": "off",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can still trigger formatting manually with {#kb editor::Format} or by opening the command palette ( {#kb commandPalette::Toggle} and selecting `Format Document`.
|
||||
|
||||
### LSP Formatting
|
||||
|
||||
If you prefer you can use `vscode-html-language-server` instead of Prettier for auto-formatting by adding the following to your Zed settings:
|
||||
|
||||
```json
|
||||
"languages": {
|
||||
"HTML": {
|
||||
"formatter": "language_server",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can customize various [formatting options](https://code.visualstudio.com/docs/languages/html#_formatting) for `vscode-html-language-server` via Zed settings.json:
|
||||
|
||||
```json
|
||||
"lsp": {
|
||||
"vscode-html-language-server": {
|
||||
"settings": {
|
||||
"html": {
|
||||
"format": {
|
||||
// Indent under <html> and <head> (default: false)
|
||||
"indentInnerHtml": true,
|
||||
// Disable formatting inside <svg> or <script>
|
||||
"contentUnformatted": "svg,script",
|
||||
// Add an extra newline before <div> and <p>
|
||||
"extraLiners": "div,p"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## See also:
|
||||
|
||||
- [CSS](./css.md)
|
||||
|
||||
@@ -16,4 +16,4 @@ language = "HTML"
|
||||
|
||||
[grammars.html]
|
||||
repository = "https://github.com/tree-sitter/tree-sitter-html"
|
||||
commit = "bfa075d83c6b97cd48440b3829ab8d24a2319809"
|
||||
commit = "5a5ca8551a179998360b4a4ca2c0f366a35acc03"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
use std::{env, fs};
|
||||
use zed::settings::LspSettings;
|
||||
use zed_extension_api::{self as zed, LanguageServerId, Result};
|
||||
use zed_extension_api::{self as zed, serde_json::json, LanguageServerId, Result};
|
||||
|
||||
const BINARY_NAME: &str = "vscode-html-language-server";
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/@zed-industries/vscode-langservers-extracted/bin/vscode-html-language-server";
|
||||
const PACKAGE_NAME: &str = "@zed-industries/vscode-langservers-extracted";
|
||||
|
||||
struct HtmlExtension {
|
||||
did_find_server: bool,
|
||||
cached_binary_path: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlExtension {
|
||||
@@ -17,7 +18,7 @@ impl HtmlExtension {
|
||||
|
||||
fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result<String> {
|
||||
let server_exists = self.server_exists();
|
||||
if self.did_find_server && server_exists {
|
||||
if self.cached_binary_path.is_some() && server_exists {
|
||||
return Ok(SERVER_PATH.to_string());
|
||||
}
|
||||
|
||||
@@ -50,8 +51,6 @@ impl HtmlExtension {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.did_find_server = true;
|
||||
Ok(SERVER_PATH.to_string())
|
||||
}
|
||||
}
|
||||
@@ -59,16 +58,22 @@ impl HtmlExtension {
|
||||
impl zed::Extension for HtmlExtension {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
did_find_server: false,
|
||||
cached_binary_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn language_server_command(
|
||||
&mut self,
|
||||
language_server_id: &LanguageServerId,
|
||||
_worktree: &zed::Worktree,
|
||||
worktree: &zed::Worktree,
|
||||
) -> Result<zed::Command> {
|
||||
let server_path = self.server_script_path(language_server_id)?;
|
||||
let server_path = if let Some(path) = worktree.which(BINARY_NAME) {
|
||||
path
|
||||
} else {
|
||||
self.server_script_path(language_server_id)?
|
||||
};
|
||||
self.cached_binary_path = Some(server_path.clone());
|
||||
|
||||
Ok(zed::Command {
|
||||
command: zed::node_binary_path()?,
|
||||
args: vec![
|
||||
@@ -94,6 +99,15 @@ impl zed::Extension for HtmlExtension {
|
||||
.unwrap_or_default();
|
||||
Ok(Some(settings))
|
||||
}
|
||||
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
_server_id: &LanguageServerId,
|
||||
_worktree: &zed_extension_api::Worktree,
|
||||
) -> Result<Option<zed_extension_api::serde_json::Value>> {
|
||||
let initialization_options = json!({"provideFormatter": true });
|
||||
Ok(Some(initialization_options))
|
||||
}
|
||||
}
|
||||
|
||||
zed::register_extension!(HtmlExtension);
|
||||
|
||||
Reference in New Issue
Block a user