Detailed changes
@@ -2759,6 +2759,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+[[package]]
+name = "human_bytes"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
+
[[package]]
name = "humantime"
version = "2.1.0"
@@ -3757,6 +3763,15 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "ntapi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
+dependencies = [
+ "winapi 0.3.9",
+]
+
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -4426,7 +4441,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
dependencies = [
"libc",
"log",
- "ntapi",
+ "ntapi 0.3.7",
"winapi 0.3.9",
]
@@ -6222,6 +6237,21 @@ dependencies = [
"libc",
]
+[[package]]
+name = "sysinfo"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
+dependencies = [
+ "cfg-if 1.0.0",
+ "core-foundation-sys",
+ "libc",
+ "ntapi 0.4.0",
+ "once_cell",
+ "rayon",
+ "winapi 0.3.9",
+]
+
[[package]]
name = "system-interface"
version = "0.20.0"
@@ -7204,6 +7234,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "urlencoding"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
+
[[package]]
name = "usvg"
version = "0.14.1"
@@ -8151,7 +8187,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
[[package]]
name = "zed"
-version = "0.68.0"
+version = "0.69.0"
dependencies = [
"activity_indicator",
"anyhow",
@@ -8183,6 +8219,7 @@ dependencies = [
"fuzzy",
"go_to_line",
"gpui",
+ "human_bytes",
"ignore",
"image",
"indexmap",
@@ -8216,6 +8253,7 @@ dependencies = [
"smallvec",
"smol",
"sum_tree",
+ "sysinfo",
"tempdir",
"terminal_view",
"text",
@@ -8244,6 +8282,7 @@ dependencies = [
"tree-sitter-typescript",
"unindent",
"url",
+ "urlencoding",
"util",
"vim",
"workspace",
@@ -20,8 +20,10 @@
"alt-cmd-left": "pane::ActivatePrevItem",
"alt-cmd-right": "pane::ActivateNextItem",
"cmd-w": "pane::CloseActiveItem",
- "cmd-shift-w": "workspace::CloseWindow",
"alt-cmd-t": "pane::CloseInactiveItems",
+ "cmd-k u": "pane::CloseCleanItems",
+ "cmd-k cmd-w": "pane::CloseAllItems",
+ "cmd-shift-w": "workspace::CloseWindow",
"cmd-s": "workspace::Save",
"cmd-shift-s": "workspace::SaveAs",
"cmd-=": "zed::IncreaseBufferFontSize",
@@ -67,9 +69,11 @@
"up": "editor::MoveUp",
"pageup": "editor::PageUp",
"shift-pageup": "editor::MovePageUp",
+ "home": "editor::MoveToBeginningOfLine",
"down": "editor::MoveDown",
"pagedown": "editor::PageDown",
"shift-pagedown": "editor::MovePageDown",
+ "end": "editor::MoveToEndOfLine",
"left": "editor::MoveLeft",
"right": "editor::MoveRight",
"ctrl-p": "editor::MoveUp",
@@ -110,6 +114,12 @@
"stop_at_soft_wraps": true
}
],
+ "shift-home": [
+ "editor::SelectToBeginningOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
"ctrl-shift-a": [
"editor::SelectToBeginningOfLine",
{
@@ -122,6 +132,12 @@
"stop_at_soft_wraps": true
}
],
+ "shift-end": [
+ "editor::SelectToEndOfLine",
+ {
+ "stop_at_soft_wraps": true
+ }
+ ],
"ctrl-shift-e": [
"editor::SelectToEndOfLine",
{
@@ -1,6 +1,6 @@
[
{
- "context": "Editor && VimControl",
+ "context": "Editor && VimControl && !VimWaiting",
"bindings": {
"g": [
"vim::PushOperator",
@@ -53,6 +53,42 @@
}
],
"%": "vim::Matching",
+ "ctrl-y": [
+ "vim::Scroll",
+ "LineUp"
+ ],
+ "f": [
+ "vim::PushOperator",
+ {
+ "FindForward": {
+ "before": false
+ }
+ }
+ ],
+ "t": [
+ "vim::PushOperator",
+ {
+ "FindForward": {
+ "before": true
+ }
+ }
+ ],
+ "shift-f": [
+ "vim::PushOperator",
+ {
+ "FindBackward": {
+ "after": false
+ }
+ }
+ ],
+ "shift-t": [
+ "vim::PushOperator",
+ {
+ "FindBackward": {
+ "after": true
+ }
+ }
+ ],
"escape": "editor::Cancel",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
"1": [
@@ -94,7 +130,7 @@
}
},
{
- "context": "Editor && vim_mode == normal && vim_operator == none",
+ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
"bindings": {
"c": [
"vim::PushOperator",
@@ -173,10 +209,6 @@
"ctrl-e": [
"vim::Scroll",
"LineDown"
- ],
- "ctrl-y": [
- "vim::Scroll",
- "LineUp"
]
}
},
@@ -255,7 +287,7 @@
}
},
{
- "context": "Editor && vim_mode == visual",
+ "context": "Editor && vim_mode == visual && !VimWaiting",
"bindings": {
"u": "editor::Undo",
"c": "vim::VisualChange",
@@ -271,5 +303,11 @@
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore"
}
+ },
+ {
+ "context": "Editor && VimWaiting",
+ "bindings": {
+ "*": "gpui::KeyPressed"
+ }
}
]
@@ -221,7 +221,7 @@
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
- // "rust_analyzer": {
+ // "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {
// "checkOnSave": {
@@ -1131,6 +1131,7 @@ async fn test_unshare_project(
.unwrap();
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
project_b
@@ -1160,6 +1161,7 @@ async fn test_unshare_project(
.await
.unwrap();
let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
+ deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
project_c2
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
@@ -1213,6 +1215,7 @@ async fn test_host_disconnect(
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ deterministic.run_until_parked();
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
let (_, workspace_b) = cx_b.add_window(|cx| {
@@ -1467,7 +1470,7 @@ async fn test_project_reconnect(
.read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
.await;
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
- assert!(tree.as_local().unwrap().is_shared());
+ assert!(!tree.as_local().unwrap().is_shared());
tree.id()
});
deterministic.run_until_parked();
@@ -1489,6 +1492,7 @@ async fn test_project_reconnect(
deterministic.run_until_parked();
project_a1.read_with(cx_a, |project, cx| {
assert!(project.is_shared());
+ assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
assert_eq!(
worktree_a1
.read(cx)
@@ -1510,6 +1514,7 @@ async fn test_project_reconnect(
"subdir2/i.txt"
]
);
+ assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
assert_eq!(
worktree_a3
.read(cx)
@@ -15,7 +15,10 @@ use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf1
use lsp::FakeLanguageServer;
use parking_lot::Mutex;
use project::{search::SearchQuery, Project, ProjectPath};
-use rand::prelude::*;
+use rand::{
+ distributions::{Alphanumeric, DistString},
+ prelude::*,
+};
use serde::{Deserialize, Serialize};
use std::{
env,
@@ -293,9 +296,15 @@ async fn test_random_collaboration(
);
}
(None, None) => {}
- (None, _) => panic!("host's file is None, guest's isn't "),
- (_, None) => panic!("guest's file is None, hosts's isn't "),
+ (None, _) => panic!("host's file is None, guest's isn't"),
+ (_, None) => panic!("guest's file is None, hosts's isn't"),
}
+
+ let host_diff_base =
+ host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
+ let guest_diff_base = guest_buffer
+ .read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
+ assert_eq!(guest_diff_base, host_diff_base);
}
}
}
@@ -918,6 +927,37 @@ async fn apply_client_operation(
.unwrap();
}
}
+
+ ClientOperation::WriteGitIndex {
+ repo_path,
+ contents,
+ } => {
+ if !client
+ .fs
+ .metadata(&repo_path)
+ .await?
+ .map_or(false, |m| m.is_dir)
+ {
+ return Ok(false);
+ }
+
+ log::info!(
+ "{}: writing git index for repo {:?}: {:?}",
+ client.username,
+ repo_path,
+ contents
+ );
+
+ let dot_git_dir = repo_path.join(".git");
+ let contents = contents
+ .iter()
+ .map(|(path, contents)| (path.as_path(), contents.clone()))
+ .collect::<Vec<_>>();
+ if client.fs.metadata(&dot_git_dir).await?.is_none() {
+ client.fs.create_dir(&dot_git_dir).await?;
+ }
+ client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
+ }
}
Ok(true)
}
@@ -1038,6 +1078,10 @@ enum ClientOperation {
path: PathBuf,
is_dir: bool,
},
+ WriteGitIndex {
+ repo_path: PathBuf,
+ contents: Vec<(PathBuf, String)>,
+ },
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -1221,6 +1265,7 @@ impl TestPlan {
return None;
}
+ let executor = cx.background();
self.operation_ix += 1;
let call = cx.read(ActiveCall::global);
Some(loop {
@@ -1337,7 +1382,7 @@ impl TestPlan {
.choose(&mut self.rng)
.cloned() else { continue };
let project_root_name = root_name_for_project(&project, cx);
- let mut paths = cx.background().block(client.fs.paths());
+ let mut paths = executor.block(client.fs.paths());
paths.remove(0);
let new_root_path = if paths.is_empty() || self.rng.gen() {
Path::new("/").join(&self.next_root_dir_name(user_id))
@@ -1385,7 +1430,7 @@ impl TestPlan {
},
// Query and mutate buffers
- 60..=95 => {
+ 60..=90 => {
let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
let project_root_name = root_name_for_project(&project, cx);
let is_local = project.read_with(cx, |project, _| project.is_local());
@@ -1505,6 +1550,39 @@ impl TestPlan {
}
}
+ // Update a git index
+ 91..=95 => {
+ let repo_path = executor
+ .block(client.fs.directories())
+ .choose(&mut self.rng)
+ .unwrap()
+ .clone();
+
+ let mut file_paths = executor
+ .block(client.fs.files())
+ .into_iter()
+ .filter(|path| path.starts_with(&repo_path))
+ .collect::<Vec<_>>();
+ let count = self.rng.gen_range(0..=file_paths.len());
+ file_paths.shuffle(&mut self.rng);
+ file_paths.truncate(count);
+
+ let mut contents = Vec::new();
+ for abs_child_file_path in &file_paths {
+ let child_file_path = abs_child_file_path
+ .strip_prefix(&repo_path)
+ .unwrap()
+ .to_path_buf();
+ let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
+ contents.push((child_file_path, new_base));
+ }
+
+ break ClientOperation::WriteGitIndex {
+ repo_path,
+ contents,
+ };
+ }
+
// Create a file or directory
96.. => {
let is_dir = self.rng.gen::<bool>();
@@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
elements::*,
geometry::{rect::RectF, vector::vec2f},
- impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
- MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
+ impl_actions, impl_internal_actions,
+ keymap_matcher::KeymapContext,
+ AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
+ Subscription, View, ViewContext, ViewHandle,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::Project;
@@ -1267,7 +1269,7 @@ impl View for ContactList {
"ContactList"
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
@@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
actions,
elements::{ChildView, Flex, Label, ParentElement},
- keymap::Keystroke,
+ keymap_matcher::Keystroke,
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle,
};
@@ -64,8 +64,10 @@ impl CommandPalette {
name: humanize_action_name(name),
action,
keystrokes: bindings
+ .iter()
+ .filter_map(|binding| binding.keystrokes())
.last()
- .map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
+ .map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
})
})
.collect();
@@ -1,7 +1,7 @@
use gpui::{
- elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
- Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
- SizeConstraint, Subscription, View, ViewContext,
+ elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
+ platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
+ MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
};
use menu::*;
use settings::Settings;
@@ -75,7 +75,7 @@ impl View for ContextMenu {
"ContextMenu"
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
@@ -36,6 +36,7 @@ use gpui::{
fonts::{self, HighlightStyle, TextStyle},
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
+ keymap_matcher::KeymapContext,
platform::CursorStyle,
serde_json::json,
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
@@ -464,7 +465,7 @@ pub struct Editor {
searchable: bool,
cursor_shape: CursorShape,
workspace_id: Option<WorkspaceId>,
- keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
+ keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
leader_replica_id: Option<u16>,
remote_id: Option<ViewId>,
@@ -827,6 +828,23 @@ impl CompletionsMenu {
})
.collect()
};
+
+ //Remove all candidates where the query's start does not match the start of any word in the candidate
+ if let Some(query) = query {
+ if let Some(query_start) = query.chars().next() {
+ matches.retain(|string_match| {
+ split_words(&string_match.string).any(|word| {
+ //Check that the first codepoint of the word as lowercase matches the first
+ //codepoint of the query as lowercase
+ word.chars()
+ .flat_map(|codepoint| codepoint.to_lowercase())
+ .zip(query_start.to_lowercase())
+ .all(|(word_cp, query_cp)| word_cp == query_cp)
+ })
+ });
+ }
+ }
+
matches.sort_unstable_by_key(|mat| {
let completion = &self.completions[mat.candidate_id];
(
@@ -1225,7 +1243,7 @@ impl Editor {
}
}
- pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
+ pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
self.keymap_context_layers
.insert(TypeId::of::<Tag>(), context);
}
@@ -3611,7 +3629,9 @@ impl Editor {
}
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
+ dbg!("undo");
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
+ dbg!(tx_id);
if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
self.change_selections(None, cx, |s| {
s.select_anchors(selections.to_vec());
@@ -6245,7 +6265,7 @@ impl View for Editor {
false
}
- fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut context = Self::default_keymap_context();
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
@@ -6799,6 +6819,34 @@ pub fn styled_runs_for_code_label<'a>(
})
}
+pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
+ let mut index = 0;
+ let mut codepoints = text.char_indices().peekable();
+
+ std::iter::from_fn(move || {
+ let start_index = index;
+ while let Some((new_index, codepoint)) = codepoints.next() {
+ index = new_index + codepoint.len_utf8();
+ let current_upper = codepoint.is_uppercase();
+ let next_upper = codepoints
+ .peek()
+ .map(|(_, c)| c.is_uppercase())
+ .unwrap_or(false);
+
+ if !current_upper && next_upper {
+ return Some(&text[start_index..index]);
+ }
+ }
+
+ index = text.len();
+ if start_index < text.len() {
+ return Some(&text[start_index..]);
+ }
+ None
+ })
+ .flat_map(|word| word.split_inclusive('_'))
+}
+
trait RangeExt<T> {
fn sorted(&self) -> Range<T>;
fn to_inclusive(&self) -> RangeInclusive<T>;
@@ -29,7 +29,11 @@ use workspace::{
#[gpui::test]
fn test_edit_events(cx: &mut MutableAppContext) {
cx.set_global(Settings::test(cx));
- let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = language::Buffer::new(0, "123456", cx);
+ buffer.set_group_interval(Duration::from_secs(1));
+ buffer
+ });
let events = Rc::new(RefCell::new(Vec::new()));
let (_, editor1) = cx.add_window(Default::default(), {
@@ -3502,6 +3506,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
]
);
+ view.undo(&Undo, cx);
+ view.undo(&Undo, cx);
view.undo(&Undo, cx);
assert_eq!(
view.text(cx),
@@ -5439,6 +5445,20 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
);
}
+#[test]
+fn test_split_words() {
+ fn split<'a>(text: &'a str) -> Vec<&'a str> {
+ split_words(text).collect()
+ }
+
+ assert_eq!(split("HelloWorld"), &["Hello", "World"]);
+ assert_eq!(split("hello_world"), &["hello_", "world"]);
+ assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
+ assert_eq!(split("Hello_World"), &["Hello_", "World"]);
+ assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
+ assert_eq!(split("helloworld"), &["helloworld"]);
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -9,7 +9,9 @@ use indoc::indoc;
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
-use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
+use gpui::{
+ keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
+};
use language::{Buffer, BufferSnapshot};
use settings::Settings;
use util::{
@@ -35,7 +35,7 @@ use repository::FakeGitRepositoryState;
use std::sync::Weak;
lazy_static! {
- static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
+ static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
}
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -80,13 +80,13 @@ impl LineEnding {
}
pub fn normalize(text: &mut String) {
- if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
+ if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
*text = replaced;
}
}
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
- if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
+ if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
replaced.into()
} else {
text
@@ -52,7 +52,7 @@ fn compile_metal_shaders() {
println!("cargo:rerun-if-changed={}", shader_path);
let output = Command::new("xcrun")
- .args(&[
+ .args([
"-sdk",
"macosx",
"metal",
@@ -76,7 +76,7 @@ fn compile_metal_shaders() {
}
let output = Command::new("xcrun")
- .args(&["-sdk", "macosx", "metallib"])
+ .args(["-sdk", "macosx", "metallib"])
.arg(air_output_path)
.arg("-o")
.arg(metallib_output_path)
@@ -26,9 +26,8 @@ use smallvec::SmallVec;
use smol::prelude::*;
pub use action::*;
-use callback_collection::{CallbackCollection, Mapping};
-use collections::{btree_map, hash_map::Entry, BTreeMap, HashMap, HashSet, VecDeque};
-use keymap::MatchResult;
+use callback_collection::CallbackCollection;
+use collections::{hash_map::Entry, HashMap, HashSet, VecDeque};
use platform::Event;
#[cfg(any(test, feature = "test-support"))]
pub use test_app_context::{ContextHandle, TestAppContext};
@@ -37,7 +36,7 @@ use crate::{
elements::ElementBox,
executor::{self, Task},
geometry::rect::RectF,
- keymap::{self, Binding, Keystroke},
+ keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
platform::{self, KeyDownEvent, Platform, PromptLevel, WindowOptions},
presenter::Presenter,
util::post_inc,
@@ -72,11 +71,11 @@ pub trait View: Entity + Sized {
false
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> keymap_matcher::KeymapContext {
Self::default_keymap_context()
}
- fn default_keymap_context() -> keymap::Context {
- let mut cx = keymap::Context::default();
+ fn default_keymap_context() -> keymap_matcher::KeymapContext {
+ let mut cx = keymap_matcher::KeymapContext::default();
cx.set.insert(Self::ui_name().into());
cx
}
@@ -588,9 +587,9 @@ type GlobalActionCallback = dyn FnMut(&dyn Action, &mut MutableAppContext);
type SubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext) -> bool>;
type GlobalSubscriptionCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ObservationCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
-type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type GlobalObservationCallback = Box<dyn FnMut(&mut MutableAppContext)>;
-type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContext)>;
+type FocusObservationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext)>;
type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
@@ -609,24 +608,23 @@ pub struct MutableAppContext {
capture_actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
actions: HashMap<TypeId, HashMap<TypeId, Vec<Box<ActionCallback>>>>,
global_actions: HashMap<TypeId, Box<GlobalActionCallback>>,
- keystroke_matcher: keymap::Matcher,
+ keystroke_matcher: KeymapMatcher,
next_entity_id: usize,
next_window_id: usize,
next_subscription_id: usize,
frame_count: usize,
- focus_observations: CallbackCollection<usize, FocusObservationCallback>,
- global_subscriptions: CallbackCollection<TypeId, GlobalSubscriptionCallback>,
- global_observations: CallbackCollection<TypeId, GlobalObservationCallback>,
subscriptions: CallbackCollection<usize, SubscriptionCallback>,
+ global_subscriptions: CallbackCollection<TypeId, GlobalSubscriptionCallback>,
observations: CallbackCollection<usize, ObservationCallback>,
+ global_observations: CallbackCollection<TypeId, GlobalObservationCallback>,
+ focus_observations: CallbackCollection<usize, FocusObservationCallback>,
+ release_observations: CallbackCollection<usize, ReleaseObservationCallback>,
+ action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
- release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
- action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
-
#[allow(clippy::type_complexity)]
presenters_and_platform_windows:
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
@@ -669,7 +667,7 @@ impl MutableAppContext {
capture_actions: Default::default(),
actions: Default::default(),
global_actions: Default::default(),
- keystroke_matcher: keymap::Matcher::default(),
+ keystroke_matcher: KeymapMatcher::default(),
next_entity_id: 0,
next_window_id: 0,
next_subscription_id: 0,
@@ -1047,12 +1045,10 @@ impl MutableAppContext {
callback(payload, cx)
}),
});
-
- Subscription::GlobalSubscription {
- id: subscription_id,
- type_id,
- subscriptions: Some(self.global_subscriptions.downgrade()),
- }
+ Subscription::GlobalSubscription(
+ self.global_subscriptions
+ .subscribe(type_id, subscription_id),
+ )
}
pub fn observe<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1089,11 +1085,7 @@ impl MutableAppContext {
}
}),
});
- Subscription::Subscription {
- id: subscription_id,
- entity_id: handle.id(),
- subscriptions: Some(self.subscriptions.downgrade()),
- }
+ Subscription::Subscription(self.subscriptions.subscribe(handle.id(), subscription_id))
}
fn observe_internal<E, H, F>(&mut self, handle: &H, mut callback: F) -> Subscription
@@ -1117,11 +1109,7 @@ impl MutableAppContext {
}
}),
});
- Subscription::Observation {
- id: subscription_id,
- entity_id,
- observations: Some(self.observations.downgrade()),
- }
+ Subscription::Observation(self.observations.subscribe(entity_id, subscription_id))
}
fn observe_focus<F, V>(&mut self, handle: &ViewHandle<V>, mut callback: F) -> Subscription
@@ -1144,12 +1132,7 @@ impl MutableAppContext {
}
}),
});
-
- Subscription::FocusObservation {
- id: subscription_id,
- view_id,
- observations: Some(self.focus_observations.downgrade()),
- }
+ Subscription::FocusObservation(self.focus_observations.subscribe(view_id, subscription_id))
}
pub fn observe_global<G, F>(&mut self, mut observe: F) -> Subscription
@@ -1165,12 +1148,7 @@ impl MutableAppContext {
id,
Box::new(move |cx: &mut MutableAppContext| observe(cx)),
);
-
- Subscription::GlobalObservation {
- id,
- type_id,
- observations: Some(self.global_observations.downgrade()),
- }
+ Subscription::GlobalObservation(self.global_observations.subscribe(type_id, id))
}
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
@@ -1192,36 +1170,31 @@ impl MutableAppContext {
F: 'static + FnOnce(&E, &mut Self),
{
let id = post_inc(&mut self.next_subscription_id);
- self.release_observations
- .lock()
- .entry(handle.id())
- .or_default()
- .insert(
- id,
- Box::new(move |released, cx| {
- let released = released.downcast_ref().unwrap();
- callback(released, cx)
- }),
- );
- Subscription::ReleaseObservation {
+ let mut callback = Some(callback);
+ self.release_observations.add_callback(
+ handle.id(),
id,
- entity_id: handle.id(),
- observations: Some(Arc::downgrade(&self.release_observations)),
- }
+ Box::new(move |released, cx| {
+ let released = released.downcast_ref().unwrap();
+ if let Some(callback) = callback.take() {
+ callback(released, cx)
+ }
+ }),
+ );
+ Subscription::ReleaseObservation(self.release_observations.subscribe(handle.id(), id))
}
pub fn observe_actions<F>(&mut self, callback: F) -> Subscription
where
F: 'static + FnMut(TypeId, &mut MutableAppContext),
{
- let id = post_inc(&mut self.next_subscription_id);
+ let subscription_id = post_inc(&mut self.next_subscription_id);
self.action_dispatch_observations
- .lock()
- .insert(id, Box::new(callback));
- Subscription::ActionObservation {
- id,
- observations: Some(Arc::downgrade(&self.action_dispatch_observations)),
- }
+ .add_callback((), subscription_id, Box::new(callback));
+ Subscription::ActionObservation(
+ self.action_dispatch_observations
+ .subscribe((), subscription_id),
+ )
}
fn observe_window_activation<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1235,11 +1208,10 @@ impl MutableAppContext {
subscription_id,
callback: Box::new(callback),
});
- Subscription::WindowActivationObservation {
- id: subscription_id,
- window_id,
- observations: Some(self.window_activation_observations.downgrade()),
- }
+ Subscription::WindowActivationObservation(
+ self.window_activation_observations
+ .subscribe(window_id, subscription_id),
+ )
}
fn observe_fullscreen<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1253,11 +1225,10 @@ impl MutableAppContext {
subscription_id,
callback: Box::new(callback),
});
- Subscription::WindowFullscreenObservation {
- id: subscription_id,
- window_id,
- observations: Some(self.window_activation_observations.downgrade()),
- }
+ Subscription::WindowActivationObservation(
+ self.window_activation_observations
+ .subscribe(window_id, subscription_id),
+ )
}
pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
@@ -1273,12 +1244,10 @@ impl MutableAppContext {
let subscription_id = post_inc(&mut self.next_subscription_id);
self.keystroke_observations
.add_callback(window_id, subscription_id, Box::new(callback));
-
- Subscription::KeystrokeObservation {
- id: subscription_id,
- window_id,
- observations: Some(self.keystroke_observations.downgrade()),
- }
+ Subscription::KeystrokeObservation(
+ self.keystroke_observations
+ .subscribe(window_id, subscription_id),
+ )
}
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
@@ -1391,8 +1360,10 @@ impl MutableAppContext {
.views
.get(&(window_id, *view_id))
.expect("view in responder chain does not exist");
- let cx = view.keymap_context(self.as_ref());
- let keystrokes = self.keystroke_matcher.keystrokes_for_action(action, &cx);
+ let keymap_context = view.keymap_context(self.as_ref());
+ let keystrokes = self
+ .keystroke_matcher
+ .keystrokes_for_action(action, &keymap_context);
if keystrokes.is_some() {
return keystrokes;
}
@@ -1473,7 +1444,7 @@ impl MutableAppContext {
})
}
- pub fn add_bindings<T: IntoIterator<Item = keymap::Binding>>(&mut self, bindings: T) {
+ pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
self.keystroke_matcher.add_bindings(bindings);
}
@@ -1999,15 +1970,13 @@ impl MutableAppContext {
entity_id,
subscription_id,
callback,
- } => self.subscriptions.add_or_remove_callback(
- entity_id,
- subscription_id,
- callback,
- ),
+ } => self
+ .subscriptions
+ .add_callback(entity_id, subscription_id, callback),
Effect::Event { entity_id, payload } => {
let mut subscriptions = self.subscriptions.clone();
- subscriptions.emit_and_cleanup(entity_id, self, |callback, this| {
+ subscriptions.emit(entity_id, self, |callback, this| {
callback(payload.as_ref(), this)
})
}
@@ -2016,7 +1985,7 @@ impl MutableAppContext {
type_id,
subscription_id,
callback,
- } => self.global_subscriptions.add_or_remove_callback(
+ } => self.global_subscriptions.add_callback(
type_id,
subscription_id,
callback,
@@ -2028,16 +1997,13 @@ impl MutableAppContext {
entity_id,
subscription_id,
callback,
- } => self.observations.add_or_remove_callback(
- entity_id,
- subscription_id,
- callback,
- ),
+ } => self
+ .observations
+ .add_callback(entity_id, subscription_id, callback),
Effect::ModelNotification { model_id } => {
let mut observations = self.observations.clone();
- observations
- .emit_and_cleanup(model_id, self, |callback, this| callback(this));
+ observations.emit(model_id, self, |callback, this| callback(this));
}
Effect::ViewNotification { window_id, view_id } => {
@@ -2046,7 +2012,7 @@ impl MutableAppContext {
Effect::GlobalNotification { type_id } => {
let mut subscriptions = self.global_observations.clone();
- subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+ subscriptions.emit(type_id, self, |callback, this| {
callback(this);
true
});
@@ -2080,7 +2046,7 @@ impl MutableAppContext {
subscription_id,
callback,
} => {
- self.focus_observations.add_or_remove_callback(
+ self.focus_observations.add_callback(
view_id,
subscription_id,
callback,
@@ -2099,7 +2065,7 @@ impl MutableAppContext {
window_id,
subscription_id,
callback,
- } => self.window_activation_observations.add_or_remove_callback(
+ } => self.window_activation_observations.add_callback(
window_id,
subscription_id,
callback,
@@ -2114,7 +2080,7 @@ impl MutableAppContext {
window_id,
subscription_id,
callback,
- } => self.window_fullscreen_observations.add_or_remove_callback(
+ } => self.window_fullscreen_observations.add_callback(
window_id,
subscription_id,
callback,
@@ -2159,6 +2125,7 @@ impl MutableAppContext {
self.remove_dropped_entities();
} else {
self.remove_dropped_entities();
+
if refreshing {
self.perform_window_refresh();
} else {
@@ -2295,7 +2262,7 @@ impl MutableAppContext {
let type_id = (&*payload).type_id();
let mut subscriptions = self.global_subscriptions.clone();
- subscriptions.emit_and_cleanup(type_id, self, |callback, this| {
+ subscriptions.emit(type_id, self, |callback, this| {
callback(payload.as_ref(), this);
true //Always alive
});
@@ -2320,17 +2287,18 @@ impl MutableAppContext {
}
let mut observations = self.observations.clone();
- observations.emit_and_cleanup(observed_view_id, self, |callback, this| callback(this));
+ observations.emit(observed_view_id, self, |callback, this| callback(this));
}
}
fn handle_entity_release_effect(&mut self, entity_id: usize, entity: &dyn Any) {
- let callbacks = self.release_observations.lock().remove(&entity_id);
- if let Some(callbacks) = callbacks {
- for (_, callback) in callbacks {
- callback(entity, self);
- }
- }
+ self.release_observations
+ .clone()
+ .emit(entity_id, self, |callback, this| {
+ callback(entity, this);
+ // Release observations happen one time. So clear the callback by returning false
+ false
+ })
}
fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) {
@@ -2350,7 +2318,7 @@ impl MutableAppContext {
window.is_fullscreen = is_fullscreen;
let mut observations = this.window_fullscreen_observations.clone();
- observations.emit_and_cleanup(window_id, this, |callback, this| {
+ observations.emit(window_id, this, |callback, this| {
callback(is_fullscreen, this)
});
@@ -2367,7 +2335,7 @@ impl MutableAppContext {
) {
self.update(|this| {
let mut observations = this.keystroke_observations.clone();
- observations.emit_and_cleanup(window_id, this, {
+ observations.emit(window_id, this, {
move |callback, this| callback(&keystroke, &result, handled_by.as_ref(), this)
});
});
@@ -2403,7 +2371,7 @@ impl MutableAppContext {
}
let mut observations = this.window_activation_observations.clone();
- observations.emit_and_cleanup(window_id, this, |callback, this| callback(active, this));
+ observations.emit(window_id, this, |callback, this| callback(active, this));
Some(())
});
@@ -2443,8 +2411,7 @@ impl MutableAppContext {
}
let mut subscriptions = this.focus_observations.clone();
- subscriptions
- .emit_and_cleanup(blurred_id, this, |callback, this| callback(false, this));
+ subscriptions.emit(blurred_id, this, |callback, this| callback(false, this));
}
if let Some(focused_id) = focused_id {
@@ -2456,8 +2423,7 @@ impl MutableAppContext {
}
let mut subscriptions = this.focus_observations.clone();
- subscriptions
- .emit_and_cleanup(focused_id, this, |callback, this| callback(true, this));
+ subscriptions.emit(focused_id, this, |callback, this| callback(true, this));
}
})
}
@@ -2513,11 +2479,12 @@ impl MutableAppContext {
}
fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) {
- let mut callbacks = mem::take(&mut *self.action_dispatch_observations.lock());
- for callback in callbacks.values_mut() {
- callback(action_id, self);
- }
- self.action_dispatch_observations.lock().extend(callbacks);
+ self.action_dispatch_observations
+ .clone()
+ .emit((), self, |callback, this| {
+ callback(action_id, this);
+ true
+ });
}
fn handle_window_should_close_subscription_effect(
@@ -3173,7 +3140,7 @@ pub trait AnyView {
window_id: usize,
view_id: usize,
) -> bool;
- fn keymap_context(&self, cx: &AppContext) -> keymap::Context;
+ fn keymap_context(&self, cx: &AppContext) -> KeymapContext;
fn debug_json(&self, cx: &AppContext) -> serde_json::Value;
fn text_for_range(&self, range: Range<usize>, cx: &AppContext) -> Option<String>;
@@ -3315,7 +3282,7 @@ where
View::modifiers_changed(self, event, &mut cx)
}
- fn keymap_context(&self, cx: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, cx: &AppContext) -> KeymapContext {
View::keymap_context(self, cx)
}
@@ -4038,7 +4005,7 @@ pub struct RenderContext<'a, T: View> {
pub refreshing: bool,
}
-#[derive(Clone, Default)]
+#[derive(Debug, Clone, Default)]
pub struct MouseState {
hovered: bool,
clicked: Option<MouseButton>,
@@ -5106,269 +5073,46 @@ impl<T> Drop for ElementStateHandle<T> {
#[must_use]
pub enum Subscription {
- Subscription {
- id: usize,
- entity_id: usize,
- subscriptions: Option<Weak<Mapping<usize, SubscriptionCallback>>>,
- },
- GlobalSubscription {
- id: usize,
- type_id: TypeId,
- subscriptions: Option<Weak<Mapping<TypeId, GlobalSubscriptionCallback>>>,
- },
- Observation {
- id: usize,
- entity_id: usize,
- observations: Option<Weak<Mapping<usize, ObservationCallback>>>,
- },
- GlobalObservation {
- id: usize,
- type_id: TypeId,
- observations: Option<Weak<Mapping<TypeId, GlobalObservationCallback>>>,
- },
- FocusObservation {
- id: usize,
- view_id: usize,
- observations: Option<Weak<Mapping<usize, FocusObservationCallback>>>,
- },
- WindowActivationObservation {
- id: usize,
- window_id: usize,
- observations: Option<Weak<Mapping<usize, WindowActivationCallback>>>,
- },
- WindowFullscreenObservation {
- id: usize,
- window_id: usize,
- observations: Option<Weak<Mapping<usize, WindowFullscreenCallback>>>,
- },
- KeystrokeObservation {
- id: usize,
- window_id: usize,
- observations: Option<Weak<Mapping<usize, KeystrokeCallback>>>,
- },
-
- ReleaseObservation {
- id: usize,
- entity_id: usize,
- #[allow(clippy::type_complexity)]
- observations:
- Option<Weak<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>>,
- },
- ActionObservation {
- id: usize,
- observations: Option<Weak<Mutex<BTreeMap<usize, ActionObservationCallback>>>>,
- },
+ Subscription(callback_collection::Subscription<usize, SubscriptionCallback>),
+ Observation(callback_collection::Subscription<usize, ObservationCallback>),
+ GlobalSubscription(callback_collection::Subscription<TypeId, GlobalSubscriptionCallback>),
+ GlobalObservation(callback_collection::Subscription<TypeId, GlobalObservationCallback>),
+ FocusObservation(callback_collection::Subscription<usize, FocusObservationCallback>),
+ WindowActivationObservation(callback_collection::Subscription<usize, WindowActivationCallback>),
+ WindowFullscreenObservation(callback_collection::Subscription<usize, WindowFullscreenCallback>),
+ KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
+ ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
+ ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
}
impl Subscription {
- pub fn detach(&mut self) {
+ pub fn id(&self) -> usize {
match self {
- Subscription::Subscription { subscriptions, .. } => {
- subscriptions.take();
- }
- Subscription::GlobalSubscription { subscriptions, .. } => {
- subscriptions.take();
- }
- Subscription::Observation { observations, .. } => {
- observations.take();
- }
- Subscription::GlobalObservation { observations, .. } => {
- observations.take();
- }
- Subscription::ReleaseObservation { observations, .. } => {
- observations.take();
- }
- Subscription::FocusObservation { observations, .. } => {
- observations.take();
- }
- Subscription::ActionObservation { observations, .. } => {
- observations.take();
- }
- Subscription::KeystrokeObservation { observations, .. } => {
- observations.take();
- }
- Subscription::WindowActivationObservation { observations, .. } => {
- observations.take();
- }
- Subscription::WindowFullscreenObservation { observations, .. } => {
- observations.take();
- }
+ Subscription::Subscription(subscription) => subscription.id(),
+ Subscription::Observation(subscription) => subscription.id(),
+ Subscription::GlobalSubscription(subscription) => subscription.id(),
+ Subscription::GlobalObservation(subscription) => subscription.id(),
+ Subscription::FocusObservation(subscription) => subscription.id(),
+ Subscription::WindowActivationObservation(subscription) => subscription.id(),
+ Subscription::WindowFullscreenObservation(subscription) => subscription.id(),
+ Subscription::KeystrokeObservation(subscription) => subscription.id(),
+ Subscription::ReleaseObservation(subscription) => subscription.id(),
+ Subscription::ActionObservation(subscription) => subscription.id(),
}
}
-}
-impl Drop for Subscription {
- fn drop(&mut self) {
+ pub fn detach(&mut self) {
match self {
- Subscription::Subscription {
- id,
- entity_id,
- subscriptions,
- } => {
- if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
- match subscriptions
- .lock()
- .entry(*entity_id)
- .or_default()
- .entry(*id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::GlobalSubscription {
- id,
- type_id,
- subscriptions,
- } => {
- if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) {
- match subscriptions.lock().entry(*type_id).or_default().entry(*id) {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::Observation {
- id,
- entity_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations
- .lock()
- .entry(*entity_id)
- .or_default()
- .entry(*id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::GlobalObservation {
- id,
- type_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations.lock().entry(*type_id).or_default().entry(*id) {
- collections::btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- collections::btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::ReleaseObservation {
- id,
- entity_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- if let Some(observations) = observations.lock().get_mut(entity_id) {
- observations.remove(id);
- }
- }
- }
- Subscription::FocusObservation {
- id,
- view_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations.lock().entry(*view_id).or_default().entry(*id) {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::ActionObservation { id, observations } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- observations.lock().remove(id);
- }
- }
- Subscription::KeystrokeObservation {
- id,
- window_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations
- .lock()
- .entry(*window_id)
- .or_default()
- .entry(*id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::WindowActivationObservation {
- id,
- window_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations
- .lock()
- .entry(*window_id)
- .or_default()
- .entry(*id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
- Subscription::WindowFullscreenObservation {
- id,
- window_id,
- observations,
- } => {
- if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
- match observations
- .lock()
- .entry(*window_id)
- .or_default()
- .entry(*id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(None);
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
- }
+ Subscription::Subscription(subscription) => subscription.detach(),
+ Subscription::GlobalSubscription(subscription) => subscription.detach(),
+ Subscription::Observation(subscription) => subscription.detach(),
+ Subscription::GlobalObservation(subscription) => subscription.detach(),
+ Subscription::FocusObservation(subscription) => subscription.detach(),
+ Subscription::KeystrokeObservation(subscription) => subscription.detach(),
+ Subscription::WindowActivationObservation(subscription) => subscription.detach(),
+ Subscription::WindowFullscreenObservation(subscription) => subscription.detach(),
+ Subscription::ReleaseObservation(subscription) => subscription.detach(),
+ Subscription::ActionObservation(subscription) => subscription.detach(),
}
}
}
@@ -6015,60 +5759,44 @@ mod tests {
#[crate::test(self)]
fn test_view_events(cx: &mut MutableAppContext) {
- #[derive(Default)]
- struct View {
- events: Vec<usize>,
- }
-
- impl Entity for View {
- type Event = usize;
- }
-
- impl super::View for View {
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
-
- fn ui_name() -> &'static str {
- "View"
- }
- }
-
struct Model;
impl Entity for Model {
- type Event = usize;
+ type Event = String;
}
- let (_, handle_1) = cx.add_window(Default::default(), |_| View::default());
- let handle_2 = cx.add_view(&handle_1, |_| View::default());
+ let (_, handle_1) = cx.add_window(Default::default(), |_| TestView::default());
+ let handle_2 = cx.add_view(&handle_1, |_| TestView::default());
let handle_3 = cx.add_model(|_| Model);
handle_1.update(cx, |_, cx| {
cx.subscribe(&handle_2, move |me, emitter, event, cx| {
- me.events.push(*event);
+ me.events.push(event.clone());
cx.subscribe(&emitter, |me, _, event, _| {
- me.events.push(*event * 2);
+ me.events.push(format!("{event} from inner"));
})
.detach();
})
.detach();
cx.subscribe(&handle_3, |me, _, event, _| {
- me.events.push(*event);
+ me.events.push(event.clone());
})
.detach();
});
- handle_2.update(cx, |_, c| c.emit(7));
- assert_eq!(handle_1.read(cx).events, vec![7]);
+ handle_2.update(cx, |_, c| c.emit("7".into()));
+ assert_eq!(handle_1.read(cx).events, vec!["7"]);
- handle_2.update(cx, |_, c| c.emit(5));
- assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]);
+ handle_2.update(cx, |_, c| c.emit("5".into()));
+ assert_eq!(handle_1.read(cx).events, vec!["7", "5", "5 from inner"]);
- handle_3.update(cx, |_, c| c.emit(9));
- assert_eq!(handle_1.read(cx).events, vec![7, 5, 10, 9]);
+ handle_3.update(cx, |_, c| c.emit("9".into()));
+ assert_eq!(
+ handle_1.read(cx).events,
+ vec!["7", "5", "5 from inner", "9"]
+ );
}
#[crate::test(self)]
@@ -6259,31 +5987,15 @@ mod tests {
#[crate::test(self)]
fn test_dropping_subscribers(cx: &mut MutableAppContext) {
- struct View;
-
- impl Entity for View {
- type Event = ();
- }
-
- impl super::View for View {
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
-
- fn ui_name() -> &'static str {
- "View"
- }
- }
-
struct Model;
impl Entity for Model {
type Event = ();
}
- let (_, root_view) = cx.add_window(Default::default(), |_| View);
- let observing_view = cx.add_view(&root_view, |_| View);
- let emitting_view = cx.add_view(&root_view, |_| View);
+ let (_, root_view) = cx.add_window(Default::default(), |_| TestView::default());
+ let observing_view = cx.add_view(&root_view, |_| TestView::default());
+ let emitting_view = cx.add_view(&root_view, |_| TestView::default());
let observing_model = cx.add_model(|_| Model);
let observed_model = cx.add_model(|_| Model);
@@ -6300,165 +6012,117 @@ mod tests {
drop(observing_model);
});
- emitting_view.update(cx, |_, cx| cx.emit(()));
+ emitting_view.update(cx, |_, cx| cx.emit(Default::default()));
observed_model.update(cx, |_, cx| cx.emit(()));
}
#[crate::test(self)]
fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) {
- #[derive(Default)]
- struct TestView;
-
- impl Entity for TestView {
- type Event = ();
- }
-
- impl View for TestView {
- fn ui_name() -> &'static str {
- "TestView"
- }
-
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
- }
-
- let events = Rc::new(RefCell::new(Vec::new()));
- cx.add_window(Default::default(), |cx| {
+ let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
drop(cx.subscribe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _, _| events.borrow_mut().push("dropped before flush")
+ move |this, _, _, _| this.events.push("dropped before flush".into())
}));
cx.subscribe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _, _| events.borrow_mut().push("before emit")
+ move |this, _, _, _| this.events.push("before emit".into())
})
.detach();
- cx.emit(());
+ cx.emit("the event".into());
cx.subscribe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _, _| events.borrow_mut().push("after emit")
+ move |this, _, _, _| this.events.push("after emit".into())
})
.detach();
- TestView
+ TestView { events: Vec::new() }
});
- assert_eq!(*events.borrow(), ["before emit"]);
+
+ assert_eq!(view.read(cx).events, ["before emit"]);
}
#[crate::test(self)]
fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) {
- #[derive(Default)]
- struct View {
- events: Vec<usize>,
- }
-
- impl Entity for View {
- type Event = usize;
- }
-
- impl super::View for View {
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
-
- fn ui_name() -> &'static str {
- "View"
- }
- }
-
#[derive(Default)]
struct Model {
- count: usize,
+ state: String,
}
impl Entity for Model {
type Event = ();
}
- let (_, view) = cx.add_window(Default::default(), |_| View::default());
- let model = cx.add_model(|_| Model::default());
+ let (_, view) = cx.add_window(Default::default(), |_| TestView::default());
+ let model = cx.add_model(|_| Model {
+ state: "old-state".into(),
+ });
view.update(cx, |_, c| {
c.observe(&model, |me, observed, c| {
- me.events.push(observed.read(c).count)
+ me.events.push(observed.read(c).state.clone())
})
.detach();
});
- model.update(cx, |model, c| {
- model.count = 11;
- c.notify();
+ model.update(cx, |model, cx| {
+ model.state = "new-state".into();
+ cx.notify();
});
- assert_eq!(view.read(cx).events, vec![11]);
+ assert_eq!(view.read(cx).events, vec!["new-state"]);
}
#[crate::test(self)]
fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) {
- #[derive(Default)]
- struct TestView;
-
- impl Entity for TestView {
- type Event = ();
- }
-
- impl View for TestView {
- fn ui_name() -> &'static str {
- "TestView"
- }
-
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
- }
-
- let events = Rc::new(RefCell::new(Vec::new()));
- cx.add_window(Default::default(), |cx| {
+ let (_, view) = cx.add_window::<TestView, _>(Default::default(), |cx| {
drop(cx.observe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _| events.borrow_mut().push("dropped before flush")
+ move |this, _, _| this.events.push("dropped before flush".into())
}));
cx.observe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _| events.borrow_mut().push("before notify")
+ move |this, _, _| this.events.push("before notify".into())
})
.detach();
cx.notify();
cx.observe(&cx.handle(), {
- let events = events.clone();
- move |_, _, _| events.borrow_mut().push("after notify")
+ move |this, _, _| this.events.push("after notify".into())
})
.detach();
- TestView
+ TestView { events: Vec::new() }
});
- assert_eq!(*events.borrow(), ["before notify"]);
+
+ assert_eq!(view.read(cx).events, ["before notify"]);
}
#[crate::test(self)]
- fn test_dropping_observers(cx: &mut MutableAppContext) {
- struct View;
-
- impl Entity for View {
+ fn test_notify_and_drop_observe_subscription_in_same_update_cycle(cx: &mut MutableAppContext) {
+ struct Model;
+ impl Entity for Model {
type Event = ();
}
- impl super::View for View {
- fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
- Empty::new().boxed()
- }
+ let model = cx.add_model(|_| Model);
+ let (_, view) = cx.add_window(Default::default(), |_| TestView::default());
- fn ui_name() -> &'static str {
- "View"
- }
+ view.update(cx, |_, cx| {
+ model.update(cx, |_, cx| cx.notify());
+ drop(cx.observe(&model, move |this, _, _| {
+ this.events.push("model notified".into());
+ }));
+ model.update(cx, |_, cx| cx.notify());
+ });
+
+ for _ in 0..3 {
+ model.update(cx, |_, cx| cx.notify());
}
+ assert_eq!(view.read(cx).events, Vec::<String>::new());
+ }
+
+ #[crate::test(self)]
+ fn test_dropping_observers(cx: &mut MutableAppContext) {
struct Model;
impl Entity for Model {
type Event = ();
}
- let (_, root_view) = cx.add_window(Default::default(), |_| View);
- let observing_view = cx.add_view(root_view, |_| View);
+ let (_, root_view) = cx.add_window(Default::default(), |_| TestView::default());
+ let observing_view = cx.add_view(root_view, |_| TestView::default());
let observing_model = cx.add_model(|_| Model);
let observed_model = cx.add_model(|_| Model);
@@ -1,19 +1,44 @@
+use crate::MutableAppContext;
+use collections::{BTreeMap, HashMap, HashSet};
+use parking_lot::Mutex;
use std::sync::Arc;
use std::{hash::Hash, sync::Weak};
-use parking_lot::Mutex;
+pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
+ internal: Arc<Mutex<Mapping<K, F>>>,
+}
-use collections::{btree_map, BTreeMap, HashMap};
+pub struct Subscription<K: Clone + Hash + Eq, F> {
+ key: K,
+ id: usize,
+ mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
+}
-use crate::MutableAppContext;
+struct Mapping<K, F> {
+ callbacks: HashMap<K, BTreeMap<usize, F>>,
+ dropped_subscriptions: HashMap<K, HashSet<usize>>,
+}
-pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
+impl<K: Hash + Eq, F> Mapping<K, F> {
+ fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
+ if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
+ subscriptions.remove(&subscription_id)
+ } else {
+ false
+ }
+ }
+}
-pub struct CallbackCollection<K: Hash + Eq, F> {
- internal: Arc<Mapping<K, F>>,
+impl<K, F> Default for Mapping<K, F> {
+ fn default() -> Self {
+ Self {
+ callbacks: Default::default(),
+ dropped_subscriptions: Default::default(),
+ }
+ }
}
-impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
+impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
fn clone(&self) -> Self {
Self {
internal: self.internal.clone(),
@@ -21,7 +46,7 @@ impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
}
}
-impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
+impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
fn default() -> Self {
CallbackCollection {
internal: Arc::new(Mutex::new(Default::default())),
@@ -29,78 +54,114 @@ impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
}
}
-impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
- pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
- Arc::downgrade(&self.internal)
- }
-
+impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
#[cfg(test)]
pub fn is_empty(&self) -> bool {
- self.internal.lock().is_empty()
+ self.internal.lock().callbacks.is_empty()
}
- pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
- self.internal
- .lock()
- .entry(id)
- .or_default()
- .insert(subscription_id, Some(callback));
+ pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
+ Subscription {
+ key,
+ id: subscription_id,
+ mapping: Some(Arc::downgrade(&self.internal)),
+ }
}
- pub fn remove(&mut self, id: K) {
- self.internal.lock().remove(&id);
- }
+ pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
+ let mut this = self.internal.lock();
+
+ // If this callback's subscription was dropped before the callback was
+ // added, then just drop the callback.
+ if this.clear_dropped_state(&key, subscription_id) {
+ return;
+ }
- pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
- match self
- .internal
- .lock()
- .entry(id)
+ this.callbacks
+ .entry(key)
.or_default()
- .entry(subscription_id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(Some(callback));
- }
+ .insert(subscription_id, callback);
+ }
- btree_map::Entry::Occupied(entry) => {
- // TODO: This seems like it should never be called because no code
- // should ever attempt to remove an existing callback
- debug_assert!(entry.get().is_none());
- entry.remove();
- }
- }
+ pub fn remove(&mut self, key: K) {
+ // Drop these callbacks after releasing the lock, in case one of them
+ // owns a subscription to this callback collection.
+ let mut this = self.internal.lock();
+ let callbacks = this.callbacks.remove(&key);
+ this.dropped_subscriptions.remove(&key);
+ drop(this);
+ drop(callbacks);
}
- pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
+ pub fn emit<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
&mut self,
- id: K,
+ key: K,
cx: &mut MutableAppContext,
mut call_callback: C,
) {
- let callbacks = self.internal.lock().remove(&id);
+ let callbacks = self.internal.lock().callbacks.remove(&key);
if let Some(callbacks) = callbacks {
- for (subscription_id, callback) in callbacks {
- if let Some(mut callback) = callback {
- let alive = call_callback(&mut callback, cx);
- if alive {
- match self
- .internal
- .lock()
- .entry(id)
- .or_default()
- .entry(subscription_id)
- {
- btree_map::Entry::Vacant(entry) => {
- entry.insert(Some(callback));
- }
- btree_map::Entry::Occupied(entry) => {
- entry.remove();
- }
- }
- }
+ for (subscription_id, mut callback) in callbacks {
+ // If this callback's subscription was dropped while invoking an
+ // earlier callback, then just drop the callback.
+ let mut this = self.internal.lock();
+ if this.clear_dropped_state(&key, subscription_id) {
+ continue;
}
+
+ drop(this);
+ let alive = call_callback(&mut callback, cx);
+
+ // If this callback's subscription was dropped while invoking the callback
+ // itself, or if the callback returns false, then just drop the callback.
+ let mut this = self.internal.lock();
+ if this.clear_dropped_state(&key, subscription_id) || !alive {
+ continue;
+ }
+
+ this.callbacks
+ .entry(key)
+ .or_default()
+ .insert(subscription_id, callback);
}
}
}
}
+
+impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
+ pub fn id(&self) -> usize {
+ self.id
+ }
+
+ pub fn detach(&mut self) {
+ self.mapping.take();
+ }
+}
+
+impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
+ fn drop(&mut self) {
+ if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
+ let mut mapping = mapping.lock();
+
+ // If the callback is present in the mapping, then just remove it.
+ if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
+ let callback = callbacks.remove(&self.id);
+ if callback.is_some() {
+ drop(mapping);
+ drop(callback);
+ return;
+ }
+ }
+
+ // If this subscription's callback is not present, then either it has been
+ // temporarily removed during emit, or it has not yet been added. Record
+ // that this subscription has been dropped so that the callback can be
+ // removed later.
+ mapping
+ .dropped_subscriptions
+ .entry(self.key.clone())
+ .or_default()
+ .insert(self.id);
+ }
+ }
+}
@@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock};
use smol::stream::StreamExt;
use crate::{
- executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
- AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
- ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
- RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
- WindowInputHandler,
+ executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
+ AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
+ LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
+ ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
+ WeakHandle, WindowInputHandler,
};
use collections::BTreeMap;
@@ -25,7 +25,7 @@ pub mod executor;
pub use executor::Task;
pub mod color;
pub mod json;
-pub mod keymap;
+pub mod keymap_matcher;
pub mod platform;
pub use gpui_macros::test;
pub use platform::*;
@@ -1,757 +0,0 @@
-use crate::Action;
-use anyhow::{anyhow, Result};
-use smallvec::SmallVec;
-use std::{
- any::{Any, TypeId},
- collections::{HashMap, HashSet},
- fmt::{Debug, Write},
-};
-use tree_sitter::{Language, Node, Parser};
-
-extern "C" {
- fn tree_sitter_context_predicate() -> Language;
-}
-
-pub struct Matcher {
- pending_views: HashMap<usize, Context>,
- pending_keystrokes: Vec<Keystroke>,
- keymap: Keymap,
-}
-
-#[derive(Default)]
-pub struct Keymap {
- bindings: Vec<Binding>,
- binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
-}
-
-pub struct Binding {
- keystrokes: SmallVec<[Keystroke; 2]>,
- action: Box<dyn Action>,
- context_predicate: Option<ContextPredicate>,
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Keystroke {
- pub ctrl: bool,
- pub alt: bool,
- pub shift: bool,
- pub cmd: bool,
- pub function: bool,
- pub key: String,
-}
-
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub struct Context {
- pub set: HashSet<String>,
- pub map: HashMap<String, String>,
-}
-
-#[derive(Debug, Eq, PartialEq)]
-enum ContextPredicate {
- Identifier(String),
- Equal(String, String),
- NotEqual(String, String),
- Not(Box<ContextPredicate>),
- And(Box<ContextPredicate>, Box<ContextPredicate>),
- Or(Box<ContextPredicate>, Box<ContextPredicate>),
-}
-
-trait ActionArg {
- fn boxed_clone(&self) -> Box<dyn Any>;
-}
-
-impl<T> ActionArg for T
-where
- T: 'static + Any + Clone,
-{
- fn boxed_clone(&self) -> Box<dyn Any> {
- Box::new(self.clone())
- }
-}
-
-pub enum MatchResult {
- None,
- Pending,
- Matches(Vec<(usize, Box<dyn Action>)>),
-}
-
-impl Debug for MatchResult {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- MatchResult::None => f.debug_struct("MatchResult::None").finish(),
- MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
- MatchResult::Matches(matches) => f
- .debug_list()
- .entries(
- matches
- .iter()
- .map(|(view_id, action)| format!("{view_id}, {}", action.name())),
- )
- .finish(),
- }
- }
-}
-
-impl PartialEq for MatchResult {
- fn eq(&self, other: &Self) -> bool {
- match (self, other) {
- (MatchResult::None, MatchResult::None) => true,
- (MatchResult::Pending, MatchResult::Pending) => true,
- (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
- matches.len() == other_matches.len()
- && matches.iter().zip(other_matches.iter()).all(
- |((view_id, action), (other_view_id, other_action))| {
- view_id == other_view_id && action.eq(other_action.as_ref())
- },
- )
- }
- _ => false,
- }
- }
-}
-
-impl Eq for MatchResult {}
-
-impl Clone for MatchResult {
- fn clone(&self) -> Self {
- match self {
- MatchResult::None => MatchResult::None,
- MatchResult::Pending => MatchResult::Pending,
- MatchResult::Matches(matches) => MatchResult::Matches(
- matches
- .iter()
- .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
- .collect(),
- ),
- }
- }
-}
-
-impl Matcher {
- pub fn new(keymap: Keymap) -> Self {
- Self {
- pending_views: HashMap::new(),
- pending_keystrokes: Vec::new(),
- keymap,
- }
- }
-
- pub fn set_keymap(&mut self, keymap: Keymap) {
- self.clear_pending();
- self.keymap = keymap;
- }
-
- pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
- self.clear_pending();
- self.keymap.add_bindings(bindings);
- }
-
- pub fn clear_bindings(&mut self) {
- self.clear_pending();
- self.keymap.clear();
- }
-
- pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
- self.keymap.bindings_for_action_type(action_type)
- }
-
- pub fn clear_pending(&mut self) {
- self.pending_keystrokes.clear();
- self.pending_views.clear();
- }
-
- pub fn has_pending_keystrokes(&self) -> bool {
- !self.pending_keystrokes.is_empty()
- }
-
- pub fn push_keystroke(
- &mut self,
- keystroke: Keystroke,
- dispatch_path: Vec<(usize, Context)>,
- ) -> MatchResult {
- let mut any_pending = false;
- let mut matched_bindings = Vec::new();
-
- let first_keystroke = self.pending_keystrokes.is_empty();
- self.pending_keystrokes.push(keystroke);
-
- for (view_id, context) in dispatch_path {
- // Don't require pending view entry if there are no pending keystrokes
- if !first_keystroke && !self.pending_views.contains_key(&view_id) {
- continue;
- }
-
- // If there is a previous view context, invalidate that view if it
- // has changed
- if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
- if previous_view_context != context {
- continue;
- }
- }
-
- // Find the bindings which map the pending keystrokes and current context
- for binding in self.keymap.bindings.iter().rev() {
- if binding.keystrokes.starts_with(&self.pending_keystrokes)
- && binding
- .context_predicate
- .as_ref()
- .map(|c| c.eval(&context))
- .unwrap_or(true)
- {
- // If the binding is completed, push it onto the matches list
- if binding.keystrokes.len() == self.pending_keystrokes.len() {
- matched_bindings.push((view_id, binding.action.boxed_clone()));
- } else {
- // Otherwise, the binding is still pending
- self.pending_views.insert(view_id, context.clone());
- any_pending = true;
- }
- }
- }
- }
-
- if !any_pending {
- self.clear_pending();
- }
-
- if !matched_bindings.is_empty() {
- MatchResult::Matches(matched_bindings)
- } else if any_pending {
- MatchResult::Pending
- } else {
- MatchResult::None
- }
- }
-
- pub fn keystrokes_for_action(
- &self,
- action: &dyn Action,
- cx: &Context,
- ) -> Option<SmallVec<[Keystroke; 2]>> {
- for binding in self.keymap.bindings.iter().rev() {
- if binding.action.eq(action)
- && binding
- .context_predicate
- .as_ref()
- .map_or(true, |predicate| predicate.eval(cx))
- {
- return Some(binding.keystrokes.clone());
- }
- }
- None
- }
-}
-
-impl Default for Matcher {
- fn default() -> Self {
- Self::new(Keymap::default())
- }
-}
-
-impl Keymap {
- pub fn new(bindings: Vec<Binding>) -> Self {
- let mut binding_indices_by_action_type = HashMap::new();
- for (ix, binding) in bindings.iter().enumerate() {
- binding_indices_by_action_type
- .entry(binding.action.as_any().type_id())
- .or_insert_with(SmallVec::new)
- .push(ix);
- }
- Self {
- binding_indices_by_action_type,
- bindings,
- }
- }
-
- fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &'_ Binding> {
- self.binding_indices_by_action_type
- .get(&action_type)
- .map(SmallVec::as_slice)
- .unwrap_or(&[])
- .iter()
- .map(|ix| &self.bindings[*ix])
- }
-
- fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
- for binding in bindings {
- self.binding_indices_by_action_type
- .entry(binding.action.as_any().type_id())
- .or_default()
- .push(self.bindings.len());
- self.bindings.push(binding);
- }
- }
-
- fn clear(&mut self) {
- self.bindings.clear();
- self.binding_indices_by_action_type.clear();
- }
-}
-
-impl Binding {
- pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
- Self::load(keystrokes, Box::new(action), context).unwrap()
- }
-
- pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
- let context = if let Some(context) = context {
- Some(ContextPredicate::parse(context)?)
- } else {
- None
- };
-
- let keystrokes = keystrokes
- .split_whitespace()
- .map(Keystroke::parse)
- .collect::<Result<_>>()?;
-
- Ok(Self {
- keystrokes,
- action,
- context_predicate: context,
- })
- }
-
- pub fn keystrokes(&self) -> &[Keystroke] {
- &self.keystrokes
- }
-
- pub fn action(&self) -> &dyn Action {
- self.action.as_ref()
- }
-}
-
-impl Keystroke {
- pub fn parse(source: &str) -> anyhow::Result<Self> {
- let mut ctrl = false;
- let mut alt = false;
- let mut shift = false;
- let mut cmd = false;
- let mut function = false;
- let mut key = None;
-
- let mut components = source.split('-').peekable();
- while let Some(component) = components.next() {
- match component {
- "ctrl" => ctrl = true,
- "alt" => alt = true,
- "shift" => shift = true,
- "cmd" => cmd = true,
- "fn" => function = true,
- _ => {
- if let Some(component) = components.peek() {
- if component.is_empty() && source.ends_with('-') {
- key = Some(String::from("-"));
- break;
- } else {
- return Err(anyhow!("Invalid keystroke `{}`", source));
- }
- } else {
- key = Some(String::from(component));
- }
- }
- }
- }
-
- let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
-
- Ok(Keystroke {
- ctrl,
- alt,
- shift,
- cmd,
- function,
- key,
- })
- }
-
- pub fn modified(&self) -> bool {
- self.ctrl || self.alt || self.shift || self.cmd
- }
-}
-
-impl std::fmt::Display for Keystroke {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- if self.ctrl {
- f.write_char('^')?;
- }
- if self.alt {
- f.write_char('β')?;
- }
- if self.cmd {
- f.write_char('β')?;
- }
- if self.shift {
- f.write_char('β§')?;
- }
- let key = match self.key.as_str() {
- "backspace" => 'β«',
- "up" => 'β',
- "down" => 'β',
- "left" => 'β',
- "right" => 'β',
- "tab" => 'β₯',
- "escape" => 'β',
- key => {
- if key.len() == 1 {
- key.chars().next().unwrap().to_ascii_uppercase()
- } else {
- return f.write_str(key);
- }
- }
- };
- f.write_char(key)
- }
-}
-
-impl Context {
- pub fn extend(&mut self, other: &Context) {
- for v in &other.set {
- self.set.insert(v.clone());
- }
- for (k, v) in &other.map {
- self.map.insert(k.clone(), v.clone());
- }
- }
-}
-
-impl ContextPredicate {
- fn parse(source: &str) -> anyhow::Result<Self> {
- let mut parser = Parser::new();
- let language = unsafe { tree_sitter_context_predicate() };
- parser.set_language(language).unwrap();
- let source = source.as_bytes();
- let tree = parser.parse(source, None).unwrap();
- Self::from_node(tree.root_node(), source)
- }
-
- fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
- let parse_error = "error parsing context predicate";
- let kind = node.kind();
-
- match kind {
- "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
- "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
- "not" => {
- let child = Self::from_node(
- node.child_by_field_name("expression")
- .ok_or_else(|| anyhow!(parse_error))?,
- source,
- )?;
- Ok(Self::Not(Box::new(child)))
- }
- "and" | "or" => {
- let left = Box::new(Self::from_node(
- node.child_by_field_name("left")
- .ok_or_else(|| anyhow!(parse_error))?,
- source,
- )?);
- let right = Box::new(Self::from_node(
- node.child_by_field_name("right")
- .ok_or_else(|| anyhow!(parse_error))?,
- source,
- )?);
- if kind == "and" {
- Ok(Self::And(left, right))
- } else {
- Ok(Self::Or(left, right))
- }
- }
- "equal" | "not_equal" => {
- let left = node
- .child_by_field_name("left")
- .ok_or_else(|| anyhow!(parse_error))?
- .utf8_text(source)?
- .into();
- let right = node
- .child_by_field_name("right")
- .ok_or_else(|| anyhow!(parse_error))?
- .utf8_text(source)?
- .into();
- if kind == "equal" {
- Ok(Self::Equal(left, right))
- } else {
- Ok(Self::NotEqual(left, right))
- }
- }
- "parenthesized" => Self::from_node(
- node.child_by_field_name("expression")
- .ok_or_else(|| anyhow!(parse_error))?,
- source,
- ),
- _ => Err(anyhow!(parse_error)),
- }
- }
-
- fn eval(&self, cx: &Context) -> bool {
- match self {
- Self::Identifier(name) => cx.set.contains(name.as_str()),
- Self::Equal(left, right) => cx
- .map
- .get(left)
- .map(|value| value == right)
- .unwrap_or(false),
- Self::NotEqual(left, right) => {
- cx.map.get(left).map(|value| value != right).unwrap_or(true)
- }
- Self::Not(pred) => !pred.eval(cx),
- Self::And(left, right) => left.eval(cx) && right.eval(cx),
- Self::Or(left, right) => left.eval(cx) || right.eval(cx),
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use anyhow::Result;
- use serde::Deserialize;
-
- use crate::{actions, impl_actions};
-
- use super::*;
-
- #[test]
- fn test_push_keystroke() -> Result<()> {
- actions!(test, [B, AB, C, D, DA]);
-
- let mut ctx1 = Context::default();
- ctx1.set.insert("1".into());
-
- let mut ctx2 = Context::default();
- ctx2.set.insert("2".into());
-
- let dispatch_path = vec![(2, ctx2), (1, ctx1)];
-
- let keymap = Keymap::new(vec![
- Binding::new("a b", AB, Some("1")),
- Binding::new("b", B, Some("2")),
- Binding::new("c", C, Some("2")),
- Binding::new("d", D, Some("1")),
- Binding::new("d", D, Some("2")),
- Binding::new("d a", DA, Some("2")),
- ]);
-
- let mut matcher = Matcher::new(keymap);
-
- // Binding with pending prefix always takes precedence
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
- MatchResult::Pending,
- );
- // B alone doesn't match because a was pending, so AB is returned instead
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
- MatchResult::Matches(vec![(1, Box::new(AB))]),
- );
- assert!(!matcher.has_pending_keystrokes());
-
- // Without an a prefix, B is dispatched like expected
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
- MatchResult::Matches(vec![(2, Box::new(B))]),
- );
- assert!(!matcher.has_pending_keystrokes());
-
- // If a is prefixed, C will not be dispatched because there
- // was a pending binding for it
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
- MatchResult::Pending,
- );
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
- MatchResult::None,
- );
- assert!(!matcher.has_pending_keystrokes());
-
- // If a single keystroke matches multiple bindings in the tree
- // all of them are returned so that we can fallback if the action
- // handler decides to propagate the action
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
- MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
- );
- // If none of the d action handlers consume the binding, a pending
- // binding may then be used
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
- MatchResult::Matches(vec![(2, Box::new(DA))]),
- );
- assert!(!matcher.has_pending_keystrokes());
-
- Ok(())
- }
-
- #[test]
- fn test_keystroke_parsing() -> Result<()> {
- assert_eq!(
- Keystroke::parse("ctrl-p")?,
- Keystroke {
- key: "p".into(),
- ctrl: true,
- alt: false,
- shift: false,
- cmd: false,
- function: false,
- }
- );
-
- assert_eq!(
- Keystroke::parse("alt-shift-down")?,
- Keystroke {
- key: "down".into(),
- ctrl: false,
- alt: true,
- shift: true,
- cmd: false,
- function: false,
- }
- );
-
- assert_eq!(
- Keystroke::parse("shift-cmd--")?,
- Keystroke {
- key: "-".into(),
- ctrl: false,
- alt: false,
- shift: true,
- cmd: true,
- function: false,
- }
- );
-
- Ok(())
- }
-
- #[test]
- fn test_context_predicate_parsing() -> Result<()> {
- use ContextPredicate::*;
-
- assert_eq!(
- ContextPredicate::parse("a && (b == c || d != e)")?,
- And(
- Box::new(Identifier("a".into())),
- Box::new(Or(
- Box::new(Equal("b".into(), "c".into())),
- Box::new(NotEqual("d".into(), "e".into())),
- ))
- )
- );
-
- assert_eq!(
- ContextPredicate::parse("!a")?,
- Not(Box::new(Identifier("a".into())),)
- );
-
- Ok(())
- }
-
- #[test]
- fn test_context_predicate_eval() -> Result<()> {
- let predicate = ContextPredicate::parse("a && b || c == d")?;
-
- let mut context = Context::default();
- context.set.insert("a".into());
- assert!(!predicate.eval(&context));
-
- context.set.insert("b".into());
- assert!(predicate.eval(&context));
-
- context.set.remove("b");
- context.map.insert("c".into(), "x".into());
- assert!(!predicate.eval(&context));
-
- context.map.insert("c".into(), "d".into());
- assert!(predicate.eval(&context));
-
- let predicate = ContextPredicate::parse("!a")?;
- assert!(predicate.eval(&Context::default()));
-
- Ok(())
- }
-
- #[test]
- fn test_matcher() -> Result<()> {
- #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
- pub struct A(pub String);
- impl_actions!(test, [A]);
- actions!(test, [B, Ab]);
-
- #[derive(Clone, Debug, Eq, PartialEq)]
- struct ActionArg {
- a: &'static str,
- }
-
- let keymap = Keymap::new(vec![
- Binding::new("a", A("x".to_string()), Some("a")),
- Binding::new("b", B, Some("a")),
- Binding::new("a b", Ab, Some("a || b")),
- ]);
-
- let mut ctx_a = Context::default();
- ctx_a.set.insert("a".into());
-
- let mut ctx_b = Context::default();
- ctx_b.set.insert("b".into());
-
- let mut matcher = Matcher::new(keymap);
-
- // Basic match
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
- MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
- );
- matcher.clear_pending();
-
- // Multi-keystroke match
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
- MatchResult::Pending
- );
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
- MatchResult::Matches(vec![(1, Box::new(Ab))])
- );
- matcher.clear_pending();
-
- // Failed matches don't interfere with matching subsequent keys
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, ctx_a.clone())]),
- MatchResult::None
- );
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
- MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
- );
- matcher.clear_pending();
-
- // Pending keystrokes are cleared when the context changes
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
- MatchResult::Pending
- );
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_a.clone())]),
- MatchResult::None
- );
- matcher.clear_pending();
-
- let mut ctx_c = Context::default();
- ctx_c.set.insert("c".into());
-
- // Pending keystrokes are maintained per-view
- assert_eq!(
- matcher.push_keystroke(
- Keystroke::parse("a")?,
- vec![(1, ctx_b.clone()), (2, ctx_c.clone())]
- ),
- MatchResult::Pending
- );
- assert_eq!(
- matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
- MatchResult::Matches(vec![(1, Box::new(Ab))])
- );
-
- Ok(())
- }
-}
@@ -0,0 +1,459 @@
+mod binding;
+mod keymap;
+mod keymap_context;
+mod keystroke;
+
+use std::{any::TypeId, fmt::Debug};
+
+use collections::HashMap;
+use serde::Deserialize;
+use smallvec::SmallVec;
+
+use crate::{impl_actions, Action};
+
+pub use binding::{Binding, BindingMatchResult};
+pub use keymap::Keymap;
+pub use keymap_context::{KeymapContext, KeymapContextPredicate};
+pub use keystroke::Keystroke;
+
+#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
+pub struct KeyPressed {
+ #[serde(default)]
+ pub keystroke: Keystroke,
+}
+
+impl_actions!(gpui, [KeyPressed]);
+
+pub struct KeymapMatcher {
+ pending_views: HashMap<usize, KeymapContext>,
+ pending_keystrokes: Vec<Keystroke>,
+ keymap: Keymap,
+}
+
+impl KeymapMatcher {
+ pub fn new(keymap: Keymap) -> Self {
+ Self {
+ pending_views: Default::default(),
+ pending_keystrokes: Vec::new(),
+ keymap,
+ }
+ }
+
+ pub fn set_keymap(&mut self, keymap: Keymap) {
+ self.clear_pending();
+ self.keymap = keymap;
+ }
+
+ pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
+ self.clear_pending();
+ self.keymap.add_bindings(bindings);
+ }
+
+ pub fn clear_bindings(&mut self) {
+ self.clear_pending();
+ self.keymap.clear();
+ }
+
+ pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
+ self.keymap.bindings_for_action_type(action_type)
+ }
+
+ pub fn clear_pending(&mut self) {
+ self.pending_keystrokes.clear();
+ self.pending_views.clear();
+ }
+
+ pub fn has_pending_keystrokes(&self) -> bool {
+ !self.pending_keystrokes.is_empty()
+ }
+
+ pub fn push_keystroke(
+ &mut self,
+ keystroke: Keystroke,
+ dispatch_path: Vec<(usize, KeymapContext)>,
+ ) -> MatchResult {
+ let mut any_pending = false;
+ let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
+
+ let first_keystroke = self.pending_keystrokes.is_empty();
+ self.pending_keystrokes.push(keystroke.clone());
+
+ for (view_id, context) in dispatch_path {
+ // Don't require pending view entry if there are no pending keystrokes
+ if !first_keystroke && !self.pending_views.contains_key(&view_id) {
+ continue;
+ }
+
+ // If there is a previous view context, invalidate that view if it
+ // has changed
+ if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
+ if previous_view_context != context {
+ continue;
+ }
+ }
+
+ // Find the bindings which map the pending keystrokes and current context
+ for binding in self.keymap.bindings().iter().rev() {
+ match binding.match_keys_and_context(&self.pending_keystrokes, &context) {
+ BindingMatchResult::Complete(mut action) => {
+ // Swap in keystroke for special KeyPressed action
+ if action.name() == "KeyPressed" && action.namespace() == "gpui" {
+ action = Box::new(KeyPressed {
+ keystroke: keystroke.clone(),
+ });
+ }
+ matched_bindings.push((view_id, action))
+ }
+ BindingMatchResult::Partial => {
+ self.pending_views.insert(view_id, context.clone());
+ any_pending = true;
+ }
+ _ => {}
+ }
+ }
+ }
+
+ if !any_pending {
+ self.clear_pending();
+ }
+
+ if !matched_bindings.is_empty() {
+ MatchResult::Matches(matched_bindings)
+ } else if any_pending {
+ MatchResult::Pending
+ } else {
+ MatchResult::None
+ }
+ }
+
+ pub fn keystrokes_for_action(
+ &self,
+ action: &dyn Action,
+ context: &KeymapContext,
+ ) -> Option<SmallVec<[Keystroke; 2]>> {
+ self.keymap
+ .bindings()
+ .iter()
+ .rev()
+ .find_map(|binding| binding.keystrokes_for_action(action, context))
+ }
+}
+
+impl Default for KeymapMatcher {
+ fn default() -> Self {
+ Self::new(Keymap::default())
+ }
+}
+
+pub enum MatchResult {
+ None,
+ Pending,
+ Matches(Vec<(usize, Box<dyn Action>)>),
+}
+
+impl Debug for MatchResult {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ MatchResult::None => f.debug_struct("MatchResult::None").finish(),
+ MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
+ MatchResult::Matches(matches) => f
+ .debug_list()
+ .entries(
+ matches
+ .iter()
+ .map(|(view_id, action)| format!("{view_id}, {}", action.name())),
+ )
+ .finish(),
+ }
+ }
+}
+
+impl PartialEq for MatchResult {
+ fn eq(&self, other: &Self) -> bool {
+ match (self, other) {
+ (MatchResult::None, MatchResult::None) => true,
+ (MatchResult::Pending, MatchResult::Pending) => true,
+ (MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
+ matches.len() == other_matches.len()
+ && matches.iter().zip(other_matches.iter()).all(
+ |((view_id, action), (other_view_id, other_action))| {
+ view_id == other_view_id && action.eq(other_action.as_ref())
+ },
+ )
+ }
+ _ => false,
+ }
+ }
+}
+
+impl Eq for MatchResult {}
+
+impl Clone for MatchResult {
+ fn clone(&self) -> Self {
+ match self {
+ MatchResult::None => MatchResult::None,
+ MatchResult::Pending => MatchResult::Pending,
+ MatchResult::Matches(matches) => MatchResult::Matches(
+ matches
+ .iter()
+ .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
+ .collect(),
+ ),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use anyhow::Result;
+ use serde::Deserialize;
+
+ use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
+
+ use super::*;
+
+ #[test]
+ fn test_push_keystroke() -> Result<()> {
+ actions!(test, [B, AB, C, D, DA]);
+
+ let mut context1 = KeymapContext::default();
+ context1.set.insert("1".into());
+
+ let mut context2 = KeymapContext::default();
+ context2.set.insert("2".into());
+
+ let dispatch_path = vec![(2, context2), (1, context1)];
+
+ let keymap = Keymap::new(vec![
+ Binding::new("a b", AB, Some("1")),
+ Binding::new("b", B, Some("2")),
+ Binding::new("c", C, Some("2")),
+ Binding::new("d", D, Some("1")),
+ Binding::new("d", D, Some("2")),
+ Binding::new("d a", DA, Some("2")),
+ ]);
+
+ let mut matcher = KeymapMatcher::new(keymap);
+
+ // Binding with pending prefix always takes precedence
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+ MatchResult::Pending,
+ );
+ // B alone doesn't match because a was pending, so AB is returned instead
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+ MatchResult::Matches(vec![(1, Box::new(AB))]),
+ );
+ assert!(!matcher.has_pending_keystrokes());
+
+ // Without an a prefix, B is dispatched like expected
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
+ MatchResult::Matches(vec![(2, Box::new(B))]),
+ );
+ assert!(!matcher.has_pending_keystrokes());
+
+ // If a is prefixed, C will not be dispatched because there
+ // was a pending binding for it
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+ MatchResult::Pending,
+ );
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
+ MatchResult::None,
+ );
+ assert!(!matcher.has_pending_keystrokes());
+
+ // If a single keystroke matches multiple bindings in the tree
+ // all of them are returned so that we can fallback if the action
+ // handler decides to propagate the action
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
+ MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
+ );
+ // If none of the d action handlers consume the binding, a pending
+ // binding may then be used
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
+ MatchResult::Matches(vec![(2, Box::new(DA))]),
+ );
+ assert!(!matcher.has_pending_keystrokes());
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_keystroke_parsing() -> Result<()> {
+ assert_eq!(
+ Keystroke::parse("ctrl-p")?,
+ Keystroke {
+ key: "p".into(),
+ ctrl: true,
+ alt: false,
+ shift: false,
+ cmd: false,
+ function: false,
+ }
+ );
+
+ assert_eq!(
+ Keystroke::parse("alt-shift-down")?,
+ Keystroke {
+ key: "down".into(),
+ ctrl: false,
+ alt: true,
+ shift: true,
+ cmd: false,
+ function: false,
+ }
+ );
+
+ assert_eq!(
+ Keystroke::parse("shift-cmd--")?,
+ Keystroke {
+ key: "-".into(),
+ ctrl: false,
+ alt: false,
+ shift: true,
+ cmd: true,
+ function: false,
+ }
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_context_predicate_parsing() -> Result<()> {
+ use KeymapContextPredicate::*;
+
+ assert_eq!(
+ KeymapContextPredicate::parse("a && (b == c || d != e)")?,
+ And(
+ Box::new(Identifier("a".into())),
+ Box::new(Or(
+ Box::new(Equal("b".into(), "c".into())),
+ Box::new(NotEqual("d".into(), "e".into())),
+ ))
+ )
+ );
+
+ assert_eq!(
+ KeymapContextPredicate::parse("!a")?,
+ Not(Box::new(Identifier("a".into())),)
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_context_predicate_eval() -> Result<()> {
+ let predicate = KeymapContextPredicate::parse("a && b || c == d")?;
+
+ let mut context = KeymapContext::default();
+ context.set.insert("a".into());
+ assert!(!predicate.eval(&context));
+
+ context.set.insert("b".into());
+ assert!(predicate.eval(&context));
+
+ context.set.remove("b");
+ context.map.insert("c".into(), "x".into());
+ assert!(!predicate.eval(&context));
+
+ context.map.insert("c".into(), "d".into());
+ assert!(predicate.eval(&context));
+
+ let predicate = KeymapContextPredicate::parse("!a")?;
+ assert!(predicate.eval(&KeymapContext::default()));
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_matcher() -> Result<()> {
+ #[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
+ pub struct A(pub String);
+ impl_actions!(test, [A]);
+ actions!(test, [B, Ab]);
+
+ #[derive(Clone, Debug, Eq, PartialEq)]
+ struct ActionArg {
+ a: &'static str,
+ }
+
+ let keymap = Keymap::new(vec![
+ Binding::new("a", A("x".to_string()), Some("a")),
+ Binding::new("b", B, Some("a")),
+ Binding::new("a b", Ab, Some("a || b")),
+ ]);
+
+ let mut context_a = KeymapContext::default();
+ context_a.set.insert("a".into());
+
+ let mut context_b = KeymapContext::default();
+ context_b.set.insert("b".into());
+
+ let mut matcher = KeymapMatcher::new(keymap);
+
+ // Basic match
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+ MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
+ );
+ matcher.clear_pending();
+
+ // Multi-keystroke match
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+ MatchResult::Pending
+ );
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+ MatchResult::Matches(vec![(1, Box::new(Ab))])
+ );
+ matcher.clear_pending();
+
+ // Failed matches don't interfere with matching subsequent keys
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
+ MatchResult::None
+ );
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
+ MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
+ );
+ matcher.clear_pending();
+
+ // Pending keystrokes are cleared when the context changes
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
+ MatchResult::Pending
+ );
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
+ MatchResult::None
+ );
+ matcher.clear_pending();
+
+ let mut context_c = KeymapContext::default();
+ context_c.set.insert("c".into());
+
+ // Pending keystrokes are maintained per-view
+ assert_eq!(
+ matcher.push_keystroke(
+ Keystroke::parse("a")?,
+ vec![(1, context_b.clone()), (2, context_c.clone())]
+ ),
+ MatchResult::Pending
+ );
+ assert_eq!(
+ matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
+ MatchResult::Matches(vec![(1, Box::new(Ab))])
+ );
+
+ Ok(())
+ }
+}
@@ -0,0 +1,104 @@
+use anyhow::Result;
+use smallvec::SmallVec;
+
+use crate::Action;
+
+use super::{KeymapContext, KeymapContextPredicate, Keystroke};
+
+pub struct Binding {
+ action: Box<dyn Action>,
+ keystrokes: Option<SmallVec<[Keystroke; 2]>>,
+ context_predicate: Option<KeymapContextPredicate>,
+}
+
+impl Binding {
+ pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
+ Self::load(keystrokes, Box::new(action), context).unwrap()
+ }
+
+ pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
+ let context = if let Some(context) = context {
+ Some(KeymapContextPredicate::parse(context)?)
+ } else {
+ None
+ };
+
+ let keystrokes = if keystrokes == "*" {
+ None // Catch all context
+ } else {
+ Some(
+ keystrokes
+ .split_whitespace()
+ .map(Keystroke::parse)
+ .collect::<Result<_>>()?,
+ )
+ };
+
+ Ok(Self {
+ keystrokes,
+ action,
+ context_predicate: context,
+ })
+ }
+
+ fn match_context(&self, context: &KeymapContext) -> bool {
+ self.context_predicate
+ .as_ref()
+ .map(|predicate| predicate.eval(context))
+ .unwrap_or(true)
+ }
+
+ pub fn match_keys_and_context(
+ &self,
+ pending_keystrokes: &Vec<Keystroke>,
+ context: &KeymapContext,
+ ) -> BindingMatchResult {
+ if self
+ .keystrokes
+ .as_ref()
+ .map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
+ .unwrap_or(true)
+ && self.match_context(context)
+ {
+ // If the binding is completed, push it onto the matches list
+ if self
+ .keystrokes
+ .as_ref()
+ .map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
+ .unwrap_or(true)
+ {
+ BindingMatchResult::Complete(self.action.boxed_clone())
+ } else {
+ BindingMatchResult::Partial
+ }
+ } else {
+ BindingMatchResult::Fail
+ }
+ }
+
+ pub fn keystrokes_for_action(
+ &self,
+ action: &dyn Action,
+ context: &KeymapContext,
+ ) -> Option<SmallVec<[Keystroke; 2]>> {
+ if self.action.eq(action) && self.match_context(context) {
+ self.keystrokes.clone()
+ } else {
+ None
+ }
+ }
+
+ pub fn keystrokes(&self) -> Option<&[Keystroke]> {
+ self.keystrokes.as_deref()
+ }
+
+ pub fn action(&self) -> &dyn Action {
+ self.action.as_ref()
+ }
+}
+
+pub enum BindingMatchResult {
+ Complete(Box<dyn Action>),
+ Partial,
+ Fail,
+}
@@ -0,0 +1,61 @@
+use smallvec::SmallVec;
+use std::{
+ any::{Any, TypeId},
+ collections::HashMap,
+};
+
+use super::Binding;
+
+#[derive(Default)]
+pub struct Keymap {
+ bindings: Vec<Binding>,
+ binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
+}
+
+impl Keymap {
+ pub fn new(bindings: Vec<Binding>) -> Self {
+ let mut binding_indices_by_action_type = HashMap::new();
+ for (ix, binding) in bindings.iter().enumerate() {
+ binding_indices_by_action_type
+ .entry(binding.action().type_id())
+ .or_insert_with(SmallVec::new)
+ .push(ix);
+ }
+
+ Self {
+ binding_indices_by_action_type,
+ bindings,
+ }
+ }
+
+ pub(crate) fn bindings_for_action_type(
+ &self,
+ action_type: TypeId,
+ ) -> impl Iterator<Item = &'_ Binding> {
+ self.binding_indices_by_action_type
+ .get(&action_type)
+ .map(SmallVec::as_slice)
+ .unwrap_or(&[])
+ .iter()
+ .map(|ix| &self.bindings[*ix])
+ }
+
+ pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
+ for binding in bindings {
+ self.binding_indices_by_action_type
+ .entry(binding.action().type_id())
+ .or_default()
+ .push(self.bindings.len());
+ self.bindings.push(binding);
+ }
+ }
+
+ pub(crate) fn clear(&mut self) {
+ self.bindings.clear();
+ self.binding_indices_by_action_type.clear();
+ }
+
+ pub fn bindings(&self) -> &Vec<Binding> {
+ &self.bindings
+ }
+}
@@ -0,0 +1,123 @@
+use anyhow::anyhow;
+
+use collections::{HashMap, HashSet};
+use tree_sitter::{Language, Node, Parser};
+
+extern "C" {
+ fn tree_sitter_context_predicate() -> Language;
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct KeymapContext {
+ pub set: HashSet<String>,
+ pub map: HashMap<String, String>,
+}
+
+impl KeymapContext {
+ pub fn extend(&mut self, other: &Self) {
+ for v in &other.set {
+ self.set.insert(v.clone());
+ }
+ for (k, v) in &other.map {
+ self.map.insert(k.clone(), v.clone());
+ }
+ }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum KeymapContextPredicate {
+ Identifier(String),
+ Equal(String, String),
+ NotEqual(String, String),
+ Not(Box<KeymapContextPredicate>),
+ And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
+ Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
+}
+
+impl KeymapContextPredicate {
+ pub fn parse(source: &str) -> anyhow::Result<Self> {
+ let mut parser = Parser::new();
+ let language = unsafe { tree_sitter_context_predicate() };
+ parser.set_language(language).unwrap();
+ let source = source.as_bytes();
+ let tree = parser.parse(source, None).unwrap();
+ Self::from_node(tree.root_node(), source)
+ }
+
+ fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
+ let parse_error = "error parsing context predicate";
+ let kind = node.kind();
+
+ match kind {
+ "source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
+ "identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
+ "not" => {
+ let child = Self::from_node(
+ node.child_by_field_name("expression")
+ .ok_or_else(|| anyhow!(parse_error))?,
+ source,
+ )?;
+ Ok(Self::Not(Box::new(child)))
+ }
+ "and" | "or" => {
+ let left = Box::new(Self::from_node(
+ node.child_by_field_name("left")
+ .ok_or_else(|| anyhow!(parse_error))?,
+ source,
+ )?);
+ let right = Box::new(Self::from_node(
+ node.child_by_field_name("right")
+ .ok_or_else(|| anyhow!(parse_error))?,
+ source,
+ )?);
+ if kind == "and" {
+ Ok(Self::And(left, right))
+ } else {
+ Ok(Self::Or(left, right))
+ }
+ }
+ "equal" | "not_equal" => {
+ let left = node
+ .child_by_field_name("left")
+ .ok_or_else(|| anyhow!(parse_error))?
+ .utf8_text(source)?
+ .into();
+ let right = node
+ .child_by_field_name("right")
+ .ok_or_else(|| anyhow!(parse_error))?
+ .utf8_text(source)?
+ .into();
+ if kind == "equal" {
+ Ok(Self::Equal(left, right))
+ } else {
+ Ok(Self::NotEqual(left, right))
+ }
+ }
+ "parenthesized" => Self::from_node(
+ node.child_by_field_name("expression")
+ .ok_or_else(|| anyhow!(parse_error))?,
+ source,
+ ),
+ _ => Err(anyhow!(parse_error)),
+ }
+ }
+
+ pub fn eval(&self, context: &KeymapContext) -> bool {
+ match self {
+ Self::Identifier(name) => context.set.contains(name.as_str()),
+ Self::Equal(left, right) => context
+ .map
+ .get(left)
+ .map(|value| value == right)
+ .unwrap_or(false),
+ Self::NotEqual(left, right) => context
+ .map
+ .get(left)
+ .map(|value| value != right)
+ .unwrap_or(true),
+ Self::Not(pred) => !pred.eval(context),
+ Self::And(left, right) => left.eval(context) && right.eval(context),
+ Self::Or(left, right) => left.eval(context) || right.eval(context),
+ }
+ }
+}
@@ -0,0 +1,97 @@
+use std::fmt::Write;
+
+use anyhow::anyhow;
+use serde::Deserialize;
+
+#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)]
+pub struct Keystroke {
+ pub ctrl: bool,
+ pub alt: bool,
+ pub shift: bool,
+ pub cmd: bool,
+ pub function: bool,
+ pub key: String,
+}
+
+impl Keystroke {
+ pub fn parse(source: &str) -> anyhow::Result<Self> {
+ let mut ctrl = false;
+ let mut alt = false;
+ let mut shift = false;
+ let mut cmd = false;
+ let mut function = false;
+ let mut key = None;
+
+ let mut components = source.split('-').peekable();
+ while let Some(component) = components.next() {
+ match component {
+ "ctrl" => ctrl = true,
+ "alt" => alt = true,
+ "shift" => shift = true,
+ "cmd" => cmd = true,
+ "fn" => function = true,
+ _ => {
+ if let Some(component) = components.peek() {
+ if component.is_empty() && source.ends_with('-') {
+ key = Some(String::from("-"));
+ break;
+ } else {
+ return Err(anyhow!("Invalid keystroke `{}`", source));
+ }
+ } else {
+ key = Some(String::from(component));
+ }
+ }
+ }
+ }
+
+ let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
+
+ Ok(Keystroke {
+ ctrl,
+ alt,
+ shift,
+ cmd,
+ function,
+ key,
+ })
+ }
+
+ pub fn modified(&self) -> bool {
+ self.ctrl || self.alt || self.shift || self.cmd
+ }
+}
+
+impl std::fmt::Display for Keystroke {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ if self.ctrl {
+ f.write_char('^')?;
+ }
+ if self.alt {
+ f.write_char('β')?;
+ }
+ if self.cmd {
+ f.write_char('β')?;
+ }
+ if self.shift {
+ f.write_char('β§')?;
+ }
+ let key = match self.key.as_str() {
+ "backspace" => 'β«',
+ "up" => 'β',
+ "down" => 'β',
+ "left" => 'β',
+ "right" => 'β',
+ "tab" => 'β₯',
+ "escape" => 'β',
+ key => {
+ if key.len() == 1 {
+ key.chars().next().unwrap().to_ascii_uppercase()
+ } else {
+ return f.write_str(key);
+ }
+ }
+ };
+ f.write_char(key)
+ }
+}
@@ -14,7 +14,7 @@ use crate::{
rect::{RectF, RectI},
vector::Vector2F,
},
- keymap,
+ keymap_matcher::KeymapMatcher,
text_layout::{LineLayout, RunStyle},
Action, ClipboardItem, Menu, Scene,
};
@@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform {
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
- fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
+ fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
fn prompt_for_paths(
&self,
options: PathPromptOptions,
@@ -2,7 +2,7 @@ use std::ops::Deref;
use pathfinder_geometry::vector::vec2f;
-use crate::{geometry::vector::Vector2F, keymap::Keystroke};
+use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
#[derive(Clone, Debug)]
pub struct KeyDownEvent {
@@ -1,6 +1,6 @@
use crate::{
geometry::vector::vec2f,
- keymap::Keystroke,
+ keymap_matcher::Keystroke,
platform::{Event, NavigationDirection},
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
@@ -47,6 +47,8 @@ pub fn key_to_native(key: &str) -> Cow<str> {
"right" => NSRightArrowFunctionKey,
"pageup" => NSPageUpFunctionKey,
"pagedown" => NSPageDownFunctionKey,
+ "home" => NSHomeFunctionKey,
+ "end" => NSEndFunctionKey,
"delete" => NSDeleteFunctionKey,
"f1" => NSF1FunctionKey,
"f2" => NSF2FunctionKey,
@@ -258,6 +260,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
Some(NSRightArrowFunctionKey) => "right".to_string(),
Some(NSPageUpFunctionKey) => "pageup".to_string(),
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
+ Some(NSHomeFunctionKey) => "home".to_string(),
+ Some(NSEndFunctionKey) => "end".to_string(),
Some(NSDeleteFunctionKey) => "delete".to_string(),
Some(NSF1FunctionKey) => "f1".to_string(),
Some(NSF2FunctionKey) => "f2".to_string(),
@@ -3,7 +3,8 @@ use super::{
FontSystem, Window,
};
use crate::{
- executor, keymap,
+ executor,
+ keymap_matcher::KeymapMatcher,
platform::{self, CursorStyle},
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
};
@@ -135,7 +136,7 @@ impl MacForegroundPlatform {
menus: Vec<Menu>,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
- keystroke_matcher: &keymap::Matcher,
+ keystroke_matcher: &KeymapMatcher,
) -> id {
let application_menu = NSMenu::new(nil).autorelease();
application_menu.setDelegate_(delegate);
@@ -172,7 +173,7 @@ impl MacForegroundPlatform {
item: MenuItem,
delegate: id,
actions: &mut Vec<Box<dyn Action>>,
- keystroke_matcher: &keymap::Matcher,
+ keystroke_matcher: &KeymapMatcher,
) -> id {
match item {
MenuItem::Separator => NSMenuItem::separatorItem(nil),
@@ -183,7 +184,7 @@ impl MacForegroundPlatform {
.map(|binding| binding.keystrokes());
let item;
- if let Some(keystrokes) = keystrokes {
+ if let Some(keystrokes) = keystrokes.flatten() {
if keystrokes.len() == 1 {
let keystroke = &keystrokes[0];
let mut mask = NSEventModifierFlags::empty();
@@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
self.0.borrow_mut().validate_menu_command = Some(callback);
}
- fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
+ fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
unsafe {
let app: id = msg_send![APP_CLASS, sharedApplication];
let mut state = self.0.borrow_mut();
@@ -647,7 +648,7 @@ impl platform::Platform for MacPlatform {
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
- let mut result = CFTypeRef::from(ptr::null_mut());
+ let mut result = CFTypeRef::from(ptr::null());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
@@ -4,7 +4,7 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- keymap::Keystroke,
+ keymap_matcher::Keystroke,
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
platform::{
self,
@@ -4,7 +4,8 @@ use crate::{
rect::RectF,
vector::{vec2f, Vector2F},
},
- keymap, Action, ClipboardItem,
+ keymap_matcher::KeymapMatcher,
+ Action, ClipboardItem,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -84,7 +85,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
- fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
+ fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
fn prompt_for_paths(
&self,
@@ -4,7 +4,7 @@ use crate::{
font_cache::FontCache,
geometry::rect::RectF,
json::{self, ToJson},
- keymap::Keystroke,
+ keymap_matcher::Keystroke,
platform::{CursorStyle, Event},
scene::{
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
@@ -162,11 +162,9 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
- match last_segment.map(|s| s.ident.to_string()).as_deref() {
- Some("StdRng") => {
- inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
- }
- _ => {}
+
+ if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
+ inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
}
} else {
inner_fn_args.extend(quote!(cx,));
@@ -682,7 +682,6 @@ impl Buffer {
task
}
- #[cfg(any(test, feature = "test-support"))]
pub fn diff_base(&self) -> Option<&str> {
self.diff_base.as_deref()
}
@@ -289,6 +289,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
);
buffer.update(cx, |buf, cx| {
+ buf.undo(cx);
+ buf.undo(cx);
+ buf.undo(cx);
buf.undo(cx);
assert_eq!(buf.text(), "fn a() {}");
assert!(buf.is_parsing());
@@ -304,6 +307,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
);
buffer.update(cx, |buf, cx| {
+ buf.redo(cx);
+ buf.redo(cx);
+ buf.redo(cx);
buf.redo(cx);
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
assert!(buf.is_parsing());
@@ -1022,8 +1028,11 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
.unindent()
);
+ // Grouping is disabled in tests, so we need 2 undos
+ buffer.undo(cx); // Undo the auto-indent
+ buffer.undo(cx); // Undo the original edit
+
// Insert the block at a deeper indent level. The entire block is outdented.
- buffer.undo(cx);
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
buffer.edit(
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
@@ -5,7 +5,7 @@ use std::{
process::Command,
};
-const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
+const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge";
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -61,8 +61,8 @@ fn build_bridge(swift_target: &SwiftTarget) {
let swift_package_root = swift_package_root();
if !Command::new("swift")
.arg("build")
- .args(&["--configuration", &env::var("PROFILE").unwrap()])
- .args(&["--triple", &swift_target.target.triple])
+ .args(["--configuration", &env::var("PROFILE").unwrap()])
+ .args(["--triple", &swift_target.target.triple])
.current_dir(&swift_package_root)
.status()
.unwrap()
@@ -116,7 +116,7 @@ fn get_swift_target() -> SwiftTarget {
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
let swift_target_info_str = Command::new("swift")
- .args(&["-target", &target, "-print-target-info"])
+ .args(["-target", &target, "-print-target-info"])
.output()
.unwrap()
.stdout;
@@ -143,7 +143,7 @@ fn copy_dir(source: &Path, destination: &Path) {
assert!(
Command::new("cp")
.arg("-R")
- .args(&[source, destination])
+ .args([source, destination])
.status()
.unwrap()
.success(),
@@ -1,5 +1,5 @@
use futures::StreamExt;
-use gpui::{actions, keymap::Binding, Menu, MenuItem};
+use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem};
use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
use live_kit_server::token::{self, VideoGrant};
use log::LevelFilter;
@@ -3,7 +3,7 @@ use std::{env, path::PathBuf, process::Command};
fn main() {
let sdk_path = String::from_utf8(
Command::new("xcrun")
- .args(&["--sdk", "macosx", "--show-sdk-path"])
+ .args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.unwrap()
.stdout,
@@ -113,9 +113,9 @@ pub mod core_video {
let mut this = ptr::null();
let result = CVMetalTextureCacheCreate(
kCFAllocatorDefault,
- ptr::null_mut(),
+ ptr::null(),
metal_device,
- ptr::null_mut(),
+ ptr::null(),
&mut this,
);
if result == kCVReturnSuccess {
@@ -192,7 +192,7 @@ pub mod core_video {
pub fn as_texture_ref(&self) -> &metal::TextureRef {
unsafe {
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
- &metal::TextureRef::from_ptr(texture as *mut _)
+ metal::TextureRef::from_ptr(texture as *mut _)
}
}
}
@@ -2,7 +2,7 @@ use editor::Editor;
use gpui::{
elements::*,
geometry::vector::{vec2f, Vector2F},
- keymap,
+ keymap_matcher::KeymapContext,
platform::CursorStyle,
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
@@ -124,7 +124,7 @@ impl<D: PickerDelegate> View for Picker<D> {
.named("picker")
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
@@ -5189,20 +5189,27 @@ impl Project {
let operations = buffer.serialize_ops(Some(remote_version), cx);
let client = this.client.clone();
- let file = buffer.file().cloned();
+ if let Some(file) = buffer.file() {
+ client
+ .send(proto::UpdateBufferFile {
+ project_id,
+ buffer_id: buffer_id as u64,
+ file: Some(file.to_proto()),
+ })
+ .log_err();
+ }
+
+ client
+ .send(proto::UpdateDiffBase {
+ project_id,
+ buffer_id: buffer_id as u64,
+ diff_base: buffer.diff_base().map(Into::into),
+ })
+ .log_err();
+
cx.background()
.spawn(
async move {
- if let Some(file) = file {
- client
- .send(proto::UpdateBufferFile {
- project_id,
- buffer_id: buffer_id as u64,
- file: Some(file.to_proto()),
- })
- .log_err();
- }
-
let operations = operations.await;
for chunk in split_operations(operations) {
client
@@ -10,7 +10,8 @@ use gpui::{
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
},
geometry::vector::Vector2F,
- impl_internal_actions, keymap,
+ impl_internal_actions,
+ keymap_matcher::KeymapContext,
platform::CursorStyle,
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
@@ -1301,7 +1302,7 @@ impl View for ProjectPanel {
.boxed()
}
- fn keymap_context(&self, _: &AppContext) -> keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut cx = Self::default_keymap_context();
cx.set.insert("menu".into());
cx
@@ -146,6 +146,7 @@ impl PickerDelegate for RecentProjectsView {
.matches
.iter()
.enumerate()
+ .rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(ix, _)| ix)
.unwrap_or(0);
@@ -23,7 +23,7 @@ pub fn random_token() -> String {
for byte in token_bytes.iter_mut() {
*byte = rng.gen();
}
- base64::encode_config(&token_bytes, base64::URL_SAFE)
+ base64::encode_config(token_bytes, base64::URL_SAFE)
}
impl PublicKey {
@@ -106,73 +106,79 @@ impl View for BufferSearchBar {
.with_child(
Flex::row()
.with_child(
- ChildView::new(&self.query_editor, cx)
- .aligned()
- .left()
- .flex(1., true)
- .boxed(),
- )
- .with_children(self.active_searchable_item.as_ref().and_then(
- |searchable_item| {
- let matches = self
- .seachable_items_with_matches
- .get(&searchable_item.downgrade())?;
- let message = if let Some(match_ix) = self.active_match_index {
- format!("{}/{}", match_ix + 1, matches.len())
- } else {
- "No matches".to_string()
- };
-
- Some(
- Label::new(message, theme.search.match_index.text.clone())
- .contained()
- .with_style(theme.search.match_index.container)
+ Flex::row()
+ .with_child(
+ ChildView::new(&self.query_editor, cx)
.aligned()
+ .left()
+ .flex(1., true)
.boxed(),
)
- },
- ))
- .contained()
- .with_style(editor_container)
- .aligned()
- .constrained()
- .with_min_width(theme.search.editor.min_width)
- .with_max_width(theme.search.editor.max_width)
- .flex(1., false)
- .boxed(),
- )
- .with_child(
- Flex::row()
- .with_child(self.render_nav_button("<", Direction::Prev, cx))
- .with_child(self.render_nav_button(">", Direction::Next, cx))
- .aligned()
- .boxed(),
- )
- .with_child(
- Flex::row()
- .with_children(self.render_search_option(
- supported_options.case,
- "Case",
- SearchOption::CaseSensitive,
- cx,
- ))
- .with_children(self.render_search_option(
- supported_options.word,
- "Word",
- SearchOption::WholeWord,
- cx,
- ))
- .with_children(self.render_search_option(
- supported_options.regex,
- "Regex",
- SearchOption::Regex,
- cx,
- ))
- .contained()
- .with_style(theme.search.option_button_group)
- .aligned()
+ .with_children(self.active_searchable_item.as_ref().and_then(
+ |searchable_item| {
+ let matches = self
+ .seachable_items_with_matches
+ .get(&searchable_item.downgrade())?;
+ let message = if let Some(match_ix) = self.active_match_index {
+ format!("{}/{}", match_ix + 1, matches.len())
+ } else {
+ "No matches".to_string()
+ };
+
+ Some(
+ Label::new(message, theme.search.match_index.text.clone())
+ .contained()
+ .with_style(theme.search.match_index.container)
+ .aligned()
+ .boxed(),
+ )
+ },
+ ))
+ .contained()
+ .with_style(editor_container)
+ .aligned()
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .flex(1., false)
+ .boxed(),
+ )
+ .with_child(
+ Flex::row()
+ .with_child(self.render_nav_button("<", Direction::Prev, cx))
+ .with_child(self.render_nav_button(">", Direction::Next, cx))
+ .aligned()
+ .boxed(),
+ )
+ .with_child(
+ Flex::row()
+ .with_children(self.render_search_option(
+ supported_options.case,
+ "Case",
+ SearchOption::CaseSensitive,
+ cx,
+ ))
+ .with_children(self.render_search_option(
+ supported_options.word,
+ "Word",
+ SearchOption::WholeWord,
+ cx,
+ ))
+ .with_children(self.render_search_option(
+ supported_options.regex,
+ "Regex",
+ SearchOption::Regex,
+ cx,
+ ))
+ .contained()
+ .with_style(theme.search.option_button_group)
+ .aligned()
+ .boxed(),
+ )
+ .flex(1., true)
.boxed(),
)
+ .with_child(self.render_close_button(&theme.search, cx))
.contained()
.with_style(theme.search.container)
.named("search bar")
@@ -325,7 +331,7 @@ impl BufferSearchBar {
let is_active = self.is_search_option_enabled(option);
Some(
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
- let style = &cx
+ let style = cx
.global::<Settings>()
.theme
.search
@@ -373,7 +379,7 @@ impl BufferSearchBar {
enum NavButton {}
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
- let style = &cx
+ let style = cx
.global::<Settings>()
.theme
.search
@@ -399,6 +405,38 @@ impl BufferSearchBar {
.boxed()
}
+ fn render_close_button(
+ &self,
+ theme: &theme::Search,
+ cx: &mut RenderContext<Self>,
+ ) -> ElementBox {
+ let action = Box::new(Dismiss);
+ let tooltip = "Dismiss Buffer Search";
+ let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
+
+ enum CloseButton {}
+ MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
+ let style = theme.dismiss_button.style_for(state, false);
+ Svg::new("icons/x_mark_8.svg")
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .constrained()
+ .with_width(style.button_width)
+ .contained()
+ .with_style(style.container)
+ .boxed()
+ })
+ .on_click(MouseButton::Left, {
+ let action = action.boxed_clone();
+ move |_, cx| cx.dispatch_any_action(action.boxed_clone())
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
+ .boxed()
+ }
+
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
@@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings};
use anyhow::{Context, Result};
use assets::Assets;
use collections::BTreeMap;
-use gpui::{keymap::Binding, MutableAppContext};
+use gpui::{keymap_matcher::Binding, MutableAppContext};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@@ -62,7 +62,7 @@ fn parse_snippet<'a>(
}
}
Some(_) => {
- let chunk_end = source.find(&['}', '$', '\\']).unwrap_or(source.len());
+ let chunk_end = source.find(['}', '$', '\\']).unwrap_or(source.len());
let (chunk, rest) = source.split_at(chunk_end);
text.push_str(chunk);
source = rest;
@@ -20,7 +20,7 @@ unsafe impl Send for Connection {}
impl Connection {
pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
let mut connection = Self {
- sqlite3: 0 as *mut _,
+ sqlite3: ptr::null_mut(),
persistent,
write: RefCell::new(true),
_sqlite: PhantomData,
@@ -32,7 +32,7 @@ impl Connection {
CString::new(uri)?.as_ptr(),
&mut connection.sqlite3,
flags,
- 0 as *const _,
+ ptr::null(),
);
// Turn on extended error codes
@@ -97,7 +97,7 @@ impl Connection {
let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
} {
- let mut raw_statement = 0 as *mut sqlite3_stmt;
+ let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
let mut remaining_sql_ptr = ptr::null();
sqlite3_prepare_v2(
self.sqlite3,
@@ -48,7 +48,7 @@ impl<'a> Statement<'a> {
.trim();
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
} {
- let mut raw_statement = 0 as *mut sqlite3_stmt;
+ let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
let mut remaining_sql_ptr = ptr::null();
sqlite3_prepare_v2(
connection.sqlite3,
@@ -101,7 +101,7 @@ impl<'a> Statement<'a> {
}
}
- fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt) -> ()) -> Result<()> {
+ fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt)) -> Result<()> {
let mut any_succeed = false;
unsafe {
for raw_statement in self.raw_statements.iter() {
@@ -133,7 +133,7 @@ impl<'a> Statement<'a> {
})
}
- pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
+ pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> {
let index = index as c_int;
let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
@@ -217,7 +217,7 @@ impl<'a> Statement<'a> {
})
}
- pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
+ pub fn column_text(&mut self, index: i32) -> Result<&str> {
let index = index as c_int;
let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
@@ -114,12 +114,12 @@ impl<M: Migrator> ThreadSafeConnection<M> {
let mut queues = QUEUES.write();
if !queues.contains_key(&self.uri) {
let mut write_queue_constructor =
- write_queue_constructor.unwrap_or(background_thread_queue());
+ write_queue_constructor.unwrap_or_else(background_thread_queue);
queues.insert(self.uri.clone(), write_queue_constructor());
return true;
}
}
- return false;
+ false
}
pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
@@ -187,10 +187,9 @@ impl<M: Migrator> ThreadSafeConnection<M> {
*connection.write.get_mut() = false;
if let Some(initialize_query) = connection_initialize_query {
- connection.exec(initialize_query).expect(&format!(
- "Initialize query failed to execute: {}",
- initialize_query
- ))()
+ connection.exec(initialize_query).unwrap_or_else(|_| {
+ panic!("Initialize query failed to execute: {}", initialize_query)
+ })()
.unwrap()
}
@@ -225,7 +224,7 @@ impl<M: Migrator> Clone for ThreadSafeConnection<M> {
Self {
uri: self.uri.clone(),
persistent: self.persistent,
- connection_initialize_query: self.connection_initialize_query.clone(),
+ connection_initialize_query: self.connection_initialize_query,
connections: self.connections.clone(),
_migrator: PhantomData,
}
@@ -8,7 +8,7 @@ use crate::{
impl Connection {
pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.exec())
}
@@ -16,7 +16,7 @@ impl Connection {
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
}
@@ -24,7 +24,7 @@ impl Connection {
&'a self,
query: &str,
) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.rows::<C>())
}
@@ -32,7 +32,7 @@ impl Connection {
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
}
@@ -40,7 +40,7 @@ impl Connection {
&'a self,
query: &str,
) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move || statement.maybe_row::<C>())
}
@@ -48,7 +48,7 @@ impl Connection {
&'a self,
query: &str,
) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
- let mut statement = Statement::prepare(&self, query)?;
+ let mut statement = Statement::prepare(self, query)?;
Ok(move |bindings| {
statement
.with_bindings(bindings)
@@ -33,14 +33,14 @@ fn create_error(
.skip_while(|(offset, _)| offset <= &error_offset)
.map(|(_, span)| span)
.next()
- .unwrap_or(Span::call_site());
+ .unwrap_or_else(Span::call_site);
let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql);
TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error())
}
fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) {
let mut sql_tokens = vec![];
- flatten_stream(tokens.clone(), &mut sql_tokens);
+ flatten_stream(tokens, &mut sql_tokens);
// Lookup of spans by offset at the end of the token
let mut spans: Vec<(usize, Span)> = Vec::new();
let mut sql = String::new();
@@ -67,7 +67,7 @@ fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) {
result.push((close_delimiter(group.delimiter()), group.span()));
}
TokenTree::Ident(ident) => {
- result.push((format!("{} ", ident.to_string()), ident.span()));
+ result.push((format!("{} ", ident), ident.span()));
}
leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())),
}
@@ -58,7 +58,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
self.0.insert_or_replace(MapEntry { key, value }, &());
}
- pub fn remove<'a>(&mut self, key: &'a K) -> Option<V> {
+ pub fn remove(&mut self, key: &K) -> Option<V> {
let mut removed = None;
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
let key = MapKeyRef(Some(key));
@@ -1,6 +1,6 @@
/// The mappings defined in this file where created from reading the alacritty source
use alacritty_terminal::term::TermMode;
-use gpui::keymap::Keystroke;
+use gpui::keymap_matcher::Keystroke;
#[derive(Debug, PartialEq, Eq)]
pub enum Modifiers {
@@ -273,6 +273,8 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
#[cfg(test)]
mod test {
+ use gpui::keymap_matcher::Keystroke;
+
use super::*;
#[test]
@@ -50,7 +50,7 @@ use thiserror::Error;
use gpui::{
geometry::vector::{vec2f, Vector2F},
- keymap::Keystroke,
+ keymap_matcher::Keystroke,
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
};
@@ -14,7 +14,7 @@ use gpui::{
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
geometry::vector::Vector2F,
impl_actions, impl_internal_actions,
- keymap::Keystroke,
+ keymap_matcher::{KeymapContext, Keystroke},
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
View, ViewContext, ViewHandle, WeakViewHandle,
};
@@ -465,7 +465,7 @@ impl View for TerminalView {
});
}
- fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
+ fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
let mut context = Self::default_keymap_context();
let mode = self.terminal.read(cx).last_content.mode;
@@ -247,6 +247,7 @@ pub struct Search {
pub results_status: TextStyle,
pub tab_icon_width: f32,
pub tab_icon_spacing: f32,
+ pub dismiss_button: Interactive<IconButton>,
}
#[derive(Clone, Deserialize, Default)]
@@ -4,7 +4,7 @@ use lazy_static::lazy_static;
lazy_static! {
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
- .unwrap_or(include_str!("../../zed/RELEASE_CHANNEL").to_string());
+ .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
"dev" => ReleaseChannel::Dev,
"preview" => ReleaseChannel::Preview,
@@ -36,7 +36,7 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
debug_assert!(max_chars >= 5);
if s.len() > max_chars {
- format!("{}β¦", truncate(&s, max_chars.saturating_sub(3)))
+ format!("{}β¦", truncate(s, max_chars.saturating_sub(3)))
} else {
s.to_string()
}
@@ -3,7 +3,7 @@ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement, Bias, CharKind, DisplayPoint,
};
-use gpui::{actions, impl_actions, MutableAppContext};
+use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
use language::{Point, Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@@ -32,6 +32,8 @@ pub enum Motion {
StartOfDocument,
EndOfDocument,
Matching,
+ FindForward { before: bool, character: char },
+ FindBackward { after: bool, character: char },
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -107,10 +109,34 @@ pub fn init(cx: &mut MutableAppContext) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
+ cx.add_action(
+ |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
+ .active_operator()
+ {
+ Some(Operator::FindForward { before }) => motion(
+ Motion::FindForward {
+ before,
+ character: keystroke.key.chars().next().unwrap(),
+ },
+ cx,
+ ),
+ Some(Operator::FindBackward { after }) => motion(
+ Motion::FindBackward {
+ after,
+ character: keystroke.key.chars().next().unwrap(),
+ },
+ cx,
+ ),
+ _ => cx.propagate_action(),
+ },
+ )
}
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
- if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
+ if let Some(Operator::Namespace(_))
+ | Some(Operator::FindForward { .. })
+ | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
+ {
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
@@ -152,14 +178,16 @@ impl Motion {
| CurrentLine
| EndOfLine
| NextWordEnd { .. }
- | Matching => true,
+ | Matching
+ | FindForward { .. } => true,
Left
| Backspace
| Right
| StartOfLine
| NextWordStart { .. }
| PreviousWordStart { .. }
- | FirstNonWhitespace => false,
+ | FirstNonWhitespace
+ | FindBackward { .. } => false,
}
}
@@ -196,6 +224,14 @@ impl Motion {
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
Matching => (matching(map, point), SelectionGoal::None),
+ FindForward { before, character } => (
+ find_forward(map, point, before, character, times),
+ SelectionGoal::None,
+ ),
+ FindBackward { after, character } => (
+ find_backward(map, point, after, character, times),
+ SelectionGoal::None,
+ ),
};
(new_point != point || self.infallible()).then_some((new_point, goal))
@@ -446,3 +482,50 @@ fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
point
}
}
+
+fn find_forward(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ before: bool,
+ target: char,
+ mut times: usize,
+) -> DisplayPoint {
+ let mut previous_point = from;
+
+ for (ch, point) in map.chars_at(from) {
+ if ch == target && point != from {
+ times -= 1;
+ if times == 0 {
+ return if before { previous_point } else { point };
+ }
+ } else if ch == '\n' {
+ break;
+ }
+ previous_point = point;
+ }
+
+ from
+}
+
+fn find_backward(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ after: bool,
+ target: char,
+ mut times: usize,
+) -> DisplayPoint {
+ let mut previous_point = from;
+ for (ch, point) in map.reverse_chars_at(from) {
+ if ch == target && point != from {
+ times -= 1;
+ if times == 0 {
+ return if after { previous_point } else { point };
+ }
+ } else if ch == '\n' {
+ break;
+ }
+ previous_point = point;
+ }
+
+ from
+}
@@ -18,6 +18,7 @@ use editor::{
};
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
use language::{AutoindentMode, Point, SelectionGoal};
+use log::error;
use serde::Deserialize;
use workspace::Workspace;
@@ -101,8 +102,9 @@ pub fn normal_motion(
Some(Operator::Change) => change_motion(vim, motion, times, cx),
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
- _ => {
+ Some(operator) => {
// Can't do anything for text objects or namespace operators. Ignoring
+ error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
});
@@ -912,4 +914,42 @@ mod test {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
cx.assert_all("TestΛβΛββΛβΛTest").await;
}
+
+ #[gpui::test]
+ async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ for count in 1..=3 {
+ let test_case = indoc! {"
+ ΛaaaΛbΛ ΛbΛ ΛbΛbΛ aΛaaΛbaaa
+ Λ ΛbΛaaΛa ΛbΛbΛb
+ Λ
+ Λb
+ "};
+
+ cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
+ .await;
+
+ cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
+ .await;
+ }
+ }
+
+ #[gpui::test]
+ async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ for count in 1..=3 {
+ let test_case = indoc! {"
+ ΛaaaΛbΛ ΛbΛ ΛbΛbΛ aΛaaΛbaaa
+ Λ ΛbΛaaΛa ΛbΛbΛb
+ Λ
+ Λb
+ "};
+
+ cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
+ .await;
+
+ cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
+ .await;
+ }
+ }
}
@@ -1,4 +1,4 @@
-use gpui::keymap::Context;
+use gpui::keymap_matcher::KeymapContext;
use language::CursorShape;
use serde::{Deserialize, Serialize};
@@ -29,6 +29,8 @@ pub enum Operator {
Delete,
Yank,
Object { around: bool },
+ FindForward { before: bool },
+ FindBackward { after: bool },
}
#[derive(Default)]
@@ -54,6 +56,10 @@ impl VimState {
pub fn vim_controlled(&self) -> bool {
!matches!(self.mode, Mode::Insert)
+ || matches!(
+ self.operator_stack.last(),
+ Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
+ )
}
pub fn clip_at_line_end(&self) -> bool {
@@ -64,8 +70,8 @@ impl VimState {
!matches!(self.mode, Mode::Visual { .. })
}
- pub fn keymap_context_layer(&self) -> Context {
- let mut context = Context::default();
+ pub fn keymap_context_layer(&self) -> KeymapContext {
+ let mut context = KeymapContext::default();
context.map.insert(
"vim_mode".to_string(),
match self.mode {
@@ -81,34 +87,48 @@ impl VimState {
}
let active_operator = self.operator_stack.last();
- if matches!(active_operator, Some(Operator::Object { .. })) {
- context.set.insert("VimObject".to_string());
+
+ if let Some(active_operator) = active_operator {
+ for context_flag in active_operator.context_flags().into_iter() {
+ context.set.insert(context_flag.to_string());
+ }
}
- Operator::set_context(active_operator, &mut context);
+ context.map.insert(
+ "vim_operator".to_string(),
+ active_operator
+ .map(|op| op.id())
+ .unwrap_or_else(|| "none")
+ .to_string(),
+ );
context
}
}
impl Operator {
- pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
- let operator_context = match operator {
- Some(Operator::Number(_)) => "n",
- Some(Operator::Namespace(Namespace::G)) => "g",
- Some(Operator::Namespace(Namespace::Z)) => "z",
- Some(Operator::Object { around: false }) => "i",
- Some(Operator::Object { around: true }) => "a",
- Some(Operator::Change) => "c",
- Some(Operator::Delete) => "d",
- Some(Operator::Yank) => "y",
-
- None => "none",
+ pub fn id(&self) -> &'static str {
+ match self {
+ Operator::Number(_) => "n",
+ Operator::Namespace(Namespace::G) => "g",
+ Operator::Namespace(Namespace::Z) => "z",
+ Operator::Object { around: false } => "i",
+ Operator::Object { around: true } => "a",
+ Operator::Change => "c",
+ Operator::Delete => "d",
+ Operator::Yank => "y",
+ Operator::FindForward { before: false } => "f",
+ Operator::FindForward { before: true } => "t",
+ Operator::FindBackward { after: false } => "F",
+ Operator::FindBackward { after: true } => "T",
}
- .to_owned();
+ }
- context
- .map
- .insert("vim_operator".to_string(), operator_context);
+ pub fn context_flags(&self) -> &'static [&'static str] {
+ match self {
+ Operator::Object { .. } => &["VimObject"],
+ Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
+ _ => &[],
+ }
}
}
@@ -7,7 +7,7 @@ use async_compat::Compat;
#[cfg(feature = "neovim")]
use async_trait::async_trait;
#[cfg(feature = "neovim")]
-use gpui::keymap::Keystroke;
+use gpui::keymap_matcher::Keystroke;
use language::{Point, Selection};
@@ -16,7 +16,6 @@ use editor::{Bias, Cancel, Editor};
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
use language::CursorShape;
use serde::Deserialize;
-
use settings::Settings;
use state::{Mode, Operator, VimState};
use workspace::{self, Workspace};
@@ -55,7 +54,7 @@ pub fn init(cx: &mut MutableAppContext) {
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
- // If we are in a non normal mode or have an active operator, swap to normal mode
+ // If we are in aren't in normal mode or have an active operator, swap to normal mode
// Otherwise forward cancel on to the editor
let vim = Vim::read(cx);
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
@@ -81,17 +80,21 @@ pub fn init(cx: &mut MutableAppContext) {
.detach();
}
-// Any keystrokes not mapped to vim should clear the active operator
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
if let Some(handled_by) = handled_by {
- if handled_by.namespace() == "vim" {
+ // Keystroke is handled by the vim system, so continue forward
+ // Also short circuit if it is the special cancel action
+ if handled_by.namespace() == "vim"
+ || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
+ {
return true;
}
}
Vim::update(cx, |vim, cx| {
if vim.active_operator().is_some() {
+ // If the keystroke is not handled by vim, we should clear the operator
vim.clear_operator(cx);
}
});
@@ -0,0 +1 @@
@@ -0,0 +1 @@
@@ -44,6 +44,8 @@ actions!(
ActivateLastItem,
CloseActiveItem,
CloseInactiveItems,
+ CloseCleanItems,
+ CloseAllItems,
ReopenClosedItem,
SplitLeft,
SplitUp,
@@ -122,6 +124,8 @@ pub fn init(cx: &mut MutableAppContext) {
});
cx.add_async_action(Pane::close_active_item);
cx.add_async_action(Pane::close_inactive_items);
+ cx.add_async_action(Pane::close_clean_items);
+ cx.add_async_action(Pane::close_all_items);
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
let pane = action.pane.upgrade(cx)?;
let task = Pane::close_item(workspace, pane, action.item_id, cx);
@@ -258,6 +262,13 @@ pub enum ReorderBehavior {
MoveToIndex(usize),
}
+enum ItemType {
+ Active,
+ Inactive,
+ Clean,
+ All,
+}
+
impl Pane {
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
let handle = cx.weak_handle();
@@ -696,40 +707,67 @@ impl Pane {
_: &CloseActiveItem,
cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
- let pane_handle = workspace.active_pane().clone();
- let pane = pane_handle.read(cx);
- if pane.items.is_empty() {
- None
- } else {
- let item_id_to_close = pane.items[pane.active_item_index].id();
- let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
- item_id == item_id_to_close
- });
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
- }
+ Self::close_main(workspace, ItemType::Active, cx)
}
pub fn close_inactive_items(
workspace: &mut Workspace,
_: &CloseInactiveItems,
cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ Self::close_main(workspace, ItemType::Inactive, cx)
+ }
+
+ pub fn close_all_items(
+ workspace: &mut Workspace,
+ _: &CloseAllItems,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ Self::close_main(workspace, ItemType::All, cx)
+ }
+
+ pub fn close_clean_items(
+ workspace: &mut Workspace,
+ _: &CloseCleanItems,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ Self::close_main(workspace, ItemType::Clean, cx)
+ }
+
+ fn close_main(
+ workspace: &mut Workspace,
+ close_item_type: ItemType,
+ cx: &mut ViewContext<Workspace>,
) -> Option<Task<Result<()>>> {
let pane_handle = workspace.active_pane().clone();
let pane = pane_handle.read(cx);
if pane.items.is_empty() {
- None
- } else {
- let active_item_id = pane.items[pane.active_item_index].id();
- let task =
- Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
- Some(cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- }))
+ return None;
}
+
+ let active_item_id = pane.items[pane.active_item_index].id();
+ let clean_item_ids: Vec<_> = pane
+ .items()
+ .filter(|item| !item.is_dirty(cx))
+ .map(|item| item.id())
+ .collect();
+ let task =
+ Self::close_items(
+ workspace,
+ pane_handle,
+ cx,
+ move |item_id| match close_item_type {
+ ItemType::Active => item_id == active_item_id,
+ ItemType::Inactive => item_id != active_item_id,
+ ItemType::Clean => clean_item_ids.contains(&item_id),
+ ItemType::All => true,
+ },
+ );
+
+ Some(cx.foreground().spawn(async move {
+ task.await?;
+ Ok(())
+ }))
}
pub fn close_item(
@@ -33,6 +33,7 @@ use gpui::{
actions,
elements::*,
impl_actions, impl_internal_actions,
+ keymap_matcher::KeymapContext,
platform::{CursorStyle, WindowOptions},
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
@@ -95,7 +96,7 @@ actions!(
ToggleLeftSidebar,
ToggleRightSidebar,
NewTerminal,
- NewSearch,
+ NewSearch
]
);
@@ -2142,7 +2143,6 @@ impl Workspace {
let call = self.active_call()?;
let room = call.read(cx).room()?.read(cx);
let participant = room.remote_participant_for_peer_id(leader_id)?;
-
let mut items_to_add = Vec::new();
match participant.location {
call::ParticipantLocation::SharedProject { project_id } => {
@@ -2153,6 +2153,12 @@ impl Workspace {
.and_then(|id| state.items_by_leader_view_id.get(&id))
{
items_to_add.push((pane.clone(), item.boxed_clone()));
+ } else {
+ if let Some(shared_screen) =
+ self.shared_screen_for_peer(leader_id, pane, cx)
+ {
+ items_to_add.push((pane.clone(), Box::new(shared_screen)));
+ }
}
}
}
@@ -2588,7 +2594,7 @@ impl View for Workspace {
}
}
- fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
+ fn keymap_context(&self, _: &AppContext) -> KeymapContext {
let mut keymap = Self::default_keymap_context();
if self.active_pane() == self.dock_pane() {
keymap.set.insert("Dock".into());
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.68.0"
+version = "0.69.0"
[lib]
name = "zed"
@@ -30,6 +30,7 @@ clock = { path = "../clock" }
diagnostics = { path = "../diagnostics" }
editor = { path = "../editor" }
file_finder = { path = "../file_finder" }
+human_bytes = "0.4.1"
search = { path = "../search" }
fs = { path = "../fs" }
fsevent = { path = "../fsevent" }
@@ -48,6 +49,7 @@ recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" }
settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
+sysinfo = "0.27.1"
text = { path = "../text" }
terminal_view = { path = "../terminal_view" }
theme = { path = "../theme" }
@@ -108,6 +110,7 @@ tree-sitter-html = "0.19.0"
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
url = "2.2"
+urlencoding = "2.1.2"
[dev-dependencies]
call = { path = "../call", features = ["test-support"] }
@@ -50,14 +50,14 @@ impl LspAdapter for RubyLanguageServer {
grammar.highlight_id_for_name("type")?
}
lsp::CompletionItemKind::KEYWORD => {
- if label.starts_with(":") {
+ if label.starts_with(':') {
grammar.highlight_id_for_name("string.special.symbol")?
} else {
grammar.highlight_id_for_name("keyword")?
}
}
lsp::CompletionItemKind::VARIABLE => {
- if label.starts_with("@") {
+ if label.starts_with('@') {
grammar.highlight_id_for_name("property")?
} else {
return None;
@@ -128,8 +128,14 @@ impl LspAdapter for TypeScriptLspAdapter {
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
_ => None,
}?;
+
+ let text = match &item.detail {
+ Some(detail) => format!("{} {}", item.label, detail),
+ None => item.label.clone(),
+ };
+
Some(language::CodeLabel {
- text: item.label.clone(),
+ text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
})
@@ -101,7 +101,7 @@ fn main() {
//Setup settings global before binding actions
cx.set_global(SettingsFile::new(
- &*paths::SETTINGS,
+ &paths::SETTINGS,
settings_file_content.clone(),
fs.clone(),
));
@@ -586,7 +586,7 @@ async fn handle_cli_connection(
responses
.send(CliResponse::Exit {
- status: if errored { 1 } else { 0 },
+ status: i32::from(errored),
})
.log_err();
}
@@ -339,15 +339,22 @@ pub fn menus() -> Vec<Menu<'static>> {
},
MenuItem::Separator,
MenuItem::Action {
- name: "Documentation",
- action: Box::new(crate::OpenBrowser {
- url: "https://zed.dev/docs".into(),
- }),
+ name: "Copy System Specs Into Clipboard",
+ action: Box::new(crate::CopySystemSpecsIntoClipboard),
+ },
+ MenuItem::Action {
+ name: "File Bug Report",
+ action: Box::new(crate::FileBugReport),
},
MenuItem::Action {
- name: "Give Feedback",
+ name: "Request Feature",
+ action: Box::new(crate::RequestFeature),
+ },
+ MenuItem::Separator,
+ MenuItem::Action {
+ name: "Documentation",
action: Box::new(crate::OpenBrowser {
- url: super::feedback::NEW_ISSUE_URL.into(),
+ url: "https://zed.dev/docs".into(),
}),
},
MenuItem::Action {
@@ -0,0 +1,52 @@
+use std::{env, fmt::Display};
+
+use gpui::AppContext;
+use human_bytes::human_bytes;
+use sysinfo::{System, SystemExt};
+use util::channel::ReleaseChannel;
+
+pub struct SystemSpecs {
+ app_version: &'static str,
+ release_channel: &'static str,
+ os_name: &'static str,
+ os_version: Option<String>,
+ memory: u64,
+ architecture: &'static str,
+}
+
+impl SystemSpecs {
+ pub fn new(cx: &AppContext) -> Self {
+ let platform = cx.platform();
+ let system = System::new_all();
+
+ SystemSpecs {
+ app_version: env!("CARGO_PKG_VERSION"),
+ release_channel: cx.global::<ReleaseChannel>().dev_name(),
+ os_name: platform.os_name(),
+ os_version: platform
+ .os_version()
+ .ok()
+ .map(|os_version| os_version.to_string()),
+ memory: system.total_memory(),
+ architecture: env::consts::ARCH,
+ }
+ }
+}
+
+impl Display for SystemSpecs {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let os_information = match &self.os_version {
+ Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
+ None => format!("OS: {}", self.os_name),
+ };
+ let system_specs = [
+ format!("Zed: {} ({})", self.app_version, self.release_channel),
+ os_information,
+ format!("Memory: {}", human_bytes(self.memory as f64)),
+ format!("Architecture: {}", self.architecture),
+ ]
+ .join("\n");
+
+ write!(f, "{system_specs}")
+ }
+}
@@ -1,6 +1,7 @@
mod feedback;
pub mod languages;
pub mod menus;
+pub mod system_specs;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
@@ -21,7 +22,7 @@ use gpui::{
},
impl_actions,
platform::{WindowBounds, WindowOptions},
- AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
+ AssetSource, AsyncAppContext, ClipboardItem, TitlebarOptions, ViewContext, WindowKind,
};
use language::Rope;
use lazy_static::lazy_static;
@@ -33,6 +34,7 @@ use serde::Deserialize;
use serde_json::to_string_pretty;
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
use std::{env, path::Path, str, sync::Arc};
+use system_specs::SystemSpecs;
use util::{channel::ReleaseChannel, paths, ResultExt};
pub use workspace;
use workspace::{sidebar::SidebarSide, AppState, Workspace};
@@ -67,6 +69,9 @@ actions!(
ResetBufferFontSize,
InstallCommandLineInterface,
ResetDatabase,
+ CopySystemSpecsIntoClipboard,
+ RequestFeature,
+ FileBugReport
]
);
@@ -245,6 +250,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
},
);
+ cx.add_action(
+ |_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext<Workspace>| {
+ let system_specs = SystemSpecs::new(cx).to_string();
+ let item = ClipboardItem::new(system_specs.clone());
+ cx.prompt(
+ gpui::PromptLevel::Info,
+ &format!("Copied into clipboard:\n\n{system_specs}"),
+ &["OK"],
+ );
+ cx.write_to_clipboard(item);
+ },
+ );
+
+ cx.add_action(
+ |_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
+ let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
+ cx.dispatch_action(OpenBrowser {
+ url: url.into(),
+ });
+ },
+ );
+
+ cx.add_action(
+ |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
+ let system_specs_text = SystemSpecs::new(cx).to_string();
+ let url = format!(
+ "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
+ urlencoding::encode(&system_specs_text)
+ );
+ cx.dispatch_action(OpenBrowser {
+ url: url.into(),
+ });
+ },
+ );
+
activity_indicator::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
settings::KeymapFileContent::load_defaults(cx);
@@ -298,11 +338,11 @@ pub fn initialize_workspace(
},
"schemas": [
{
- "fileMatch": [schema_file_match(&*paths::SETTINGS)],
+ "fileMatch": [schema_file_match(&paths::SETTINGS)],
"schema": settings_file_json_schema(theme_names, language_names),
},
{
- "fileMatch": [schema_file_match(&*paths::KEYMAP)],
+ "fileMatch": [schema_file_match(&paths::KEYMAP)],
"schema": keymap_file_json_schema(&action_names),
}
]
@@ -606,7 +646,7 @@ fn open_bundled_config_file(
cx: &mut ViewContext<Workspace>,
) {
workspace
- .with_local_workspace(&app_state.clone(), cx, |workspace, cx| {
+ .with_local_workspace(&app_state, cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
let text = Assets::get(asset_path).unwrap().data;
@@ -32,13 +32,13 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
},
},
dismissButton: {
- color: foreground(layer, "on"),
+ color: foreground(layer, "variant"),
iconWidth: 8,
iconHeight: 8,
buttonWidth: 8,
buttonHeight: 8,
hover: {
- color: foreground(layer, "on", "hovered"),
+ color: foreground(layer, "hovered"),
},
},
};
@@ -257,7 +257,6 @@ export default function editor(colorScheme: ColorScheme) {
right: 6,
},
hover: {
- color: foreground(layer, "on", "hovered"),
background: background(layer, "on", "hovered"),
},
},
@@ -80,5 +80,17 @@ export default function search(colorScheme: ColorScheme) {
...text(layer, "mono", "on"),
size: 18,
},
+ dismissButton: {
+ color: foreground(layer, "variant"),
+ iconWidth: 12,
+ buttonWidth: 14,
+ padding: {
+ left: 10,
+ right: 10,
+ },
+ hover: {
+ color: foreground(layer, "hovered"),
+ },
+ },
};
}
@@ -26,7 +26,7 @@ export default function tabBar(colorScheme: ColorScheme) {
// Close icons
iconWidth: 8,
iconClose: foreground(layer, "variant"),
- iconCloseActive: foreground(layer),
+ iconCloseActive: foreground(layer, "hovered"),
// Indicators
iconConflict: foreground(layer, "warning"),