Compare commits

...

24 Commits

Author SHA1 Message Date
Peter Tripp
8494a33b98 html: Update tree-sitter-html to v0.23.2
No changes to the grammar, but updated to tree-sitter v0.24

Diff:
- bfa075d83c...5a5ca8551a
2025-03-26 17:56:46 -04:00
Bennet Bo Fenner
2b5095ac91 assistant2: Fix filtering issue when using @mention completion provider (#27541)
Previously `src` would not show up because it was filtered out:

<img width="466" alt="image"
src="https://github.com/user-attachments/assets/f3802660-ad73-44be-967d-c332466d9aba"
/>

Release Notes:

- N/A
2025-03-26 21:18:25 +00:00
Mikayla Maki
9e02fee98d Align project panel and git panel deletion behavior (#27525)
This change makes the git panel and project panel behave the same, on
Linux and macOS, and adds prompts.

Release Notes:

- Changed the git panel to prompt before restoring a file.
2025-03-26 21:15:24 +00:00
loczek
999ad77a59 workspace: Double click empty pane to open new file (#27521)
Release Notes:

- Added ability to double click on empty pane to open a new file
2025-03-26 14:07:54 -07:00
Smit Barmase
780d0eb427 editor: Do not use hide_mouse_while_typing for single line editor (#27536)
Release Notes:

- N/A
2025-03-27 02:32:16 +05:30
Agus Zubiaga
7b40ab30d7 assistant2: Add scrollbar to active thread (#27534)
This required adding scrollbar support to `list`. Since `list` is
virtualized, the scrollbar height will change as more items are
measured. When the user manually drags the scrollbar, we'll persist the
initial height and offset calculations accordingly to prevent the
scrollbar from moving away from the cursor as new items are measured.

We're not doing this yet, but in the future, it'd be nice to budget some
time each frame to layout unmeasured items so that the scrollbar height
is as accurate as possible.

Release Notes:

- N/A
2025-03-26 18:01:13 -03:00
Kirill Bulatov
0a3c8a6790 Remove project strong reference from git panel's log output editor (#27496)
A readonly buffer built from a static `&str` output does not need rich
project-based capabilities, and leaking projects in global git panel
might be dangerous.

Also adds readonly capability to the buffer, as
`editor.set_read_only(true);` API is a separate thing.

Release Notes:

- N/A
2025-03-26 23:01:03 +02:00
Ben Kunkle
1463b4d201 gpui/blade: Allow forcing use of a specific GPU with ZED_DEVICE_ID env var (#27531)
Workaround for users affected by #25899

Thanks to the work done by @kvark in
https://github.com/kvark/blade/pull/210, we have the ability to tell
Vulkan (through blade) a specific GPU to use.

This will hopefully allow some of the users affected by #25899 to use
Zed by allowing them to use a specific GPU, if the primary/default GPU
will not work

Release Notes:

- Added the ability to specify which GPU Zed uses on Linux by setting
the `ZED_DEVICE_ID` environment variable. You can obtain the device ID
of your GPU by running `lspci -nn | grep VGA` which will output each GPU
on one line like:
  ```
08:00.0 VGA compatible controller [0300]: NVIDIA Corporation GA104
[GeForce RTX 3070] [10de:2484] (rev a1)
  ````
where the device ID here is `2484`. This value is in hexadecimal, so to
force Zed to use this specific GPU you would set the environment
variable like so:
  ```
  ZED_DEVICE_ID=0x2484
  ```
Make sure to export the variable if you choose to define it globally in
a `.bashrc` or similar
2025-03-26 20:32:36 +00:00
Smit Barmase
77856bf017 Hide the mouse when the user is typing in the editor - take 2 (#27519)
Closes #4461

Take 2 on https://github.com/zed-industries/zed/pull/25040. 

Fixes panic caused due to using `setHiddenUntilMouseMoves` return type
to `set` cursor on macOS.

Release Notes:

- Now cursor hides when the user is typing in editor. It will stay
hidden until it is moved again. This behavior is `true` by default, and
can be configured with `hide_mouse_while_typing` in settings.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Thomas Mickley-Doyle <thomas@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Angelk90 <angelo.k90@hotmail.it>
2025-03-27 01:58:26 +05:30
Marshall Bowers
848a99c605 assistant2: Rework enabled tool representation (#27527)
This PR reworks how we store enabled tools in the `ToolWorkingSet`.

We now track them based on which tools are explicitly enabled, rather
than by the tools that have been disabled.

Also fixed an issue where switching profiles wouldn't properly set the
right tools.

Release Notes:

- N/A
2025-03-26 20:26:26 +00:00
Peter Tripp
435a36b9f9 html: Improve settings, formatting and user binaries (#27524)
Added support for using `language_server` as HTML formatter.
Added support for finding `vscode-html-language-server` in user's path.

Release Notes:

- N/A
2025-03-26 16:24:37 -04:00
Bennet Bo Fenner
8b3ddcd545 assistant2: Fix \\ appearing for paths in file context picker (#27528)
Closes #ISSUE

Release Notes:

- N/A
2025-03-26 20:22:13 +00:00
Elvis Pranskevichus
13bf179aae python: Show environment name if available (#26741)
Right now the toolchain popup is a nondescript list of duplicate entries
like `Python 3.10.15 (VirtualEnvWrapper)` and one has to look at the
interpreter path to distinguish one virtualenv from another.

Fix this by including the env name as reported by pet, so the entries
looks like `Python 3.10.15 (myproject; VirtualEnvWrapper)`.

Release Notes:

- Python: Improved display of environments in toolchain selector
2025-03-26 21:08:26 +01:00
Marshall Bowers
cdaad2655a assistant2: Add profile selector (#27520)
This PR replaces the tool selector with a new profile selector.

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 42 PM"
src="https://github.com/user-attachments/assets/9631c6e9-9c47-411e-b9fc-5d61ed9ca1fe"
/>

<img width="1394" alt="Screenshot 2025-03-26 at 2 35 50 PM"
src="https://github.com/user-attachments/assets/3abe4e08-d044-4d3f-aa95-f472938452a8"
/>

Release Notes:

- N/A
2025-03-26 18:51:38 +00:00
Michael Sloan
7e4320f587 Fix drawing of 0-width borders when quad has other borders (#27511)
Closes #27485

Release Notes:

- N/A
2025-03-26 11:13:34 -06:00
Agus Zubiaga
130abc8998 assistant2: Encourage diagnostics check (#27510)
Release Notes:

- N/A
2025-03-26 13:42:09 -03:00
Richard Feldman
9db4c8b710 Add Create Directory Tool (#27505)
`mkdir -p` but it works cross-platform and uses project abstractions.

<img width="629" alt="Screenshot 2025-03-26 at 11 02 37 AM"
src="https://github.com/user-attachments/assets/9ef58d53-3343-4c94-a8f3-b82ab942611b"
/>

Release Notes:

- N/A
2025-03-26 11:59:03 -04:00
Marshall Bowers
e67ad1a1b6 extension_host: Rename Extension variants so that the version number components are clearer (#27507)
This PR renames the variants of the `Extension` enum with delimiters
between the version number components so that it's clearer which version
of the extension API they refer to.

Release Notes:

- N/A
2025-03-26 15:54:14 +00:00
Alvaro Parker
82536f5243 Add support for excluding files based on .gitignore (#26636)
Closes: #17543

Release Notes:

- **New Feature:** Introduced the ability to automatically remove files
and directories from the Zed project panel that are specified in
`.gitignore`.
- **Configuration Option:** This behavior can be controlled via the new
`project_panel.hide_gitignore` setting. By setting it to `true`, files
listed in `.gitignore` will be excluded from the project panel.
- **Toggle:** Ability to toggle this setting using the action
`ProjectPanel::ToggleHideGitIgnore`

```json
  "project_panel": {
    "hide_gitignore": true
  },

```

This results in a cleaner and easier to browse project panel for
projects that generate a lot of object files like `xv6-riscv` or `linux`
without needing to tweak `file_scan_exclusions` on `settings.json`

**Preview:**
- With `"project_panel.hide_gitignore": false` (default, this is how zed
currently looks)

![Screenshot From 2025-03-23
12-50-17](https://github.com/user-attachments/assets/15607e73-a474-4188-982a-eed4e0551061)

- With `"project_panel.hide_gitignore": true` 

![Screenshot From 2025-03-23
12-50-27](https://github.com/user-attachments/assets/3e281f92-294c-4133-b5e3-25e17f15bd4d)

- Action `ProjectPanel::ToggleHideGitIgnore`

![Screenshot From 2025-03-23
12-50-55](https://github.com/user-attachments/assets/4d03db33-75ad-471c-814c-098698a8cb38)
2025-03-26 20:57:09 +05:30
Richard Feldman
9eacac62a9 Escape markdown in tools' ui_text (#27502)
Escape markdown in tools' `ui_text`

<img width="628" alt="Screenshot 2025-03-26 at 10 43 23 AM"
src="https://github.com/user-attachments/assets/bb694821-aae7-4ccf-a35a-a3317b0222d5"
/>


Release Notes:

- N/A
2025-03-26 11:27:02 -04:00
Richard Feldman
82b0881dcb Make the "View Panel" focus the assistant panel (#27504)
Release Notes:

- N/A
2025-03-26 11:26:49 -04:00
iyht
0a49ccbebf Allow the keybinding context to detect the terminal vi_mode (#26236)
Release Notes:

- Added support for detecting the vi_mode in the keybinding context. Now
we can define and use the keybinding when the terminal is in vi_mode.


https://github.com/user-attachments/assets/a927b6c9-c634-4739-9502-8457614d9a90
2025-03-26 20:53:23 +05:30
张小白
d232150d67 windows: Fix performance issues after trashing or deleting a folder (#27498)
Closes #25247

Since the upstream `Notify` repo hasn't merged the related PR yet, this
is basically a temporary patch to work around it.

Release Notes:

- N/A
2025-03-26 23:20:09 +08:00
Joseph T. Lyons
9a2dfa687d Bump Zed to v0.181 (#27506)
Release Notes:

-N/A
2025-03-26 11:15:42 -04:00
79 changed files with 1580 additions and 600 deletions

86
Cargo.lock generated
View File

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

View File

@@ -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 }]
}
},
{

View File

@@ -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 }]
}
},
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -544,6 +544,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
fn sort_completions(&self) -> bool {
false
}
fn filter_completions(&self) -> bool {
false
}
}
fn confirm_completion_callback(

View File

@@ -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('/');

View File

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

View 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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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}"))
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}`")
}
_ => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ impl Render for WindowShadow {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6702,7 +6702,7 @@ pub fn client_side_decorations(
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

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

View File

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

View File

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

View File

@@ -16,4 +16,4 @@ language = "HTML"
[grammars.html]
repository = "https://github.com/tree-sitter/tree-sitter-html"
commit = "bfa075d83c6b97cd48440b3829ab8d24a2319809"
commit = "5a5ca8551a179998360b4a4ca2c0f366a35acc03"

View File

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