diff --git a/.github/ISSUE_TEMPLATE/0_feature_request.yml b/.github/ISSUE_TEMPLATE/0_feature_request.yml deleted file mode 100644 index 0bf3a61757cab5..00000000000000 --- a/.github/ISSUE_TEMPLATE/0_feature_request.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Feature Request -description: "Tip: open this issue template from within Zed with the `request feature` command palette action" -type: "Feature" -body: - - type: textarea - attributes: - label: Describe the feature - description: A one line summary, and description of what you want to happen. - value: | - Summary: - - Description: - - Screenshots: - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9a51874331a4cc..991f2ce91a6553 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,9 @@ # yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json blank_issues_enabled: false contact_links: + - name: Feature Request + url: https://github.com/zed-industries/zed/discussions/new/choose + about: To request a feature, open a new Discussion in one of the appropriate Discussion categories - name: Zed Discussion Forum url: https://github.com/zed-industries/zed/discussions about: A community discussion forum diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c45a71ff7e7fdf..7c3e8b66c08804 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -408,6 +408,7 @@ "(": "vim::Parentheses", ")": "vim::Parentheses", "b": "vim::Parentheses", + // "b": "vim::AnyBrackets", "[": "vim::SquareBrackets", "]": "vim::SquareBrackets", "r": "vim::SquareBrackets", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 13f9d6f2318656..fe2ae0be49ed07 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -397,4 +397,4 @@ gpui::actions!( action_as!(go_to_line, ToggleGoToLine as Toggle); action_with_deprecated_aliases!(editor, OpenSelectedFilename, ["editor::OpenFile"]); -action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleDiffHunk"]); +action_with_deprecated_aliases!(editor, ToggleSelectedDiffHunks, ["editor::ToggleHunkDiff"]); diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 295114c0cb4b5d..ddac6f446ee159 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -22,7 +22,7 @@ const fn zed_repo_url() -> &'static str { } fn request_feature_url() -> String { - "https://github.com/zed-industries/zed/issues/new?template=0_feature_request.yml".to_string() + "https://github.com/zed-industries/zed/discussions/new/choose".to_string() } fn file_bug_report_url(specs: &SystemSpecs) -> String { diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 15b5babf28f6e5..101695508f0ce9 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -3,7 +3,6 @@ use fs::Fs; use futures::{channel::mpsc, StreamExt}; use gpui::{App, BackgroundExecutor, ReadGlobal, UpdateGlobal}; use std::{path::PathBuf, sync::Arc, time::Duration}; -use util::ResultExt; pub const EMPTY_THEME_NAME: &str = "empty-theme"; @@ -73,9 +72,11 @@ pub fn handle_settings_file_changes( .block(user_settings_file_rx.next()) .unwrap(); SettingsStore::update_global(cx, |store, cx| { - store - .set_user_settings(&user_settings_content, cx) - .log_err(); + let result = store.set_user_settings(&user_settings_content, cx); + if let Err(err) = &result { + log::error!("Failed to load user settings: {err}"); + } + settings_changed(result.err(), cx); }); cx.spawn(move |cx| async move { while let Some(user_settings_content) = user_settings_file_rx.next().await { diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index bb04117742f829..d59d59a26ea5c0 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -29,6 +29,7 @@ pub enum Object { AnyQuotes, DoubleQuotes, VerticalBars, + AnyBrackets, Parentheses, SquareBrackets, CurlyBrackets, @@ -74,6 +75,7 @@ actions!( DoubleQuotes, VerticalBars, Parentheses, + AnyBrackets, SquareBrackets, CurlyBrackets, AngleBrackets, @@ -115,6 +117,9 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| { vim.object(Object::AnyQuotes, window, cx) }); + Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| { + vim.object(Object::AnyBrackets, window, cx) + }); Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| { vim.object(Object::DoubleQuotes, window, cx) }); @@ -186,6 +191,7 @@ impl Object { | Object::DoubleQuotes => false, Object::Sentence | Object::Paragraph + | Object::AnyBrackets | Object::Parentheses | Object::Tag | Object::AngleBrackets @@ -212,6 +218,7 @@ impl Object { | Object::AnyQuotes | Object::DoubleQuotes | Object::VerticalBars + | Object::AnyBrackets | Object::Parentheses | Object::SquareBrackets | Object::Tag @@ -239,6 +246,7 @@ impl Object { } } Object::Parentheses + | Object::AnyBrackets | Object::SquareBrackets | Object::CurlyBrackets | Object::AngleBrackets @@ -306,16 +314,7 @@ impl Object { quote, ) }) - .min_by_key(|range| { - // Calculate proximity of ranges to the cursor - let start_distance = (relative_offset - - range.start.to_offset(map, Bias::Left) as isize) - .abs(); - let end_distance = (relative_offset - - range.end.to_offset(map, Bias::Right) as isize) - .abs(); - start_distance + end_distance - }) + .min_by_key(|range| calculate_range_distance(range, relative_offset, map)) } Object::DoubleQuotes => { surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') @@ -331,6 +330,24 @@ impl Object { let range = selection.range(); surrounding_html_tag(map, head, range, around) } + Object::AnyBrackets => { + let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; + let relative_offset = relative_to.to_offset(map, Bias::Left) as isize; + + bracket_pairs + .iter() + .flat_map(|&(open_bracket, close_bracket)| { + surrounding_markers( + map, + relative_to, + around, + self.is_multiline(), + open_bracket, + close_bracket, + ) + }) + .min_by_key(|range| calculate_range_distance(range, relative_offset, map)) + } Object::SquareBrackets => { surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') } @@ -584,6 +601,37 @@ fn around_word( } } +/// Calculate distance between a range and a cursor position +/// +/// Returns a score where: +/// - Lower values indicate better matches +/// - Range containing cursor gets priority (returns range length) +/// - For non-containing ranges, uses minimum distance to boundaries as primary factor +/// - Range length is used as secondary factor for tiebreaking +fn calculate_range_distance( + range: &Range, + cursor_offset: isize, + map: &DisplaySnapshot, +) -> isize { + let start_offset = range.start.to_offset(map, Bias::Left) as isize; + let end_offset = range.end.to_offset(map, Bias::Right) as isize; + let range_length = end_offset - start_offset; + + // If cursor is inside the range, return range length + if cursor_offset >= start_offset && cursor_offset <= end_offset { + return range_length; + } + + // Calculate minimum distance to range boundaries + let start_distance = (cursor_offset - start_offset).abs(); + let end_distance = (cursor_offset - end_offset).abs(); + let min_distance = start_distance.min(end_distance); + + // Use min_distance as primary factor, range_length as secondary + // Multiply by large number to ensure distance is primary factor + min_distance * 10000 + range_length +} + fn around_subword( map: &DisplaySnapshot, relative_to: DisplayPoint, @@ -1302,9 +1350,11 @@ fn surrounding_markers( #[cfg(test)] mod test { + use gpui::KeyBinding; use indoc::indoc; use crate::{ + object::AnyBrackets, state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; @@ -1914,6 +1964,30 @@ mod test { const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ // Single quotes + ( + "c i q", + "Thisˇ is a 'quote' example.", + "This is a 'ˇ' example.", + Mode::Insert, + ), + ( + "c a q", + "Thisˇ is a 'quote' example.", + "This is a ˇexample.", + Mode::Insert, + ), + ( + "c i q", + "This is a \"simple 'qˇuote'\" example.", + "This is a \"simple 'ˇ'\" example.", + Mode::Insert, + ), + ( + "c a q", + "This is a \"simple 'qˇuote'\" example.", + "This is a \"simpleˇ\" example.", + Mode::Insert, + ), ( "c i q", "This is a 'qˇuote' example.", @@ -2022,6 +2096,151 @@ mod test { } } + #[gpui::test] + async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.update(|_, cx| { + cx.bind_keys([KeyBinding::new( + "b", + AnyBrackets, + Some("vim_operator == a || vim_operator == i || vim_operator == cs"), + )]); + }); + + const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ + // Bracket (Parentheses) + ( + "c i b", + "Thisˇ is a (simple [quote]) example.", + "This is a (ˇ) example.", + Mode::Insert, + ), + ( + "c i b", + "This is a [simple (qˇuote)] example.", + "This is a [simple (ˇ)] example.", + Mode::Insert, + ), + ( + "c a b", + "This is a [simple (qˇuote)] example.", + "This is a [simple ˇ] example.", + Mode::Insert, + ), + ( + "c a b", + "Thisˇ is a (simple [quote]) example.", + "This is a ˇ example.", + Mode::Insert, + ), + ( + "c i b", + "This is a (qˇuote) example.", + "This is a (ˇ) example.", + Mode::Insert, + ), + ( + "c a b", + "This is a (qˇuote) example.", + "This is a ˇ example.", + Mode::Insert, + ), + ( + "d i b", + "This is a (qˇuote) example.", + "This is a (ˇ) example.", + Mode::Normal, + ), + ( + "d a b", + "This is a (qˇuote) example.", + "This is a ˇ example.", + Mode::Normal, + ), + // Square brackets + ( + "c i b", + "This is a [qˇuote] example.", + "This is a [ˇ] example.", + Mode::Insert, + ), + ( + "c a b", + "This is a [qˇuote] example.", + "This is a ˇ example.", + Mode::Insert, + ), + ( + "d i b", + "This is a [qˇuote] example.", + "This is a [ˇ] example.", + Mode::Normal, + ), + ( + "d a b", + "This is a [qˇuote] example.", + "This is a ˇ example.", + Mode::Normal, + ), + // Curly brackets + ( + "c i b", + "This is a {qˇuote} example.", + "This is a {ˇ} example.", + Mode::Insert, + ), + ( + "c a b", + "This is a {qˇuote} example.", + "This is a ˇ example.", + Mode::Insert, + ), + ( + "d i b", + "This is a {qˇuote} example.", + "This is a {ˇ} example.", + Mode::Normal, + ), + ( + "d a b", + "This is a {qˇuote} example.", + "This is a ˇ example.", + Mode::Normal, + ), + ]; + + for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { + cx.set_state(initial_state, Mode::Normal); + + cx.simulate_keystrokes(keystrokes); + + cx.assert_state(expected_state, *expected_mode); + } + + const INVALID_CASES: &[(&str, &str, Mode)] = &[ + ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket + ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket + ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket + ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket + ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket + ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket + ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket + ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket + ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket + ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket + ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket + ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket + ]; + + for (keystrokes, initial_state, mode) in INVALID_CASES { + cx.set_state(initial_state, Mode::Normal); + + cx.simulate_keystrokes(keystrokes); + + cx.assert_state(initial_state, *mode); + } + } + #[gpui::test] async fn test_tags(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new_html(cx).await; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 32c11b2d22e5b9..ddd83f4666b759 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -81,6 +81,7 @@ pub enum Operator { first_char: Option, }, AddSurrounds { + // Typically no need to configure this as `SendKeystrokes` can be used - see #23088. #[serde(skip)] target: Option, }, diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 72275f8b54c2b7..e805527b91f358 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -3,17 +3,12 @@ use gpui::{ svg, AnyView, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, Global, PromptLevel, Render, ScrollHandle, Task, }; -use std::rc::Rc; +use parking_lot::Mutex; +use std::sync::{Arc, LazyLock}; use std::{any::TypeId, time::Duration}; use ui::{prelude::*, Tooltip}; use util::ResultExt; -pub fn init(cx: &mut App) { - cx.set_global(GlobalAppNotifications { - app_notifications: Vec::new(), - }) -} - #[derive(Debug, PartialEq, Clone)] pub enum NotificationId { Unique(TypeId), @@ -162,7 +157,7 @@ impl Workspace { pub fn show_initial_notifications(&mut self, cx: &mut Context) { // Allow absence of the global so that tests don't need to initialize it. let app_notifications = cx - .try_global::() + .try_global::() .iter() .flat_map(|global| global.app_notifications.iter().cloned()) .collect::>(); @@ -500,21 +495,27 @@ pub mod simple_message_notification { } } +static GLOBAL_APP_NOTIFICATIONS: LazyLock> = LazyLock::new(|| { + Mutex::new(AppNotifications { + app_notifications: Vec::new(), + }) +}); + /// Stores app notifications so that they can be shown in new workspaces. -struct GlobalAppNotifications { +struct AppNotifications { app_notifications: Vec<( NotificationId, - Rc) -> AnyView>, + Arc) -> AnyView + Send + Sync>, )>, } -impl Global for GlobalAppNotifications {} +impl Global for AppNotifications {} -impl GlobalAppNotifications { +impl AppNotifications { pub fn insert( &mut self, id: NotificationId, - build_notification: Rc) -> AnyView>, + build_notification: Arc) -> AnyView + Send + Sync>, ) { self.remove(&id); self.app_notifications.push((id, build_notification)) @@ -532,28 +533,30 @@ impl GlobalAppNotifications { pub fn show_app_notification( id: NotificationId, cx: &mut App, - build_notification: impl Fn(&mut Context) -> Entity + 'static, + build_notification: impl Fn(&mut Context) -> Entity + 'static + Send + Sync, ) { // Defer notification creation so that windows on the stack can be returned to GPUI cx.defer(move |cx| { // Handle dismiss events by removing the notification from all workspaces. - let build_notification: Rc) -> AnyView> = Rc::new({ - let id = id.clone(); - move |cx| { - let notification = build_notification(cx); - cx.subscribe(¬ification, { - let id = id.clone(); - move |_, _, _: &DismissEvent, cx| { - dismiss_app_notification(&id, cx); - } - }) - .detach(); - notification.into() - } - }); + let build_notification: Arc) -> AnyView + Send + Sync> = + Arc::new({ + let id = id.clone(); + move |cx| { + let notification = build_notification(cx); + cx.subscribe(¬ification, { + let id = id.clone(); + move |_, _, _: &DismissEvent, cx| { + dismiss_app_notification(&id, cx); + } + }) + .detach(); + notification.into() + } + }); // Store the notification so that new workspaces also receive it. - cx.global_mut::() + GLOBAL_APP_NOTIFICATIONS + .lock() .insert(id.clone(), build_notification.clone()); for window in cx.windows() { @@ -576,7 +579,7 @@ pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) { let id = id.clone(); // Defer notification dismissal so that windows on the stack can be returned to GPUI cx.defer(move |cx| { - cx.global_mut::().remove(&id); + GLOBAL_APP_NOTIFICATIONS.lock().remove(&id); for window in cx.windows() { if let Some(workspace_window) = window.downcast::() { let id = id.clone(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b901140d989012..bedadc41bdd4ce 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -365,7 +365,6 @@ fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, c pub fn init(app_state: Arc, cx: &mut App) { init_settings(cx); - notifications::init(cx); theme_preview::init(cx); cx.on_action(Workspace::close_global); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 93ad29b236fdb2..8fc4cef8de8d20 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -18,7 +18,7 @@ use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::{future, StreamExt}; use git::GitHostingProviderRegistry; -use gpui::{Action, App, AppContext as _, Application, AsyncApp, DismissEvent, UpdateGlobal as _}; +use gpui::{App, AppContext as _, Application, AsyncApp, UpdateGlobal as _}; use http_client::{read_proxy_from_env, Uri}; use language::LanguageRegistry; @@ -33,9 +33,7 @@ use project::project_settings::ProjectSettings; use recent_projects::{open_ssh_project, SshSettings}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; -use settings::{ - handle_settings_file_changes, watch_config_file, InvalidSettingsError, Settings, SettingsStore, -}; +use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore}; use simplelog::ConfigBuilder; use std::{ env, @@ -50,18 +48,13 @@ use time::UtcOffset; use util::{maybe, ResultExt, TryFutureExt}; use uuid::Uuid; use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; -use workspace::{ - notifications::{simple_message_notification::MessageNotification, NotificationId}, - AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore, -}; +use workspace::{AppState, SerializedWorkspaceLocation, WorkspaceSettings, WorkspaceStore}; use zed::{ app_menus, build_window_options, derive_paths_with_position, handle_cli_connection, - handle_keymap_file_changes, initialize_workspace, open_paths_with_positions, OpenListener, - OpenRequest, + handle_keymap_file_changes, handle_settings_changed, initialize_workspace, + inline_completion_registry, open_paths_with_positions, OpenListener, OpenRequest, }; -use crate::zed::inline_completion_registry; - #[cfg(unix)] use util::{load_login_shell_environment, load_shell_from_passwd}; @@ -614,44 +607,6 @@ fn main() { }); } -fn handle_settings_changed(error: Option, cx: &mut App) { - struct SettingsParseErrorNotification; - let id = NotificationId::unique::(); - - for workspace in workspace::local_workspace_windows(cx) { - workspace - .update(cx, |workspace, _, cx| { - match error.as_ref() { - Some(error) => { - if let Some(InvalidSettingsError::LocalSettings { .. }) = - error.downcast_ref::() - { - // Local settings will be displayed by the projects - } else { - workspace.show_notification(id.clone(), cx, |cx| { - cx.new(|_cx| { - MessageNotification::new(format!( - "Invalid user settings file\n{error}" - )) - .with_click_message("Open settings file") - .on_click(|window, cx| { - window.dispatch_action( - zed_actions::OpenSettings.boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - }) - }) - }); - } - } - None => workspace.dismiss_notification(&id, cx), - } - }) - .log_err(); - } -} - fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut App) { if let Some(connection) = request.cli_connection { let app_state = app_state.clone(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 408ca87a22ab45..422695aa4db6a9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -39,12 +39,12 @@ use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - initial_project_settings_content, initial_tasks_content, update_settings_file, KeymapFile, - KeymapFileLoadResult, Settings, SettingsStore, DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH, + initial_project_settings_content, initial_tasks_content, update_settings_file, + InvalidSettingsError, KeymapFile, KeymapFileLoadResult, Settings, SettingsStore, + DEFAULT_KEYMAP_PATH, VIM_KEYMAP_PATH, }; use std::any::TypeId; use std::path::PathBuf; -use std::rc::Rc; use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc}; use terminal_view::terminal_panel::{self, TerminalPanel}; use theme::{ActiveTheme, ThemeSettings}; @@ -1220,7 +1220,7 @@ fn show_keymap_file_load_error( }); cx.spawn(move |cx| async move { - let parsed_markdown = Rc::new(parsed_markdown.await); + let parsed_markdown = Arc::new(parsed_markdown.await); cx.update(|cx| { show_app_notification(notification_id, cx, move |cx| { let workspace_handle = cx.entity().downgrade(); @@ -1274,6 +1274,33 @@ pub fn load_default_keymap(cx: &mut App) { } } +pub fn handle_settings_changed(error: Option, cx: &mut App) { + struct SettingsParseErrorNotification; + let id = NotificationId::unique::(); + + match error { + Some(error) => { + if let Some(InvalidSettingsError::LocalSettings { .. }) = + error.downcast_ref::() + { + // Local settings errors are displayed by the projects + return; + } + show_app_notification(id, cx, move |cx| { + cx.new(|_cx| { + MessageNotification::new(format!("Invalid user settings file\n{error}")) + .with_click_message("Open settings file") + .on_click(|window, cx| { + window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); + cx.emit(DismissEvent); + }) + }) + }); + } + None => dismiss_app_notification(&id, cx), + } +} + pub fn open_new_ssh_project_from_project( workspace: &mut Workspace, paths: Vec,