Merge branch 'main' into search2

This commit is contained in:
Piotr Osiewicz 2023-11-15 12:54:26 +01:00
commit b11bfa8821
163 changed files with 20705 additions and 13985 deletions

67
Cargo.lock generated
View file

@ -2012,7 +2012,7 @@ dependencies = [
"serde_derive", "serde_derive",
"settings2", "settings2",
"smol", "smol",
"theme", "theme2",
"util", "util",
] ]
@ -2768,7 +2768,6 @@ dependencies = [
"copilot2", "copilot2",
"ctor", "ctor",
"db2", "db2",
"drag_and_drop",
"env_logger 0.9.3", "env_logger 0.9.3",
"futures 0.3.28", "futures 0.3.28",
"fuzzy2", "fuzzy2",
@ -3062,6 +3061,31 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "file_finder2"
version = "0.1.0"
dependencies = [
"collections",
"ctor",
"editor2",
"env_logger 0.9.3",
"fuzzy2",
"gpui2",
"language2",
"menu2",
"picker2",
"postage",
"project2",
"serde",
"serde_json",
"settings2",
"text2",
"theme2",
"ui2",
"util",
"workspace2",
]
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.22" version = "0.2.22"
@ -4199,6 +4223,7 @@ dependencies = [
"anyhow", "anyhow",
"gpui2", "gpui2",
"log", "log",
"serde",
"smol", "smol",
"util", "util",
] ]
@ -6609,6 +6634,36 @@ dependencies = [
"workspace", "workspace",
] ]
[[package]]
name = "project_panel2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
"collections",
"context_menu",
"db2",
"editor2",
"futures 0.3.28",
"gpui2",
"language2",
"menu2",
"postage",
"pretty_assertions",
"project2",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings2",
"smallvec",
"theme2",
"ui2",
"unicase",
"util",
"workspace2",
]
[[package]] [[package]]
name = "project_symbols" name = "project_symbols"
version = "0.1.0" version = "0.1.0"
@ -9286,9 +9341,9 @@ dependencies = [
[[package]] [[package]]
name = "tiktoken-rs" name = "tiktoken-rs"
version = "0.5.4" version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d" checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.4", "base64 0.21.4",
@ -11423,6 +11478,7 @@ dependencies = [
"editor2", "editor2",
"env_logger 0.9.3", "env_logger 0.9.3",
"feature_flags2", "feature_flags2",
"file_finder2",
"fs2", "fs2",
"fsevent", "fsevent",
"futures 0.3.28", "futures 0.3.28",
@ -11432,7 +11488,7 @@ dependencies = [
"ignore", "ignore",
"image", "image",
"indexmap 1.9.3", "indexmap 1.9.3",
"install_cli", "install_cli2",
"isahc", "isahc",
"journal2", "journal2",
"language2", "language2",
@ -11447,6 +11503,7 @@ dependencies = [
"parking_lot 0.11.2", "parking_lot 0.11.2",
"postage", "postage",
"project2", "project2",
"project_panel2",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
"rope2", "rope2",

View file

@ -80,6 +80,7 @@ members = [
"crates/project", "crates/project",
"crates/project2", "crates/project2",
"crates/project_panel", "crates/project_panel",
"crates/project_panel2",
"crates/project_symbols", "crates/project_symbols",
"crates/recent_projects", "crates/recent_projects",
"crates/rope", "crates/rope",
@ -155,6 +156,7 @@ tempdir = { version = "0.3.7" }
thiserror = { version = "1.0.29" } thiserror = { version = "1.0.29" }
time = { version = "0.3", features = ["serde", "serde-well-known"] } time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" } toml = { version = "0.5" }
tiktoken-rs = "0.5.7"
tree-sitter = "0.20" tree-sitter = "0.20"
unindent = { version = "0.1.7" } unindent = { version = "0.1.7" }
pretty_assertions = "1.3.0" pretty_assertions = "1.3.0"

View file

@ -10,6 +10,7 @@
"bindings": { "bindings": {
"ctrl->": "zed::IncreaseBufferFontSize", "ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize", "ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"cmd-d": "editor::DuplicateLine", "cmd-d": "editor::DuplicateLine",
"cmd-backspace": "editor::DeleteLine", "cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown", "cmd-pagedown": "editor::MovePageDown",
@ -18,7 +19,7 @@
"cmd-alt-enter": "editor::NewlineAbove", "cmd-alt-enter": "editor::NewlineAbove",
"shift-enter": "editor::NewlineBelow", "shift-enter": "editor::NewlineBelow",
"cmd--": "editor::Fold", "cmd--": "editor::Fold",
"cmd-=": "editor::UnfoldLines", "cmd-+": "editor::UnfoldLines",
"alt-shift-g": "editor::SplitSelectionIntoLines", "alt-shift-g": "editor::SplitSelectionIntoLines",
"ctrl-g": [ "ctrl-g": [
"editor::SelectNext", "editor::SelectNext",

View file

@ -174,7 +174,8 @@
// //
// 1. "gpt-3.5-turbo-0613"" // 1. "gpt-3.5-turbo-0613""
// 2. "gpt-4-0613"" // 2. "gpt-4-0613""
"default_open_ai_model": "gpt-4-0613" // 3. "gpt-4-1106-preview"
"default_open_ai_model": "gpt-4-1106-preview"
}, },
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
@ -270,9 +271,7 @@
"copilot": { "copilot": {
// The set of glob patterns for which copilot should be disabled // The set of glob patterns for which copilot should be disabled
// in any matching file. // in any matching file.
"disabled_globs": [ "disabled_globs": [".env"]
".env"
]
}, },
// Settings specific to journaling // Settings specific to journaling
"journal": { "journal": {
@ -381,12 +380,7 @@
// Default directories to search for virtual environments, relative // Default directories to search for virtual environments, relative
// to the current working directory. We recommend overriding this // to the current working directory. We recommend overriding this
// in your project's settings, rather than globally. // in your project's settings, rather than globally.
"directories": [ "directories": [".env", "env", ".venv", "venv"],
".env",
"env",
".venv",
"venv"
],
// Can also be 'csh', 'fish', and `nushell` // Can also be 'csh', 'fish', and `nushell`
"activate_script": "default" "activate_script": "default"
} }

View file

@ -1,38 +0,0 @@
[package]
name = "ai"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/ai.rs"
doctest = false
[features]
test-support = []
[dependencies]
gpui = { path = "../gpui" }
util = { path = "../util" }
language = { path = "../language" }
async-trait.workspace = true
anyhow.workspace = true
futures.workspace = true
lazy_static.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
isahc.workspace = true
regex.workspace = true
serde.workspace = true
serde_json.workspace = true
postage.workspace = true
rand.workspace = true
log.workspace = true
parse_duration = "2.1.1"
tiktoken-rs = "0.5.0"
matrixmultiply = "0.3.7"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
bincode = "1.3.3"
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }

View file

@ -29,7 +29,7 @@ postage.workspace = true
rand.workspace = true rand.workspace = true
log.workspace = true log.workspace = true
parse_duration = "2.1.1" parse_duration = "2.1.1"
tiktoken-rs = "0.5.0" tiktoken-rs.workspace = true
matrixmultiply = "0.3.7" matrixmultiply = "0.3.7"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
bincode = "1.3.3" bincode = "1.3.3"

View file

@ -29,7 +29,7 @@ postage.workspace = true
rand.workspace = true rand.workspace = true
log.workspace = true log.workspace = true
parse_duration = "2.1.1" parse_duration = "2.1.1"
tiktoken-rs = "0.5.0" tiktoken-rs.workspace = true
matrixmultiply = "0.3.7" matrixmultiply = "0.3.7"
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
bincode = "1.3.3" bincode = "1.3.3"

View file

@ -40,7 +40,7 @@ schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
smol.workspace = true smol.workspace = true
tiktoken-rs = "0.5" tiktoken-rs.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] }

View file

@ -9,6 +9,8 @@ pub enum OpenAIModel {
ThreePointFiveTurbo, ThreePointFiveTurbo,
#[serde(rename = "gpt-4-0613")] #[serde(rename = "gpt-4-0613")]
Four, Four,
#[serde(rename = "gpt-4-1106-preview")]
FourTurbo,
} }
impl OpenAIModel { impl OpenAIModel {
@ -16,6 +18,7 @@ impl OpenAIModel {
match self { match self {
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
OpenAIModel::Four => "gpt-4-0613", OpenAIModel::Four => "gpt-4-0613",
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
} }
} }
@ -23,13 +26,15 @@ impl OpenAIModel {
match self { match self {
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
OpenAIModel::Four => "gpt-4", OpenAIModel::Four => "gpt-4",
OpenAIModel::FourTurbo => "gpt-4-turbo",
} }
} }
pub fn cycle(&self) -> Self { pub fn cycle(&self) -> Self {
match self { match self {
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo, OpenAIModel::Four => OpenAIModel::FourTurbo,
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
} }
} }
} }

View file

@ -36,7 +36,7 @@ impl FakeServer {
peer: Peer::new(0), peer: Peer::new(0),
state: Default::default(), state: Default::default(),
user_id: client_user_id, user_id: client_user_id,
executor: cx.executor().clone(), executor: cx.executor(),
}; };
client client

View file

@ -510,7 +510,7 @@ fn test_fuzzy_like_string() {
#[gpui::test] #[gpui::test]
async fn test_fuzzy_search_users(cx: &mut TestAppContext) { async fn test_fuzzy_search_users(cx: &mut TestAppContext) {
let test_db = TestDb::postgres(cx.executor().clone()); let test_db = TestDb::postgres(cx.executor());
let db = test_db.db(); let db = test_db.db();
for (i, github_login) in [ for (i, github_login) in [
"California", "California",

View file

@ -4,6 +4,7 @@ use gpui::{Model, TestAppContext};
mod channel_buffer_tests; mod channel_buffer_tests;
mod channel_message_tests; mod channel_message_tests;
mod channel_tests; mod channel_tests;
mod editor_tests;
mod following_tests; mod following_tests;
mod integration_tests; mod integration_tests;
mod notification_tests; mod notification_tests;

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,32 @@
// use editor::{ //todo(partially ported)
// test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, // use std::{
// ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo, // path::Path,
// sync::{
// atomic::{self, AtomicBool, AtomicUsize},
// Arc,
// },
// }; // };
//todo!(editor) // use call::ActiveCall;
// use editor::{
// test::editor_test_context::{AssertionContextManager, EditorTestContext},
// Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename,
// ToggleCodeActions, Undo,
// };
// use gpui::{BackgroundExecutor, TestAppContext, VisualContext, VisualTestContext};
// use indoc::indoc;
// use language::{
// language_settings::{AllLanguageSettings, InlayHintSettings},
// tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig,
// };
// use rpc::RECEIVE_TIMEOUT;
// use serde_json::json;
// use settings::SettingsStore;
// use text::Point;
// use workspace::Workspace;
// use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
// #[gpui::test(iterations = 10)] // #[gpui::test(iterations = 10)]
// async fn test_host_disconnect( // async fn test_host_disconnect(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -11,7 +34,7 @@
// cx_b: &mut TestAppContext, // cx_b: &mut TestAppContext,
// cx_c: &mut TestAppContext, // cx_c: &mut TestAppContext,
// ) { // ) {
// let mut server = TestServer::start(&executor).await; // let mut server = TestServer::start(executor).await;
// let client_a = server.create_client(cx_a, "user_a").await; // let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await; // let client_b = server.create_client(cx_b, "user_b").await;
// let client_c = server.create_client(cx_c, "user_c").await; // let client_c = server.create_client(cx_c, "user_c").await;
@ -25,7 +48,7 @@
// .fs() // .fs()
// .insert_tree( // .insert_tree(
// "/a", // "/a",
// json!({ // serde_json::json!({
// "a.txt": "a-contents", // "a.txt": "a-contents",
// "b.txt": "b-contents", // "b.txt": "b-contents",
// }), // }),
@ -35,7 +58,7 @@
// let active_call_a = cx_a.read(ActiveCall::global); // let active_call_a = cx_a.read(ActiveCall::global);
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; // let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap()); // let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees().next().unwrap());
// let project_id = active_call_a // let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) // .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await // .await
@ -46,21 +69,25 @@
// assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); // assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
// let window_b = // let workspace_b =
// cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); // cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx));
// let workspace_b = window_b.root(cx_b); // let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
// let editor_b = workspace_b // let editor_b = workspace_b
// .update(cx_b, |workspace, cx| { // .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "b.txt"), None, true, cx) // workspace.open_path((worktree_id, "b.txt"), None, true, cx)
// }) // })
// .unwrap()
// .await // .await
// .unwrap() // .unwrap()
// .downcast::<Editor>() // .downcast::<Editor>()
// .unwrap(); // .unwrap();
// assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); // //TODO: focus
// assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx)));
// editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); // editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
// assert!(window_b.is_edited(cx_b)); // //todo(is_edited)
// // assert!(workspace_b.is_edited(cx_b));
// // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. // // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
// server.forbid_connections(); // server.forbid_connections();
@ -77,10 +104,10 @@
// // Ensure client B's edited state is reset and that the whole window is blurred. // // Ensure client B's edited state is reset and that the whole window is blurred.
// window_b.read_with(cx_b, |cx| { // workspace_b.update(cx_b, |_, cx| {
// assert_eq!(cx.focused_view_id(), None); // assert_eq!(cx.focused_view_id(), None);
// }); // });
// assert!(!window_b.is_edited(cx_b)); // // assert!(!workspace_b.is_edited(cx_b));
// // Ensure client B is not prompted to save edits when closing window after disconnecting. // // Ensure client B is not prompted to save edits when closing window after disconnecting.
// let can_close = workspace_b // let can_close = workspace_b
@ -120,7 +147,6 @@
// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); // project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
// } // }
//todo!(editor)
// #[gpui::test] // #[gpui::test]
// async fn test_newline_above_or_below_does_not_move_guest_cursor( // async fn test_newline_above_or_below_does_not_move_guest_cursor(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -152,12 +178,14 @@
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) // .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
// .await // .await
// .unwrap(); // .unwrap();
// let window_a = cx_a.add_window(|_| EmptyView); // let window_a = cx_a.add_empty_window();
// let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); // let editor_a =
// window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx));
// let mut editor_cx_a = EditorTestContext { // let mut editor_cx_a = EditorTestContext {
// cx: cx_a, // cx: cx_a,
// window: window_a.into(), // window: window_a.into(),
// editor: editor_a, // editor: editor_a,
// assertion_cx: AssertionContextManager::new(),
// }; // };
// // Open a buffer as client B // // Open a buffer as client B
@ -165,12 +193,14 @@
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) // .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
// .await // .await
// .unwrap(); // .unwrap();
// let window_b = cx_b.add_window(|_| EmptyView); // let window_b = cx_b.add_empty_window();
// let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); // let editor_b =
// window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx));
// let mut editor_cx_b = EditorTestContext { // let mut editor_cx_b = EditorTestContext {
// cx: cx_b, // cx: cx_b,
// window: window_b.into(), // window: window_b.into(),
// editor: editor_b, // editor: editor_b,
// assertion_cx: AssertionContextManager::new(),
// }; // };
// // Test newline above // // Test newline above
@ -214,7 +244,6 @@
// "}); // "});
// } // }
//todo!(editor)
// #[gpui::test(iterations = 10)] // #[gpui::test(iterations = 10)]
// async fn test_collaborating_with_completion( // async fn test_collaborating_with_completion(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -275,8 +304,8 @@
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) // .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await // .await
// .unwrap(); // .unwrap();
// let window_b = cx_b.add_window(|_| EmptyView); // let window_b = cx_b.add_empty_window();
// let editor_b = window_b.add_view(cx_b, |cx| { // let editor_b = window_b.build_view(cx_b, |cx| {
// Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) // Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
// }); // });
@ -384,7 +413,7 @@
// ); // );
// // The additional edit is applied. // // The additional edit is applied.
// cx_a.foreground().run_until_parked(); // cx_a.executor().run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| { // buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!( // assert_eq!(
@ -400,7 +429,7 @@
// ); // );
// }); // });
// } // }
//todo!(editor)
// #[gpui::test(iterations = 10)] // #[gpui::test(iterations = 10)]
// async fn test_collaborating_with_code_actions( // async fn test_collaborating_with_code_actions(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -619,7 +648,6 @@
// }); // });
// } // }
//todo!(editor)
// #[gpui::test(iterations = 10)] // #[gpui::test(iterations = 10)]
// async fn test_collaborating_with_renames( // async fn test_collaborating_with_renames(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -813,7 +841,6 @@
// }) // })
// } // }
//todo!(editor)
// #[gpui::test(iterations = 10)] // #[gpui::test(iterations = 10)]
// async fn test_language_server_statuses( // async fn test_language_server_statuses(
// executor: BackgroundExecutor, // executor: BackgroundExecutor,
@ -937,8 +964,8 @@
// cx_b: &mut TestAppContext, // cx_b: &mut TestAppContext,
// cx_c: &mut TestAppContext, // cx_c: &mut TestAppContext,
// ) { // ) {
// let window_b = cx_b.add_window(|_| EmptyView); // let window_b = cx_b.add_empty_window();
// let mut server = TestServer::start(&executor).await; // let mut server = TestServer::start(executor).await;
// let client_a = server.create_client(cx_a, "user_a").await; // let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await; // let client_b = server.create_client(cx_b, "user_b").await;
// let client_c = server.create_client(cx_c, "user_c").await; // let client_c = server.create_client(cx_c, "user_c").await;
@ -1052,7 +1079,7 @@
// .await // .await
// .unwrap(); // .unwrap();
// let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); // let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx));
// // Client A sees client B's selection // // Client A sees client B's selection
// executor.run_until_parked(); // executor.run_until_parked();
@ -1106,3 +1133,757 @@
// == 0 // == 0
// }); // });
// } // }
// #[gpui::test(iterations = 10)]
// async fn test_on_input_format_from_host_to_guest(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// // Set up a fake language server.
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
// first_trigger_character: ":".to_string(),
// more_trigger_character: Some(vec![">".to_string()]),
// }),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// client_a.language_registry().add(Arc::new(language));
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a }",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// // Open a file in an editor as the host.
// let buffer_a = project_a
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// let window_a = cx_a.add_empty_window();
// let editor_a = window_a
// .update(cx_a, |_, cx| {
// cx.build_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx))
// })
// .unwrap();
// let fake_language_server = fake_language_servers.next().await.unwrap();
// executor.run_until_parked();
// // Receive an OnTypeFormatting request as the host's language server.
// // Return some formattings from the host's language server.
// fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
// |params, _| async move {
// assert_eq!(
// params.text_document_position.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// assert_eq!(
// params.text_document_position.position,
// lsp::Position::new(0, 14),
// );
// Ok(Some(vec![lsp::TextEdit {
// new_text: "~<".to_string(),
// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
// }]))
// },
// );
// // Open the buffer on the guest and see that the formattings worked
// let buffer_b = project_b
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// // Type a on type formatting trigger character as the guest.
// editor_a.update(cx_a, |editor, cx| {
// cx.focus(&editor_a);
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input(">", cx);
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a>~< }")
// });
// // Undo should remove LSP edits first
// editor_a.update(cx_a, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a>~< }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a> }");
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a> }")
// });
// editor_a.update(cx_a, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a> }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a }");
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a }")
// });
// }
// #[gpui::test(iterations = 10)]
// async fn test_on_input_format_from_guest_to_host(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// // Set up a fake language server.
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
// first_trigger_character: ":".to_string(),
// more_trigger_character: Some(vec![">".to_string()]),
// }),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// client_a.language_registry().add(Arc::new(language));
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a }",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// // Open a file in an editor as the guest.
// let buffer_b = project_b
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// let window_b = cx_b.add_empty_window();
// let editor_b = window_b.build_view(cx_b, |cx| {
// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
// });
// let fake_language_server = fake_language_servers.next().await.unwrap();
// executor.run_until_parked();
// // Type a on type formatting trigger character as the guest.
// editor_b.update(cx_b, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input(":", cx);
// cx.focus(&editor_b);
// });
// // Receive an OnTypeFormatting request as the host's language server.
// // Return some formattings from the host's language server.
// cx_a.foreground().start_waiting();
// fake_language_server
// .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
// assert_eq!(
// params.text_document_position.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// assert_eq!(
// params.text_document_position.position,
// lsp::Position::new(0, 14),
// );
// Ok(Some(vec![lsp::TextEdit {
// new_text: "~:".to_string(),
// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
// }]))
// })
// .next()
// .await
// .unwrap();
// cx_a.foreground().finish_waiting();
// // Open the buffer on the host and see that the formattings worked
// let buffer_a = project_a
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a:~: }")
// });
// // Undo should remove LSP edits first
// editor_b.update(cx_b, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a:~: }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a: }");
// });
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a: }")
// });
// editor_b.update(cx_b, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a: }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a }");
// });
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a }")
// });
// }
// #[gpui::test(iterations = 10)]
// async fn test_mutual_editor_inlay_hint_cache_update(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// let active_call_b = cx_b.read(ActiveCall::global);
// cx_a.update(editor::init);
// cx_b.update(editor::init);
// cx_a.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: false,
// show_other_hints: true,
// })
// });
// });
// });
// cx_b.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: false,
// show_other_hints: true,
// })
// });
// });
// });
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// let language = Arc::new(language);
// client_a.language_registry().add(Arc::clone(&language));
// client_b.language_registry().add(language);
// // Client A opens a project.
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// active_call_a
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
// .await
// .unwrap();
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// // Client B joins the project
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// active_call_b
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
// .await
// .unwrap();
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root_view(cx_a);
// cx_a.foreground().start_waiting();
// // The host opens a rust file.
// let _buffer_a = project_a
// .update(cx_a, |project, cx| {
// project.open_local_buffer("/a/main.rs", cx)
// })
// .await
// .unwrap();
// let fake_language_server = fake_language_servers.next().await.unwrap();
// let editor_a = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// // Set up the language server to return an additional inlay hint on each request.
// let edits_made = Arc::new(AtomicUsize::new(0));
// let closure_edits_made = Arc::clone(&edits_made);
// fake_language_server
// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
// let task_edits_made = Arc::clone(&closure_edits_made);
// async move {
// assert_eq!(
// params.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
// Ok(Some(vec![lsp::InlayHint {
// position: lsp::Position::new(0, edits_made as u32),
// label: lsp::InlayHintLabel::String(edits_made.to_string()),
// kind: None,
// text_edits: None,
// tooltip: None,
// padding_left: None,
// padding_right: None,
// data: None,
// }]))
// }
// })
// .next()
// .await
// .unwrap();
// executor.run_until_parked();
// let initial_edit = edits_made.load(atomic::Ordering::Acquire);
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![initial_edit.to_string()],
// extract_hint_labels(editor),
// "Host should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Host editor update the cache version after every cache/view change",
// );
// });
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
// let editor_b = workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![initial_edit.to_string()],
// extract_hint_labels(editor),
// "Client should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Guest editor update the cache version after every cache/view change"
// );
// });
// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// editor_b.update(cx_b, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
// editor.handle_input(":", cx);
// cx.focus(&editor_b);
// });
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_client_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 2);
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_client_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 2);
// });
// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// editor_a.update(cx_a, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input("a change to increment both buffers' versions", cx);
// cx.focus(&editor_a);
// });
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_host_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 3);
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_host_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 3);
// });
// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// fake_language_server
// .request::<lsp::request::InlayHintRefreshRequest>(())
// .await
// .expect("inlay refresh request failed");
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_special_edit_for_refresh.to_string()],
// extract_hint_labels(editor),
// "Host should react to /refresh LSP request"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 4,
// "Host should accepted all edits and bump its cache version every time"
// );
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_special_edit_for_refresh.to_string()],
// extract_hint_labels(editor),
// "Guest should get a /refresh LSP request propagated by host"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 4,
// "Guest should accepted all edits and bump its cache version every time"
// );
// });
// }
// #[gpui::test(iterations = 10)]
// async fn test_inlay_hint_refresh_is_forwarded(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// let active_call_b = cx_b.read(ActiveCall::global);
// cx_a.update(editor::init);
// cx_b.update(editor::init);
// cx_a.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: false,
// show_type_hints: false,
// show_parameter_hints: false,
// show_other_hints: false,
// })
// });
// });
// });
// cx_b.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: true,
// show_other_hints: true,
// })
// });
// });
// });
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// let language = Arc::new(language);
// client_a.language_registry().add(Arc::clone(&language));
// client_b.language_registry().add(language);
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// active_call_a
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
// .await
// .unwrap();
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// active_call_b
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
// .await
// .unwrap();
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
// cx_a.foreground().start_waiting();
// cx_b.foreground().start_waiting();
// let editor_a = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// let editor_b = workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// let other_hints = Arc::new(AtomicBool::new(false));
// let fake_language_server = fake_language_servers.next().await.unwrap();
// let closure_other_hints = Arc::clone(&other_hints);
// fake_language_server
// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
// let task_other_hints = Arc::clone(&closure_other_hints);
// async move {
// assert_eq!(
// params.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
// let character = if other_hints { 0 } else { 2 };
// let label = if other_hints {
// "other hint"
// } else {
// "initial hint"
// };
// Ok(Some(vec![lsp::InlayHint {
// position: lsp::Position::new(0, character),
// label: lsp::InlayHintLabel::String(label.to_string()),
// kind: None,
// text_edits: None,
// tooltip: None,
// padding_left: None,
// padding_right: None,
// data: None,
// }]))
// }
// })
// .next()
// .await
// .unwrap();
// cx_a.foreground().finish_waiting();
// cx_b.foreground().finish_waiting();
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert!(
// extract_hint_labels(editor).is_empty(),
// "Host should get no hints due to them turned off"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 0,
// "Turned off hints should not generate version updates"
// );
// });
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec!["initial hint".to_string()],
// extract_hint_labels(editor),
// "Client should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Should update cache verison after first hints"
// );
// });
// other_hints.fetch_or(true, atomic::Ordering::Release);
// fake_language_server
// .request::<lsp::request::InlayHintRefreshRequest>(())
// .await
// .expect("inlay refresh request failed");
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert!(
// extract_hint_labels(editor).is_empty(),
// "Host should get nop hints due to them turned off, even after the /refresh"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 0,
// "Turned off hints should not generate version updates, again"
// );
// });
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec!["other hint".to_string()],
// extract_hint_labels(editor),
// "Guest should get a /refresh LSP request propagated by host despite host hints are off"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 2,
// "Guest should accepted all edits and bump its cache version every time"
// );
// });
// }
// fn extract_hint_labels(editor: &Editor) -> Vec<String> {
// let mut labels = Vec::new();
// for hint in editor.inlay_hint_cache().hints() {
// match hint.label {
// project::InlayHintLabel::String(s) => labels.push(s),
// _ => unreachable!(),
// }
// }
// labels
// }

View file

@ -5717,758 +5717,3 @@ async fn test_join_call_after_screen_was_shared(
); );
}); });
} }
//todo!(editor)
// #[gpui::test(iterations = 10)]
// async fn test_on_input_format_from_host_to_guest(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// // Set up a fake language server.
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
// first_trigger_character: ":".to_string(),
// more_trigger_character: Some(vec![">".to_string()]),
// }),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// client_a.language_registry().add(Arc::new(language));
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a }",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// // Open a file in an editor as the host.
// let buffer_a = project_a
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// let window_a = cx_a.add_window(|_| EmptyView);
// let editor_a = window_a.add_view(cx_a, |cx| {
// Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)
// });
// let fake_language_server = fake_language_servers.next().await.unwrap();
// executor.run_until_parked();
// // Receive an OnTypeFormatting request as the host's language server.
// // Return some formattings from the host's language server.
// fake_language_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(
// |params, _| async move {
// assert_eq!(
// params.text_document_position.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// assert_eq!(
// params.text_document_position.position,
// lsp::Position::new(0, 14),
// );
// Ok(Some(vec![lsp::TextEdit {
// new_text: "~<".to_string(),
// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
// }]))
// },
// );
// // Open the buffer on the guest and see that the formattings worked
// let buffer_b = project_b
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// // Type a on type formatting trigger character as the guest.
// editor_a.update(cx_a, |editor, cx| {
// cx.focus(&editor_a);
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input(">", cx);
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a>~< }")
// });
// // Undo should remove LSP edits first
// editor_a.update(cx_a, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a>~< }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a> }");
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a> }")
// });
// editor_a.update(cx_a, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a> }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a }");
// });
// executor.run_until_parked();
// buffer_b.read_with(cx_b, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a }")
// });
// }
// #[gpui::test(iterations = 10)]
// async fn test_on_input_format_from_guest_to_host(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// // Set up a fake language server.
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
// first_trigger_character: ":".to_string(),
// more_trigger_character: Some(vec![">".to_string()]),
// }),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// client_a.language_registry().add(Arc::new(language));
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a }",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// // Open a file in an editor as the guest.
// let buffer_b = project_b
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// let window_b = cx_b.add_window(|_| EmptyView);
// let editor_b = window_b.add_view(cx_b, |cx| {
// Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)
// });
// let fake_language_server = fake_language_servers.next().await.unwrap();
// executor.run_until_parked();
// // Type a on type formatting trigger character as the guest.
// editor_b.update(cx_b, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input(":", cx);
// cx.focus(&editor_b);
// });
// // Receive an OnTypeFormatting request as the host's language server.
// // Return some formattings from the host's language server.
// cx_a.foreground().start_waiting();
// fake_language_server
// .handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
// assert_eq!(
// params.text_document_position.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// assert_eq!(
// params.text_document_position.position,
// lsp::Position::new(0, 14),
// );
// Ok(Some(vec![lsp::TextEdit {
// new_text: "~:".to_string(),
// range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
// }]))
// })
// .next()
// .await
// .unwrap();
// cx_a.foreground().finish_waiting();
// // Open the buffer on the host and see that the formattings worked
// let buffer_a = project_a
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
// .await
// .unwrap();
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a:~: }")
// });
// // Undo should remove LSP edits first
// editor_b.update(cx_b, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a:~: }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a: }");
// });
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a: }")
// });
// editor_b.update(cx_b, |editor, cx| {
// assert_eq!(editor.text(cx), "fn main() { a: }");
// editor.undo(&Undo, cx);
// assert_eq!(editor.text(cx), "fn main() { a }");
// });
// executor.run_until_parked();
// buffer_a.read_with(cx_a, |buffer, _| {
// assert_eq!(buffer.text(), "fn main() { a }")
// });
// }
//todo!(editor)
// #[gpui::test(iterations = 10)]
// async fn test_mutual_editor_inlay_hint_cache_update(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// let active_call_b = cx_b.read(ActiveCall::global);
// cx_a.update(editor::init);
// cx_b.update(editor::init);
// cx_a.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: false,
// show_other_hints: true,
// })
// });
// });
// });
// cx_b.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: false,
// show_other_hints: true,
// })
// });
// });
// });
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// let language = Arc::new(language);
// client_a.language_registry().add(Arc::clone(&language));
// client_b.language_registry().add(language);
// // Client A opens a project.
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// active_call_a
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
// .await
// .unwrap();
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// // Client B joins the project
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// active_call_b
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
// .await
// .unwrap();
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
// cx_a.foreground().start_waiting();
// // The host opens a rust file.
// let _buffer_a = project_a
// .update(cx_a, |project, cx| {
// project.open_local_buffer("/a/main.rs", cx)
// })
// .await
// .unwrap();
// let fake_language_server = fake_language_servers.next().await.unwrap();
// let editor_a = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// // Set up the language server to return an additional inlay hint on each request.
// let edits_made = Arc::new(AtomicUsize::new(0));
// let closure_edits_made = Arc::clone(&edits_made);
// fake_language_server
// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
// let task_edits_made = Arc::clone(&closure_edits_made);
// async move {
// assert_eq!(
// params.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// let edits_made = task_edits_made.load(atomic::Ordering::Acquire);
// Ok(Some(vec![lsp::InlayHint {
// position: lsp::Position::new(0, edits_made as u32),
// label: lsp::InlayHintLabel::String(edits_made.to_string()),
// kind: None,
// text_edits: None,
// tooltip: None,
// padding_left: None,
// padding_right: None,
// data: None,
// }]))
// }
// })
// .next()
// .await
// .unwrap();
// executor.run_until_parked();
// let initial_edit = edits_made.load(atomic::Ordering::Acquire);
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![initial_edit.to_string()],
// extract_hint_labels(editor),
// "Host should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Host editor update the cache version after every cache/view change",
// );
// });
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
// let editor_b = workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![initial_edit.to_string()],
// extract_hint_labels(editor),
// "Client should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Guest editor update the cache version after every cache/view change"
// );
// });
// let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// editor_b.update(cx_b, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
// editor.handle_input(":", cx);
// cx.focus(&editor_b);
// });
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_client_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 2);
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_client_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 2);
// });
// let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// editor_a.update(cx_a, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
// editor.handle_input("a change to increment both buffers' versions", cx);
// cx.focus(&editor_a);
// });
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_host_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 3);
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_host_edit.to_string()],
// extract_hint_labels(editor),
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(inlay_cache.version(), 3);
// });
// let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
// fake_language_server
// .request::<lsp::request::InlayHintRefreshRequest>(())
// .await
// .expect("inlay refresh request failed");
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert_eq!(
// vec![after_special_edit_for_refresh.to_string()],
// extract_hint_labels(editor),
// "Host should react to /refresh LSP request"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 4,
// "Host should accepted all edits and bump its cache version every time"
// );
// });
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec![after_special_edit_for_refresh.to_string()],
// extract_hint_labels(editor),
// "Guest should get a /refresh LSP request propagated by host"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 4,
// "Guest should accepted all edits and bump its cache version every time"
// );
// });
// }
//todo!(editor)
// #[gpui::test(iterations = 10)]
// async fn test_inlay_hint_refresh_is_forwarded(
// executor: BackgroundExecutor,
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
// let mut server = TestServer::start(&executor).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
// .await;
// let active_call_a = cx_a.read(ActiveCall::global);
// let active_call_b = cx_b.read(ActiveCall::global);
// cx_a.update(editor::init);
// cx_b.update(editor::init);
// cx_a.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: false,
// show_type_hints: false,
// show_parameter_hints: false,
// show_other_hints: false,
// })
// });
// });
// });
// cx_b.update(|cx| {
// cx.update_global(|store: &mut SettingsStore, cx| {
// store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
// settings.defaults.inlay_hints = Some(InlayHintSettings {
// enabled: true,
// show_type_hints: true,
// show_parameter_hints: true,
// show_other_hints: true,
// })
// });
// });
// });
// let mut language = Language::new(
// LanguageConfig {
// name: "Rust".into(),
// path_suffixes: vec!["rs".to_string()],
// ..Default::default()
// },
// Some(tree_sitter_rust::language()),
// );
// let mut fake_language_servers = language
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities: lsp::ServerCapabilities {
// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
// ..Default::default()
// },
// ..Default::default()
// }))
// .await;
// let language = Arc::new(language);
// client_a.language_registry().add(Arc::clone(&language));
// client_b.language_registry().add(language);
// client_a
// .fs()
// .insert_tree(
// "/a",
// json!({
// "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
// "other.rs": "// Test file",
// }),
// )
// .await;
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
// active_call_a
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
// .await
// .unwrap();
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
// active_call_b
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
// .await
// .unwrap();
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
// cx_a.foreground().start_waiting();
// cx_b.foreground().start_waiting();
// let editor_a = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// let editor_b = workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "main.rs"), None, true, cx)
// })
// .await
// .unwrap()
// .downcast::<Editor>()
// .unwrap();
// let other_hints = Arc::new(AtomicBool::new(false));
// let fake_language_server = fake_language_servers.next().await.unwrap();
// let closure_other_hints = Arc::clone(&other_hints);
// fake_language_server
// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
// let task_other_hints = Arc::clone(&closure_other_hints);
// async move {
// assert_eq!(
// params.text_document.uri,
// lsp::Url::from_file_path("/a/main.rs").unwrap(),
// );
// let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
// let character = if other_hints { 0 } else { 2 };
// let label = if other_hints {
// "other hint"
// } else {
// "initial hint"
// };
// Ok(Some(vec![lsp::InlayHint {
// position: lsp::Position::new(0, character),
// label: lsp::InlayHintLabel::String(label.to_string()),
// kind: None,
// text_edits: None,
// tooltip: None,
// padding_left: None,
// padding_right: None,
// data: None,
// }]))
// }
// })
// .next()
// .await
// .unwrap();
// cx_a.foreground().finish_waiting();
// cx_b.foreground().finish_waiting();
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert!(
// extract_hint_labels(editor).is_empty(),
// "Host should get no hints due to them turned off"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 0,
// "Turned off hints should not generate version updates"
// );
// });
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec!["initial hint".to_string()],
// extract_hint_labels(editor),
// "Client should get its first hints when opens an editor"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 1,
// "Should update cache verison after first hints"
// );
// });
// other_hints.fetch_or(true, atomic::Ordering::Release);
// fake_language_server
// .request::<lsp::request::InlayHintRefreshRequest>(())
// .await
// .expect("inlay refresh request failed");
// executor.run_until_parked();
// editor_a.update(cx_a, |editor, _| {
// assert!(
// extract_hint_labels(editor).is_empty(),
// "Host should get nop hints due to them turned off, even after the /refresh"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 0,
// "Turned off hints should not generate version updates, again"
// );
// });
// executor.run_until_parked();
// editor_b.update(cx_b, |editor, _| {
// assert_eq!(
// vec!["other hint".to_string()],
// extract_hint_labels(editor),
// "Guest should get a /refresh LSP request propagated by host despite host hints are off"
// );
// let inlay_cache = editor.inlay_hint_cache();
// assert_eq!(
// inlay_cache.version(),
// 2,
// "Guest should accepted all edits and bump its cache version every time"
// );
// });
// }
// fn extract_hint_labels(editor: &Editor) -> Vec<String> {
// let mut labels = Vec::new();
// for hint in editor.inlay_hint_cache().hints() {
// match hint.label {
// project::InlayHintLabel::String(s) => labels.push(s),
// _ => unreachable!(),
// }
// }
// labels
// }

View file

@ -208,11 +208,11 @@ impl TestServer {
}) })
}); });
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx)); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx)); let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
let mut language_registry = LanguageRegistry::test(); let mut language_registry = LanguageRegistry::test();
language_registry.set_executor(cx.executor().clone()); language_registry.set_executor(cx.executor());
let app_state = Arc::new(workspace::AppState { let app_state = Arc::new(workspace::AppState {
client: client.clone(), client: client.clone(),
user_store: user_store.clone(), user_store: user_store.clone(),

View file

@ -1,14 +1,17 @@
use collections::{CommandPaletteFilter, HashMap}; use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke, actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext, Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
WeakView, WindowContext, WindowContext,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use std::cmp::{self, Reverse}; use std::{
cmp::{self, Reverse},
sync::Arc,
};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{v_stack, Label, StyledExt}; use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
use util::{ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt, ResultExt,
@ -127,16 +130,7 @@ impl CommandPaletteDelegate {
) -> Self { ) -> Self {
Self { Self {
command_palette, command_palette,
matches: commands matches: vec![],
.iter()
.enumerate()
.map(|(i, command)| StringMatch {
candidate_id: i,
string: command.name.clone(),
positions: Vec::new(),
score: 0.0,
})
.collect(),
commands, commands,
selected_ix: 0, selected_ix: 0,
previous_focus_handle, previous_focus_handle,
@ -147,6 +141,10 @@ impl CommandPaletteDelegate {
impl PickerDelegate for CommandPaletteDelegate { impl PickerDelegate for CommandPaletteDelegate {
type ListItem = Div<Picker<Self>>; type ListItem = Div<Picker<Self>>;
fn placeholder_text(&self) -> Arc<str> {
"Execute a command...".into()
}
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
self.matches.len() self.matches.len()
} }
@ -296,11 +294,10 @@ impl PickerDelegate for CommandPaletteDelegate {
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> Self::ListItem { ) -> Self::ListItem {
let colors = cx.theme().colors(); let colors = cx.theme().colors();
let Some(command) = self let Some(r#match) = self.matches.get(ix) else {
.matches return div();
.get(ix) };
.and_then(|m| self.commands.get(m.candidate_id)) let Some(command) = self.commands.get(r#match.candidate_id) else {
else {
return div(); return div();
}; };
@ -312,63 +309,16 @@ impl PickerDelegate for CommandPaletteDelegate {
.rounded_md() .rounded_md()
.when(selected, |this| this.bg(colors.ghost_element_selected)) .when(selected, |this| this.bg(colors.ghost_element_selected))
.hover(|this| this.bg(colors.ghost_element_hover)) .hover(|this| this.bg(colors.ghost_element_hover))
.child(Label::new(command.name.clone())) .child(
h_stack()
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
r#match.positions.clone(),
))
.children(KeyBinding::for_action(&*command.action, cx)),
)
} }
// fn render_match(
// &self,
// ix: usize,
// mouse_state: &mut MouseState,
// selected: bool,
// cx: &gpui::AppContext,
// ) -> AnyElement<Picker<Self>> {
// let mat = &self.matches[ix];
// let command = &self.actions[mat.candidate_id];
// let theme = theme::current(cx);
// let style = theme.picker.item.in_state(selected).style_for(mouse_state);
// let key_style = &theme.command_palette.key.in_state(selected);
// let keystroke_spacing = theme.command_palette.keystroke_spacing;
// Flex::row()
// .with_child(
// Label::new(mat.string.clone(), style.label.clone())
// .with_highlights(mat.positions.clone()),
// )
// .with_children(command.keystrokes.iter().map(|keystroke| {
// Flex::row()
// .with_children(
// [
// (keystroke.ctrl, "^"),
// (keystroke.alt, "⌥"),
// (keystroke.cmd, "⌘"),
// (keystroke.shift, "⇧"),
// ]
// .into_iter()
// .filter_map(|(modifier, label)| {
// if modifier {
// Some(
// Label::new(label, key_style.label.clone())
// .contained()
// .with_style(key_style.container),
// )
// } else {
// None
// }
// }),
// )
// .with_child(
// Label::new(keystroke.key.clone(), key_style.label.clone())
// .contained()
// .with_style(key_style.container),
// )
// .contained()
// .with_margin_left(keystroke_spacing)
// .flex_float()
// }))
// .contained()
// .with_style(style.container)
// .into_any()
// }
} }
fn humanize_action_name(name: &str) -> String { fn humanize_action_name(name: &str) -> String {

View file

@ -24,7 +24,7 @@ collections = { path = "../collections" }
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
theme = { path = "../theme" } theme = { package = "theme2", path = "../theme2" }
lsp = { package = "lsp2", path = "../lsp2" } lsp = { package = "lsp2", path = "../lsp2" }
node_runtime = { path = "../node_runtime"} node_runtime = { path = "../node_runtime"}
util = { path = "../util" } util = { path = "../util" }

View file

@ -351,28 +351,29 @@ impl Copilot {
} }
} }
// #[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
// pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) { pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, lsp::FakeLanguageServer) {
// use node_runtime::FakeNodeRuntime; use node_runtime::FakeNodeRuntime;
// let (server, fake_server) = let (server, fake_server) =
// LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
// let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
// let node_runtime = FakeNodeRuntime::new(); let node_runtime = FakeNodeRuntime::new();
// let this = cx.add_model(|_| Self { let this = cx.build_model(|cx| Self {
// server_id: LanguageServerId(0), server_id: LanguageServerId(0),
// http: http.clone(), http: http.clone(),
// node_runtime, node_runtime,
// server: CopilotServer::Running(RunningCopilotServer { server: CopilotServer::Running(RunningCopilotServer {
// name: LanguageServerName(Arc::from("copilot")), name: LanguageServerName(Arc::from("copilot")),
// lsp: Arc::new(server), lsp: Arc::new(server),
// sign_in_status: SignInStatus::Authorized, sign_in_status: SignInStatus::Authorized,
// registered_buffers: Default::default(), registered_buffers: Default::default(),
// }), }),
// buffers: Default::default(), _subscription: cx.on_app_quit(Self::shutdown_language_server),
// }); buffers: Default::default(),
// (this, fake_server) });
// } (this, fake_server)
}
fn start_language_server( fn start_language_server(
new_server_id: LanguageServerId, new_server_id: LanguageServerId,

View file

@ -27,7 +27,6 @@ client = { package = "client2", path = "../client2" }
clock = { path = "../clock" } clock = { path = "../clock" }
copilot = { package="copilot2", path = "../copilot2" } copilot = { package="copilot2", path = "../copilot2" }
db = { package="db2", path = "../db2" } db = { package="db2", path = "../db2" }
drag_and_drop = { path = "../drag_and_drop" }
collections = { path = "../collections" } collections = { path = "../collections" }
# context_menu = { path = "../context_menu" } # context_menu = { path = "../context_menu" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" }

View file

@ -578,12 +578,7 @@ impl DisplaySnapshot {
line.push_str(chunk.chunk); line.push_str(chunk.chunk);
let text_style = if let Some(style) = chunk.style { let text_style = if let Some(style) = chunk.style {
editor_style Cow::Owned(editor_style.text.clone().highlight(style))
.text
.clone()
.highlight(style)
.map(Cow::Owned)
.unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
} else { } else {
Cow::Borrowed(&editor_style.text) Cow::Borrowed(&editor_style.text)
}; };

View file

@ -2,9 +2,9 @@ use super::{
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
Highlights, Highlights,
}; };
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
use collections::{Bound, HashMap, HashSet}; use collections::{Bound, HashMap, HashSet};
use gpui::{AnyElement, ViewContext}; use gpui::{AnyElement, Pixels, ViewContext};
use language::{BufferSnapshot, Chunk, Patch, Point}; use language::{BufferSnapshot, Chunk, Patch, Point};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{ use std::{
@ -82,13 +82,13 @@ pub enum BlockStyle {
pub struct BlockContext<'a, 'b> { pub struct BlockContext<'a, 'b> {
pub view_context: &'b mut ViewContext<'a, Editor>, pub view_context: &'b mut ViewContext<'a, Editor>,
pub anchor_x: f32, pub anchor_x: Pixels,
pub scroll_x: f32, pub gutter_width: Pixels,
pub gutter_width: f32, pub gutter_padding: Pixels,
pub gutter_padding: f32, pub em_width: Pixels,
pub em_width: f32, pub line_height: Pixels,
pub line_height: f32,
pub block_id: usize, pub block_id: usize,
pub editor_style: &'b EditorStyle,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1220,8 +1220,6 @@ pub mod tests {
use super::*; use super::*;
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) { async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@ -1345,8 +1343,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) { async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -1458,8 +1454,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) { async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -1668,8 +1662,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) { async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@ -1998,8 +1990,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) { async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -2126,8 +2116,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) { async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -2411,8 +2399,6 @@ pub mod tests {
}); });
} }
// todo!()
#[ignore = "fails due to text.rs `measurement has not been performed` error"]
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) { async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -2455,14 +2441,9 @@ pub mod tests {
project.update(cx, |project, _| { project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language)) project.languages().add(Arc::clone(&language))
}); });
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let worktree_id = project.update(cx, |project, cx| {
let worktree_id = workspace project.worktrees().next().unwrap().read(cx).id()
.update(cx, |workspace, cx| { });
workspace.project().read_with(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
})
})
.unwrap();
let buffer_1 = project let buffer_1 = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
@ -2620,6 +2601,10 @@ pub mod tests {
"main hint #1".to_string(), "main hint #1".to_string(),
"main hint #2".to_string(), "main hint #2".to_string(),
"main hint #3".to_string(), "main hint #3".to_string(),
// todo!() there used to be no these hints, but new gpui2 presumably scrolls a bit farther
// (or renders less?) note that tests below pass
"main hint #4".to_string(),
"main hint #5".to_string(),
]; ];
assert_eq!( assert_eq!(
expected_hints, expected_hints,
@ -2755,8 +2740,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
}); });
} }
// todo!()
#[ignore = "fails due to text.rs `measurement has not been performed` error"]
#[gpui::test] #[gpui::test]
async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) { async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -2799,14 +2782,9 @@ all hints should be invalidated and requeried for all of its visible excerpts"
project.update(cx, |project, _| { project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language)) project.languages().add(Arc::clone(&language))
}); });
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let worktree_id = project.update(cx, |project, cx| {
let worktree_id = workspace project.worktrees().next().unwrap().read(cx).id()
.update(cx, |workspace, cx| { });
workspace.project().read_with(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
})
})
.unwrap();
let buffer_1 = project let buffer_1 = project
.update(cx, |project, cx| { .update(cx, |project, cx| {
@ -2985,8 +2963,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) { async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {
@ -3078,8 +3054,6 @@ all hints should be invalidated and requeried for all of its visible excerpts"
}); });
} }
// todo!()
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
#[gpui::test] #[gpui::test]
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) { async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| { init_test(cx, |settings| {

View file

@ -9,7 +9,7 @@ use collections::HashSet;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{ use gpui::{
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
ViewContext, VisualContext, WeakView, ViewContext, VisualContext, WeakView,
}; };
use language::{ use language::{
@ -30,6 +30,7 @@ use std::{
}; };
use text::Selection; use text::Selection;
use theme::{ActiveTheme, Theme}; use theme::{ActiveTheme, Theme};
use ui::{Label, TextColor};
use util::{paths::PathExt, ResultExt, TryFutureExt}; use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
use workspace::{ use workspace::{
@ -595,16 +596,19 @@ impl Item for Editor {
.flex_row() .flex_row()
.items_center() .items_center()
.gap_2() .gap_2()
.child(self.title(cx).to_string()) .child(Label::new(self.title(cx).to_string()))
.children(detail.and_then(|detail| { .children(detail.and_then(|detail| {
let path = path_for_buffer(&self.buffer, detail, false, cx)?; let path = path_for_buffer(&self.buffer, detail, false, cx)?;
let description = path.to_string_lossy(); let description = path.to_string_lossy();
Some( Some(
div() div().child(
.text_color(theme.colors().text_muted) Label::new(util::truncate_and_trailoff(
.text_xs() &description,
.child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)), MAX_TAB_TITLE_LEN,
))
.color(TextColor::Muted),
),
) )
})), })),
) )

View file

@ -11,19 +11,18 @@ pub enum ScrollAmount {
impl ScrollAmount { impl ScrollAmount {
pub fn lines(&self, editor: &mut Editor) -> f32 { pub fn lines(&self, editor: &mut Editor) -> f32 {
todo!() match self {
// match self { Self::Line(count) => *count,
// Self::Line(count) => *count, Self::Page(count) => editor
// Self::Page(count) => editor .visible_line_count()
// .visible_line_count() .map(|mut l| {
// .map(|mut l| { // for full pages subtract one to leave an anchor line
// // for full pages subtract one to leave an anchor line if count.abs() == 1.0 {
// if count.abs() == 1.0 { l -= 1.0
// l -= 1.0 }
// } (l * count).trunc()
// (l * count).trunc() })
// }) .unwrap_or(0.),
// .unwrap_or(0.), }
// }
} }
} }

View file

@ -315,11 +315,14 @@ impl SelectionsCollection {
let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details); let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
dbg!("****START COL****");
let start_col = layed_out_line.closest_index_for_x(positions.start) as u32; let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == layed_out_line.width) { if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
let start = DisplayPoint::new(row, start_col); let start = DisplayPoint::new(row, start_col);
dbg!("****END COL****");
let end_col = layed_out_line.closest_index_for_x(positions.end) as u32; let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col); let end = DisplayPoint::new(row, end_col);
dbg!(start_col, end_col);
Some(Selection { Some(Selection {
id: post_inc(&mut self.next_selection_id), id: post_inc(&mut self.next_selection_id),

View file

@ -1,81 +1,74 @@
pub mod editor_lsp_test_context; pub mod editor_lsp_test_context;
pub mod editor_test_context; pub mod editor_test_context;
// todo!() use crate::{
// use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, DisplayPoint, Editor, EditorMode, MultiBuffer,
// DisplayPoint, Editor, EditorMode, MultiBuffer, };
// };
// use gpui::{Model, ViewContext}; use gpui::{Context, Model, Pixels, ViewContext};
// use project::Project; use project::Project;
// use util::test::{marked_text_offsets, marked_text_ranges}; use util::test::{marked_text_offsets, marked_text_ranges};
// #[cfg(test)] #[cfg(test)]
// #[ctor::ctor] #[ctor::ctor]
// fn init_logger() { fn init_logger() {
// if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {
// env_logger::init(); env_logger::init();
// } }
// } }
// // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
// pub fn marked_display_snapshot( pub fn marked_display_snapshot(
// text: &str, text: &str,
// cx: &mut gpui::AppContext, cx: &mut gpui::AppContext,
// ) -> (DisplaySnapshot, Vec<DisplayPoint>) { ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
// let (unmarked_text, markers) = marked_text_offsets(text); let (unmarked_text, markers) = marked_text_offsets(text);
// let family_id = cx let font = cx.text_style().font();
// .font_cache() let font_size: Pixels = 14.into();
// .load_family(&["Helvetica"], &Default::default())
// .unwrap();
// let font_id = cx
// .font_cache()
// .select_font(family_id, &Default::default())
// .unwrap();
// let font_size = 14.0;
// let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
// let display_map = let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); let markers = markers
// let markers = markers .into_iter()
// .into_iter() .map(|offset| offset.to_display_point(&snapshot))
// .map(|offset| offset.to_display_point(&snapshot)) .collect();
// .collect();
// (snapshot, markers) (snapshot, markers)
// } }
// pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) { pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
// assert_eq!(editor.text(cx), unmarked_text); assert_eq!(editor.text(cx), unmarked_text);
// editor.change_selections(None, cx, |s| s.select_ranges(text_ranges)); editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
// } }
// pub fn assert_text_with_selections( pub fn assert_text_with_selections(
// editor: &mut Editor, editor: &mut Editor,
// marked_text: &str, marked_text: &str,
// cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
// ) { ) {
// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true); let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
// assert_eq!(editor.text(cx), unmarked_text); assert_eq!(editor.text(cx), unmarked_text);
// assert_eq!(editor.selections.ranges(cx), text_ranges); assert_eq!(editor.selections.ranges(cx), text_ranges);
// } }
// // RA thinks this is dead code even though it is used in a whole lot of tests // RA thinks this is dead code even though it is used in a whole lot of tests
// #[allow(dead_code)] #[allow(dead_code)]
// #[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
// pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor { pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
// Editor::new(EditorMode::Full, buffer, None, None, cx) // todo!()
// } Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
}
// pub(crate) fn build_editor_with_project( pub(crate) fn build_editor_with_project(
// project: Model<Project>, project: Model<Project>,
// buffer: Model<MultiBuffer>, buffer: Model<MultiBuffer>,
// cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
// ) -> Editor { ) -> Editor {
// Editor::new(EditorMode::Full, buffer, Some(project), None, cx) // todo!()
// } Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
}

View file

@ -1,297 +1,298 @@
// use std::{ use std::{
// borrow::Cow, borrow::Cow,
// ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
// sync::Arc, sync::Arc,
// }; };
// use anyhow::Result; use anyhow::Result;
use serde_json::json;
// use crate::{Editor, ToPoint}; use crate::{Editor, ToPoint};
// use collections::HashSet; use collections::HashSet;
// use futures::Future; use futures::Future;
// use gpui::{json, View, ViewContext}; use gpui::{View, ViewContext, VisualTestContext};
// use indoc::indoc; use indoc::indoc;
// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries}; use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
// use lsp::{notification, request}; use lsp::{notification, request};
// use multi_buffer::ToPointUtf16; use multi_buffer::ToPointUtf16;
// use project::Project; use project::Project;
// use smol::stream::StreamExt; use smol::stream::StreamExt;
// use workspace::{AppState, Workspace, WorkspaceHandle}; use workspace::{AppState, Workspace, WorkspaceHandle};
// use super::editor_test_context::EditorTestContext; use super::editor_test_context::{AssertionContextManager, EditorTestContext};
// pub struct EditorLspTestContext<'a> { pub struct EditorLspTestContext<'a> {
// pub cx: EditorTestContext<'a>, pub cx: EditorTestContext<'a>,
// pub lsp: lsp::FakeLanguageServer, pub lsp: lsp::FakeLanguageServer,
// pub workspace: View<Workspace>, pub workspace: View<Workspace>,
// pub buffer_lsp_url: lsp::Url, pub buffer_lsp_url: lsp::Url,
// } }
// impl<'a> EditorLspTestContext<'a> { impl<'a> EditorLspTestContext<'a> {
// pub async fn new( pub async fn new(
// mut language: Language, mut language: Language,
// capabilities: lsp::ServerCapabilities, capabilities: lsp::ServerCapabilities,
// cx: &'a mut gpui::TestAppContext, cx: &'a mut gpui::TestAppContext,
// ) -> EditorLspTestContext<'a> { ) -> EditorLspTestContext<'a> {
// use json::json; let app_state = cx.update(AppState::test);
// let app_state = cx.update(AppState::test); cx.update(|cx| {
language::init(cx);
crate::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
// cx.update(|cx| { let file_name = format!(
// language::init(cx); "file.{}",
// crate::init(cx); language
// workspace::init(app_state.clone(), cx); .path_suffixes()
// Project::init_settings(cx); .first()
// }); .expect("language must have a path suffix for EditorLspTestContext")
);
// let file_name = format!( let mut fake_servers = language
// "file.{}", .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// language capabilities,
// .path_suffixes() ..Default::default()
// .first() }))
// .expect("language must have a path suffix for EditorLspTestContext") .await;
// );
// let mut fake_servers = language let project = Project::test(app_state.fs.clone(), [], cx).await;
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
// capabilities,
// ..Default::default()
// }))
// .await;
// let project = Project::test(app_state.fs.clone(), [], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language)));
// project.update(cx, |project, _| project.languages().add(Arc::new(language)));
// app_state app_state
// .fs .fs
// .as_fake() .as_fake()
// .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
// .await; .await;
// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
// let workspace = window.root(cx);
// project
// .update(cx, |project, cx| {
// project.find_or_create_local_worktree("/root", true, cx)
// })
// .await
// .unwrap();
// cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
// .await;
// let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); let workspace = window.root_view(cx).unwrap();
// let item = workspace
// .update(cx, |workspace, cx| {
// workspace.open_path(file, None, true, cx)
// })
// .await
// .expect("Could not open test file");
// let editor = cx.update(|cx| { let mut cx = VisualTestContext::from_window(*window.deref(), cx);
// item.act_as::<Editor>(cx) project
// .expect("Opened test file wasn't an editor") .update(&mut cx, |project, cx| {
// }); project.find_or_create_local_worktree("/root", true, cx)
// editor.update(cx, |_, cx| cx.focus_self()); })
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(&mut cx, |workspace, cx| {
workspace.open_path(file, None, true, cx)
})
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(&mut cx, |editor, cx| editor.focus(cx));
// let lsp = fake_servers.next().await.unwrap(); let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window: window.into(),
editor,
assertion_cx: AssertionContextManager::new(),
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
}
}
// Self { pub async fn new_rust(
// cx: EditorTestContext { capabilities: lsp::ServerCapabilities,
// cx, cx: &'a mut gpui::TestAppContext,
// window: window.into(), ) -> EditorLspTestContext<'a> {
// editor, let language = Language::new(
// }, LanguageConfig {
// lsp, name: "Rust".into(),
// workspace, path_suffixes: vec!["rs".to_string()],
// buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(), ..Default::default()
// } },
// } Some(tree_sitter_rust::language()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
// pub async fn new_rust( (_ "[" "]" @end) @indent
// capabilities: lsp::ServerCapabilities, (_ "<" ">" @end) @indent
// cx: &'a mut gpui::TestAppContext, (_ "{" "}" @end) @indent
// ) -> EditorLspTestContext<'a> { (_ "(" ")" @end) @indent"#})),
// let language = Language::new( brackets: Some(Cow::from(indoc! {r#"
// LanguageConfig { ("(" @open ")" @close)
// name: "Rust".into(), ("[" @open "]" @close)
// path_suffixes: vec!["rs".to_string()], ("{" @open "}" @close)
// ..Default::default() ("<" @open ">" @close)
// }, ("\"" @open "\"" @close)
// Some(tree_sitter_rust::language()), (closure_parameters "|" @open "|" @close)"#})),
// ) ..Default::default()
// .with_queries(LanguageQueries { })
// indents: Some(Cow::from(indoc! {r#" .expect("Could not parse queries");
// [
// ((where_clause) _ @end)
// (field_expression)
// (call_expression)
// (assignment_expression)
// (let_declaration)
// (let_chain)
// (await_expression)
// ] @indent
// (_ "[" "]" @end) @indent Self::new(language, capabilities, cx).await
// (_ "<" ">" @end) @indent }
// (_ "{" "}" @end) @indent
// (_ "(" ")" @end) @indent"#})),
// brackets: Some(Cow::from(indoc! {r#"
// ("(" @open ")" @close)
// ("[" @open "]" @close)
// ("{" @open "}" @close)
// ("<" @open ">" @close)
// ("\"" @open "\"" @close)
// (closure_parameters "|" @open "|" @close)"#})),
// ..Default::default()
// })
// .expect("Could not parse queries");
// Self::new(language, capabilities, cx).await pub async fn new_typescript(
// } capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let mut word_characters: HashSet<char> = Default::default();
word_characters.insert('$');
word_characters.insert('#');
let language = Language::new(
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
brackets: language::BracketPairConfig {
pairs: vec![language::BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
disabled_scopes_by_bracket_ix: Default::default(),
},
word_characters,
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
indents: Some(Cow::from(indoc! {r#"
[
(call_expression)
(assignment_expression)
(member_expression)
(lexical_declaration)
(variable_declaration)
(assignment_expression)
(if_statement)
(for_statement)
] @indent
// pub async fn new_typescript( (_ "[" "]" @end) @indent
// capabilities: lsp::ServerCapabilities, (_ "<" ">" @end) @indent
// cx: &'a mut gpui::TestAppContext, (_ "{" "}" @end) @indent
// ) -> EditorLspTestContext<'a> { (_ "(" ")" @end) @indent
// let mut word_characters: HashSet<char> = Default::default(); "#})),
// word_characters.insert('$'); ..Default::default()
// word_characters.insert('#'); })
// let language = Language::new( .expect("Could not parse queries");
// LanguageConfig {
// name: "Typescript".into(),
// path_suffixes: vec!["ts".to_string()],
// brackets: language::BracketPairConfig {
// pairs: vec![language::BracketPair {
// start: "{".to_string(),
// end: "}".to_string(),
// close: true,
// newline: true,
// }],
// disabled_scopes_by_bracket_ix: Default::default(),
// },
// word_characters,
// ..Default::default()
// },
// Some(tree_sitter_typescript::language_typescript()),
// )
// .with_queries(LanguageQueries {
// brackets: Some(Cow::from(indoc! {r#"
// ("(" @open ")" @close)
// ("[" @open "]" @close)
// ("{" @open "}" @close)
// ("<" @open ">" @close)
// ("\"" @open "\"" @close)"#})),
// indents: Some(Cow::from(indoc! {r#"
// [
// (call_expression)
// (assignment_expression)
// (member_expression)
// (lexical_declaration)
// (variable_declaration)
// (assignment_expression)
// (if_statement)
// (for_statement)
// ] @indent
// (_ "[" "]" @end) @indent Self::new(language, capabilities, cx).await
// (_ "<" ">" @end) @indent }
// (_ "{" "}" @end) @indent
// (_ "(" ")" @end) @indent
// "#})),
// ..Default::default()
// })
// .expect("Could not parse queries");
// Self::new(language, capabilities, cx).await // Constructs lsp range using a marked string with '[', ']' range delimiters
// } pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
// // Constructs lsp range using a marked string with '[', ']' range delimiters pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
// pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range { let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// let ranges = self.ranges(marked_text); let start_point = range.start.to_point(&snapshot.buffer_snapshot);
// self.to_lsp_range(ranges[0].clone()) let end_point = range.end.to_point(&snapshot.buffer_snapshot);
// }
// pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range { self.editor(|editor, cx| {
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); let buffer = editor.buffer().read(cx);
// let start_point = range.start.to_point(&snapshot.buffer_snapshot); let start = point_to_lsp(
// let end_point = range.end.to_point(&snapshot.buffer_snapshot); buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
// self.editor(|editor, cx| { lsp::Range { start, end }
// let buffer = editor.buffer().read(cx); })
// let start = point_to_lsp( }
// buffer
// .point_to_buffer_offset(start_point, cx)
// .unwrap()
// .1
// .to_point_utf16(&buffer.read(cx)),
// );
// let end = point_to_lsp(
// buffer
// .point_to_buffer_offset(end_point, cx)
// .unwrap()
// .1
// .to_point_utf16(&buffer.read(cx)),
// );
// lsp::Range { start, end } pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
// }) let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// } let point = offset.to_point(&snapshot.buffer_snapshot);
// pub fn to_lsp(&mut self, offset: usize) -> lsp::Position { self.editor(|editor, cx| {
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx)); let buffer = editor.buffer().read(cx);
// let point = offset.to_point(&snapshot.buffer_snapshot); point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
// self.editor(|editor, cx| { pub fn update_workspace<F, T>(&mut self, update: F) -> T
// let buffer = editor.buffer().read(cx); where
// point_to_lsp( F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
// buffer {
// .point_to_buffer_offset(point, cx) self.workspace.update(&mut self.cx.cx, update)
// .unwrap() }
// .1
// .to_point_utf16(&buffer.read(cx)),
// )
// })
// }
// pub fn update_workspace<F, T>(&mut self, update: F) -> T pub fn handle_request<T, F, Fut>(
// where &self,
// F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T, mut handler: F,
// { ) -> futures::channel::mpsc::UnboundedReceiver<()>
// self.workspace.update(self.cx.cx, update) where
// } T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
// pub fn handle_request<T, F, Fut>( pub fn notify<T: notification::Notification>(&self, params: T::Params) {
// &self, self.lsp.notify::<T>(params);
// mut handler: F, }
// ) -> futures::channel::mpsc::UnboundedReceiver<()> }
// where
// T: 'static + request::Request,
// T::Params: 'static + Send,
// F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
// Fut: 'static + Send + Future<Output = Result<T::Result>>,
// {
// let url = self.buffer_lsp_url.clone();
// self.lsp.handle_request::<T, _, _>(move |params, cx| {
// let url = url.clone();
// handler(url, params, cx)
// })
// }
// pub fn notify<T: notification::Notification>(&self, params: T::Params) { impl<'a> Deref for EditorLspTestContext<'a> {
// self.lsp.notify::<T>(params); type Target = EditorTestContext<'a>;
// }
// }
// impl<'a> Deref for EditorLspTestContext<'a> { fn deref(&self) -> &Self::Target {
// type Target = EditorTestContext<'a>; &self.cx
}
}
// fn deref(&self) -> &Self::Target { impl<'a> DerefMut for EditorLspTestContext<'a> {
// &self.cx fn deref_mut(&mut self) -> &mut Self::Target {
// } &mut self.cx
// } }
}
// impl<'a> DerefMut for EditorLspTestContext<'a> {
// fn deref_mut(&mut self) -> &mut Self::Target {
// &mut self.cx
// }
// }

View file

@ -1,331 +1,400 @@
use crate::{ use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
}; };
use collections::BTreeMap;
use futures::Future; use futures::Future;
use gpui::{ use gpui::{
AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext, AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
VisualTestContext, WindowHandle,
}; };
use indoc::indoc; use indoc::indoc;
use itertools::Itertools;
use language::{Buffer, BufferSnapshot}; use language::{Buffer, BufferSnapshot};
use parking_lot::RwLock;
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use std::{ use std::{
any::TypeId, any::TypeId,
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
}; };
use util::{ use util::{
assert_set_eq, assert_set_eq,
test::{generate_marked_text, marked_text_ranges}, test::{generate_marked_text, marked_text_ranges},
}; };
// use super::build_editor_with_project; use super::build_editor_with_project;
// pub struct EditorTestContext<'a> { pub struct EditorTestContext<'a> {
// pub cx: &'a mut gpui::TestAppContext, pub cx: gpui::VisualTestContext<'a>,
// pub window: AnyWindowHandle, pub window: AnyWindowHandle,
// pub editor: View<Editor>, pub editor: View<Editor>,
// } pub assertion_cx: AssertionContextManager,
}
// impl<'a> EditorTestContext<'a> { impl<'a> EditorTestContext<'a> {
// pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
// let fs = FakeFs::new(cx.background()); let fs = FakeFs::new(cx.executor());
// // fs.insert_file("/file", "".to_owned()).await; // fs.insert_file("/file", "".to_owned()).await;
// fs.insert_tree( fs.insert_tree(
// "/root", "/root",
// gpui::serde_json::json!({ gpui::serde_json::json!({
// "file": "", "file": "",
// }), }),
// ) )
// .await; .await;
// let project = Project::test(fs, ["/root".as_ref()], cx).await; let project = Project::test(fs, ["/root".as_ref()], cx).await;
// let buffer = project let buffer = project
// .update(cx, |project, cx| { .update(cx, |project, cx| {
// project.open_local_buffer("/root/file", cx) project.open_local_buffer("/root/file", cx)
// }) })
// .await .await
// .unwrap(); .unwrap();
// let window = cx.add_window(|cx| { let editor = cx.add_window(|cx| {
// cx.focus_self(); let editor =
// build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx) build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
// }); editor.focus(cx);
// let editor = window.root(cx); editor
// Self { });
// cx, let editor_view = editor.root_view(cx).unwrap();
// window: window.into(), Self {
// editor, cx: VisualTestContext::from_window(*editor.deref(), cx),
// } window: editor.into(),
// } editor: editor_view,
assertion_cx: AssertionContextManager::new(),
}
}
// pub fn condition( pub fn condition(
// &self, &self,
// predicate: impl FnMut(&Editor, &AppContext) -> bool, predicate: impl FnMut(&Editor, &AppContext) -> bool,
// ) -> impl Future<Output = ()> { ) -> impl Future<Output = ()> {
// self.editor.condition(self.cx, predicate) self.editor.condition::<crate::Event>(&self.cx, predicate)
// } }
// pub fn editor<F, T>(&self, read: F) -> T #[track_caller]
// where pub fn editor<F, T>(&mut self, read: F) -> T
// F: FnOnce(&Editor, &ViewContext<Editor>) -> T, where
// { F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
// self.editor.update(self.cx, read) {
// } self.editor
.update(&mut self.cx, |this, cx| read(&this, &cx))
}
// pub fn update_editor<F, T>(&mut self, update: F) -> T #[track_caller]
// where pub fn update_editor<F, T>(&mut self, update: F) -> T
// F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T, where
// { F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
// self.editor.update(self.cx, update) {
// } self.editor.update(&mut self.cx, update)
}
// pub fn multibuffer<F, T>(&self, read: F) -> T pub fn multibuffer<F, T>(&mut self, read: F) -> T
// where where
// F: FnOnce(&MultiBuffer, &AppContext) -> T, F: FnOnce(&MultiBuffer, &AppContext) -> T,
// { {
// self.editor(|editor, cx| read(editor.buffer().read(cx), cx)) self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
// } }
// pub fn update_multibuffer<F, T>(&mut self, update: F) -> T pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
// where where
// F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T, F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
// { {
// self.update_editor(|editor, cx| editor.buffer().update(cx, update)) self.update_editor(|editor, cx| editor.buffer().update(cx, update))
// } }
// pub fn buffer_text(&self) -> String { pub fn buffer_text(&mut self) -> String {
// self.multibuffer(|buffer, cx| buffer.snapshot(cx).text()) self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
// } }
// pub fn buffer<F, T>(&self, read: F) -> T pub fn buffer<F, T>(&mut self, read: F) -> T
// where where
// F: FnOnce(&Buffer, &AppContext) -> T, F: FnOnce(&Buffer, &AppContext) -> T,
// { {
// self.multibuffer(|multibuffer, cx| { self.multibuffer(|multibuffer, cx| {
// let buffer = multibuffer.as_singleton().unwrap().read(cx); let buffer = multibuffer.as_singleton().unwrap().read(cx);
// read(buffer, cx) read(buffer, cx)
// }) })
// } }
// pub fn update_buffer<F, T>(&mut self, update: F) -> T pub fn update_buffer<F, T>(&mut self, update: F) -> T
// where where
// F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T, F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
// { {
// self.update_multibuffer(|multibuffer, cx| { self.update_multibuffer(|multibuffer, cx| {
// let buffer = multibuffer.as_singleton().unwrap(); let buffer = multibuffer.as_singleton().unwrap();
// buffer.update(cx, update) buffer.update(cx, update)
// }) })
// } }
// pub fn buffer_snapshot(&self) -> BufferSnapshot { pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
// self.buffer(|buffer, _| buffer.snapshot()) self.buffer(|buffer, _| buffer.snapshot())
// } }
// pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle { pub fn add_assertion_context(&self, context: String) -> ContextHandle {
// let keystroke_under_test_handle = self.assertion_cx.add_context(context)
// self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); }
// let keystroke = Keystroke::parse(keystroke_text).unwrap();
// self.cx.dispatch_keystroke(self.window, keystroke, false); pub fn assertion_context(&self) -> String {
self.assertion_cx.context()
}
// keystroke_under_test_handle pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
// } let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap();
// pub fn simulate_keystrokes<const COUNT: usize>( self.cx.dispatch_keystroke(self.window, keystroke, false);
// &mut self,
// keystroke_texts: [&str; COUNT],
// ) -> ContextHandle {
// let keystrokes_under_test_handle =
// self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
// for keystroke_text in keystroke_texts.into_iter() {
// self.simulate_keystroke(keystroke_text);
// }
// // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
// // before returning.
// // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// // quickly races with async actions.
// if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
// executor.run_until_parked();
// } else {
// unreachable!();
// }
// keystrokes_under_test_handle keystroke_under_test_handle
// } }
// pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> { pub fn simulate_keystrokes<const COUNT: usize>(
// let (unmarked_text, ranges) = marked_text_ranges(marked_text, false); &mut self,
// assert_eq!(self.buffer_text(), unmarked_text); keystroke_texts: [&str; COUNT],
// ranges ) -> ContextHandle {
// } let keystrokes_under_test_handle =
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
// it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
// before returning.
// NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// quickly races with async actions.
self.cx.background_executor.run_until_parked();
// pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint { keystrokes_under_test_handle
// let ranges = self.ranges(marked_text); }
// let snapshot = self
// .editor
// .update(self.cx, |editor, cx| editor.snapshot(cx));
// ranges[0].start.to_display_point(&snapshot)
// }
// // Returns anchors for the current buffer using `«` and `»` pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
// pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> { let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
// let ranges = self.ranges(marked_text); assert_eq!(self.buffer_text(), unmarked_text);
// let snapshot = self.buffer_snapshot(); ranges
// snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end) }
// }
// pub fn set_diff_base(&mut self, diff_base: Option<&str>) { pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
// let diff_base = diff_base.map(String::from); let ranges = self.ranges(marked_text);
// self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx)); let snapshot = self
// } .editor
.update(&mut self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// /// Change the editor's text and selections using a string containing // Returns anchors for the current buffer using `«` and `»`
// /// embedded range markers that represent the ranges and directions of pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
// /// each selection. let ranges = self.ranges(marked_text);
// /// let snapshot = self.buffer_snapshot();
// /// Returns a context handle so that assertion failures can print what snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
// /// editor state was needed to cause the failure. }
// ///
// /// See the `util::test::marked_text_ranges` function for more information.
// pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
// let state_context = self.add_assertion_context(format!(
// "Initial Editor State: \"{}\"",
// marked_text.escape_debug().to_string()
// ));
// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
// self.editor.update(self.cx, |editor, cx| {
// editor.set_text(unmarked_text, cx);
// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
// s.select_ranges(selection_ranges)
// })
// });
// state_context
// }
// /// Only change the editor's selections pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
// pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle { let diff_base = diff_base.map(String::from);
// let state_context = self.add_assertion_context(format!( self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
// "Initial Editor State: \"{}\"", }
// marked_text.escape_debug().to_string()
// ));
// let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
// self.editor.update(self.cx, |editor, cx| {
// assert_eq!(editor.text(cx), unmarked_text);
// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
// s.select_ranges(selection_ranges)
// })
// });
// state_context
// }
// /// Make an assertion about the editor's text and the ranges and directions /// Change the editor's text and selections using a string containing
// /// of its selections using a string containing embedded range markers. /// embedded range markers that represent the ranges and directions of
// /// /// each selection.
// /// See the `util::test::marked_text_ranges` function for more information. ///
// #[track_caller] /// Returns a context handle so that assertion failures can print what
// pub fn assert_editor_state(&mut self, marked_text: &str) { /// editor state was needed to cause the failure.
// let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true); ///
// let buffer_text = self.buffer_text(); /// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(&mut self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
// if buffer_text != unmarked_text { /// Only change the editor's selections
// panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}"); pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
// } let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(&mut self.cx, |editor, cx| {
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
// self.assert_selections(expected_selections, marked_text.to_string()) /// Make an assertion about the editor's text and the ranges and directions
// } /// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
// pub fn editor_state(&mut self) -> String { if buffer_text != unmarked_text {
// generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
// } }
// #[track_caller] self.assert_selections(expected_selections, marked_text.to_string())
// pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) { }
// let expected_ranges = self.ranges(marked_text);
// let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
// let snapshot = editor.snapshot(cx);
// editor
// .background_highlights
// .get(&TypeId::of::<Tag>())
// .map(|h| h.1.clone())
// .unwrap_or_default()
// .into_iter()
// .map(|range| range.to_offset(&snapshot.buffer_snapshot))
// .collect()
// });
// assert_set_eq!(actual_ranges, expected_ranges);
// }
// #[track_caller] pub fn editor_state(&mut self) -> String {
// pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) { generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
// let expected_ranges = self.ranges(marked_text); }
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// let actual_ranges: Vec<Range<usize>> = snapshot
// .text_highlight_ranges::<Tag>()
// .map(|ranges| ranges.as_ref().clone().1)
// .unwrap_or_default()
// .into_iter()
// .map(|range| range.to_offset(&snapshot.buffer_snapshot))
// .collect();
// assert_set_eq!(actual_ranges, expected_ranges);
// }
// #[track_caller] #[track_caller]
// pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) { pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
// let expected_marked_text = let expected_ranges = self.ranges(marked_text);
// generate_marked_text(&self.buffer_text(), &expected_selections, true); let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
// self.assert_selections(expected_selections, expected_marked_text) let snapshot = editor.snapshot(cx);
// } editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
// fn editor_selections(&self) -> Vec<Range<usize>> { #[track_caller]
// self.editor pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
// .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx)) let expected_ranges = self.ranges(marked_text);
// .into_iter() let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
// .map(|s| { let actual_ranges: Vec<Range<usize>> = snapshot
// if s.reversed { .text_highlight_ranges::<Tag>()
// s.end..s.start .map(|ranges| ranges.as_ref().clone().1)
// } else { .unwrap_or_default()
// s.start..s.end .into_iter()
// } .map(|range| range.to_offset(&snapshot.buffer_snapshot))
// }) .collect();
// .collect::<Vec<_>>() assert_set_eq!(actual_ranges, expected_ranges);
// } }
// #[track_caller] #[track_caller]
// fn assert_selections( pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
// &mut self, let expected_marked_text =
// expected_selections: Vec<Range<usize>>, generate_marked_text(&self.buffer_text(), &expected_selections, true);
// expected_marked_text: String, self.assert_selections(expected_selections, expected_marked_text)
// ) { }
// let actual_selections = self.editor_selections();
// let actual_marked_text =
// generate_marked_text(&self.buffer_text(), &actual_selections, true);
// if expected_selections != actual_selections {
// panic!(
// indoc! {"
// {}Editor has unexpected selections. #[track_caller]
fn editor_selections(&mut self) -> Vec<Range<usize>> {
self.editor
.update(&mut self.cx, |editor, cx| {
editor.selections.all::<usize>(cx)
})
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>()
}
// Expected selections: #[track_caller]
// {} fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
// Actual selections: {}Editor has unexpected selections.
// {}
// "},
// self.assertion_context(),
// expected_marked_text,
// actual_marked_text,
// );
// }
// }
// }
//
// impl<'a> Deref for EditorTestContext<'a> {
// type Target = gpui::TestAppContext;
// fn deref(&self) -> &Self::Target { Expected selections:
// self.cx {}
// }
// }
// impl<'a> DerefMut for EditorTestContext<'a> { Actual selections:
// fn deref_mut(&mut self) -> &mut Self::Target { {}
// &mut self.cx "},
// } self.assertion_context(),
// } expected_marked_text,
actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}
/// Tracks string context to be printed when assertions fail.
/// Often this is done by storing a context string in the manager and returning the handle.
#[derive(Clone)]
pub struct AssertionContextManager {
id: Arc<AtomicUsize>,
contexts: Arc<RwLock<BTreeMap<usize, String>>>,
}
impl AssertionContextManager {
pub fn new() -> Self {
Self {
id: Arc::new(AtomicUsize::new(0)),
contexts: Arc::new(RwLock::new(BTreeMap::new())),
}
}
pub fn add_context(&self, context: String) -> ContextHandle {
let id = self.id.fetch_add(1, Ordering::Relaxed);
let mut contexts = self.contexts.write();
contexts.insert(id, context);
ContextHandle {
id,
manager: self.clone(),
}
}
pub fn context(&self) -> String {
let contexts = self.contexts.read();
format!("\n{}\n", contexts.values().join("\n"))
}
}
/// Used to track the lifetime of a piece of context so that it can be provided when an assertion fails.
/// For example, in the EditorTestContext, `set_state` returns a context handle so that if an assertion fails,
/// the state that was set initially for the failure can be printed in the error message
pub struct ContextHandle {
id: usize,
manager: AssertionContextManager,
}
impl Drop for ContextHandle {
fn drop(&mut self) {
let mut contexts = self.manager.contexts.write();
contexts.remove(&self.id);
}
}

View file

@ -0,0 +1,37 @@
[package]
name = "file_finder2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/file_finder.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
collections = { path = "../collections" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
picker = { package = "picker2", path = "../picker2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
text = { package = "text2", path = "../text2" }
util = { path = "../util" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }
workspace = { package = "workspace2", path = "../workspace2" }
postage.workspace = true
serde.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
serde_json.workspace = true
ctor.workspace = true
env_logger.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,11 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{ use gpui::{
actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString, actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
VisualContext, WindowContext,
}; };
use text::{Bias, Point}; use text::{Bias, Point};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, modal, v_stack, Label, LabelColor}; use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
use util::paths::FILE_ROW_COLUMN_DELIMITER; use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::{Modal, ModalEvent, Workspace}; use workspace::{Modal, ModalEvent, Workspace};
@ -146,11 +145,12 @@ impl GoToLine {
} }
impl Render for GoToLine { impl Render for GoToLine {
type Element = Div<Self, StatefulInteractivity<Self>>; type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
modal(cx) div()
.id("go to line") .elevation_2(cx)
.key_context("GoToLine")
.on_action(Self::cancel) .on_action(Self::cancel)
.on_action(Self::confirm) .on_action(Self::confirm)
.w_96() .w_96()
@ -176,7 +176,7 @@ impl Render for GoToLine {
.justify_between() .justify_between()
.px_2() .px_2()
.py_1() .py_1()
.child(Label::new(self.current_text.clone()).color(LabelColor::Muted)), .child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
), ),
) )
} }

View file

@ -2112,6 +2112,10 @@ impl AppContext {
AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()) AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
} }
pub fn open_url(&self, url: &str) {
self.platform.open_url(url)
}
pub fn write_to_clipboard(&self, item: ClipboardItem) { pub fn write_to_clipboard(&self, item: ClipboardItem) {
self.platform.write_to_clipboard(item); self.platform.write_to_clipboard(item);
} }

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,41 @@
# Contexts
GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services.
There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead.
```
AppContext
/ \
ModelContext WindowContext
/
ViewContext
```
- The `AppContext` forms the root of the hierarchy
- `ModelContext` and `WindowContext` both dereference to `AppContext`
- `ViewContext` dereferences to `WindowContext`
## `AppContext`
Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model<T>` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`...
## `WindowContext`
Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`.
## `ModelContext<T>`
Available when you create or update a `Model<T>`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events.
## `ViewContext<V>`
Available when you create or update a `View<V>`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events.
## `AsyncAppContext` and `AsyncWindowContext`
Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
## `TestAppContext` and `TestVisualContext`
These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.

View file

@ -0,0 +1,101 @@
# Key Dispatch
GPUI is designed for keyboard-first interactivity.
To expose functionality to the mouse, you render a button with a click handler.
To expose functionality to the keyboard, you bind an *action* in a *key context*.
Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
```rust
mod menu {
#[gpui::action]
struct MoveUp;
#[gpui::action]
struct MoveDown;
}
```
Actions are frequently unit structs, for which we have a macro. The above could also be written:
```rust
mod menu {
actions!(MoveUp, MoveDown);
}
```
Actions can also be more complex types:
```rust
mod menu {
#[gpui::action]
struct Move {
direction: Direction,
select: bool,
}
}
```
To bind actions, chain `on_action` on to your element:
```rust
impl Render for Menu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
div()
.on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
// ...
})
.on_action(|this, move: &MoveDown, cx| {
// ...
})
.children(todo!())
}
}
```
In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`.
```rust
impl Render for Menu {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
div()
.key_context("menu")
.on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
// ...
})
.on_action(|this, move: &MoveDown, cx| {
// ...
})
.children(todo!())
}
}
```
Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
```json
{
"context": "menu",
"bindings": {
"up": "menu::MoveUp",
"down": "menu::MoveDown"
}
}
```
If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
```json
{
"context": "menu",
"bindings": {
"up": ["menu::Move", {direction: "up", select: false}]
"down": ["menu::Move", {direction: "down", select: false}]
"shift-up": ["menu::Move", {direction: "up", select: true}]
"shift-down": ["menu::Move", {direction: "down", select: true}]
}
}
```

View file

@ -1,6 +1,6 @@
use crate::SharedString; use crate::SharedString;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::{HashMap, HashSet}; use collections::HashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard}; use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use serde::Deserialize; use serde::Deserialize;
@ -68,8 +68,12 @@ where
A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
{ {
fn qualified_name() -> SharedString { fn qualified_name() -> SharedString {
let name = type_name::<A>();
let mut separator_matches = name.rmatch_indices("::");
separator_matches.next().unwrap();
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
// todo!() remove the 2 replacement when migration is done // todo!() remove the 2 replacement when migration is done
type_name::<A>().replace("2::", "::").into() name[name_start_ix..].replace("2::", "::").into()
} }
fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>> fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
@ -176,8 +180,7 @@ macro_rules! actions {
() => {}; () => {};
( $name:ident ) => { ( $name:ident ) => {
#[gpui::register_action] #[gpui::action]
#[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
pub struct $name; pub struct $name;
}; };
@ -186,401 +189,3 @@ macro_rules! actions {
actions!($($rest)*); actions!($($rest)*);
}; };
} }
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct DispatchContext {
set: HashSet<SharedString>,
map: HashMap<SharedString, SharedString>,
}
impl<'a> TryFrom<&'a str> for DispatchContext {
type Error = anyhow::Error;
fn try_from(value: &'a str) -> Result<Self> {
Self::parse(value)
}
}
impl DispatchContext {
pub fn parse(source: &str) -> Result<Self> {
let mut context = Self::default();
let source = skip_whitespace(source);
Self::parse_expr(&source, &mut context)?;
Ok(context)
}
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
if source.is_empty() {
return Ok(());
}
let key = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[key.len()..]);
if let Some(suffix) = source.strip_prefix('=') {
source = skip_whitespace(suffix);
let value = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[value.len()..]);
context.set(key, value);
} else {
context.insert(key);
}
Self::parse_expr(source, context)
}
pub fn is_empty(&self) -> bool {
self.set.is_empty() && self.map.is_empty()
}
pub fn clear(&mut self) {
self.set.clear();
self.map.clear();
}
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());
}
}
pub fn insert<I: Into<SharedString>>(&mut self, identifier: I) {
self.set.insert(identifier.into());
}
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
self.map.insert(key.into(), value.into());
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum DispatchContextPredicate {
Identifier(SharedString),
Equal(SharedString, SharedString),
NotEqual(SharedString, SharedString),
Child(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
Not(Box<DispatchContextPredicate>),
And(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
Or(Box<DispatchContextPredicate>, Box<DispatchContextPredicate>),
}
impl DispatchContextPredicate {
pub fn parse(source: &str) -> Result<Self> {
let source = skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
pub fn eval(&self, contexts: &[&DispatchContext]) -> bool {
let Some(context) = contexts.last() else {
return false;
};
match self {
Self::Identifier(name) => context.set.contains(name),
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(contexts),
Self::Child(parent, child) => {
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
}
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op = fn(
DispatchContextPredicate,
DispatchContextPredicate,
) -> Result<DispatchContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
Ok((DispatchContextPredicate::Not(Box::new(predicate)), source))
}
_ if is_identifier_char(next) => {
let len = source
.find(|c: char| !is_identifier_char(c))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = skip_whitespace(rest);
Ok((
DispatchContextPredicate::Identifier(identifier.to_string().into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
fn is_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
#[cfg(test)]
mod tests {
use super::*;
use crate as gpui;
use DispatchContextPredicate::*;
#[test]
fn test_actions_definition() {
{
actions!(A, B, C, D, E, F, G);
}
{
actions!(
A,
B,
C,
D,
E,
F,
G, // Don't wrap, test the trailing comma
);
}
}
#[test]
fn test_parse_context() {
let mut expected = DispatchContext::default();
expected.set("foo", "bar");
expected.insert("baz");
assert_eq!(DispatchContext::parse("baz foo=bar").unwrap(), expected);
assert_eq!(DispatchContext::parse("foo = bar baz").unwrap(), expected);
assert_eq!(
DispatchContext::parse(" baz foo = bar baz").unwrap(),
expected
);
assert_eq!(DispatchContext::parse(" foo = bar baz").unwrap(), expected);
}
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
DispatchContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
DispatchContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
DispatchContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
DispatchContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
DispatchContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
DispatchContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
DispatchContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
DispatchContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
DispatchContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
DispatchContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
DispatchContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
DispatchContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
DispatchContextPredicate::parse("a && (b == c || d != e)").unwrap(),
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!(
DispatchContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View file

@ -234,10 +234,10 @@ impl AppContext {
app_version: platform.app_version().ok(), app_version: platform.app_version().ok(),
}; };
Rc::new_cyclic(|this| AppCell { let app = Rc::new_cyclic(|this| AppCell {
app: RefCell::new(AppContext { app: RefCell::new(AppContext {
this: this.clone(), this: this.clone(),
platform, platform: platform.clone(),
app_metadata, app_metadata,
text_system, text_system,
flushing_effects: false, flushing_effects: false,
@ -269,12 +269,21 @@ impl AppContext {
layout_id_buffer: Default::default(), layout_id_buffer: Default::default(),
propagate_event: true, propagate_event: true,
}), }),
}) });
platform.on_quit(Box::new({
let cx = app.clone();
move || {
cx.borrow_mut().shutdown();
}
}));
app
} }
/// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit` /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
/// will be given 100ms to complete before exiting. /// will be given 100ms to complete before exiting.
pub fn quit(&mut self) { pub fn shutdown(&mut self) {
let mut futures = Vec::new(); let mut futures = Vec::new();
for observer in self.quit_observers.remove(&()) { for observer in self.quit_observers.remove(&()) {
@ -292,8 +301,10 @@ impl AppContext {
{ {
log::error!("timed out waiting on app_will_quit"); log::error!("timed out waiting on app_will_quit");
} }
}
self.globals_by_type.clear(); pub fn quit(&mut self) {
self.platform.quit();
} }
pub fn app_metadata(&self) -> AppMetadata { pub fn app_metadata(&self) -> AppMetadata {
@ -431,6 +442,18 @@ impl AppContext {
self.platform.activate(ignoring_other_apps); self.platform.activate(ignoring_other_apps);
} }
pub fn hide(&self) {
self.platform.hide();
}
pub fn hide_other_apps(&self) {
self.platform.hide_other_apps();
}
pub fn unhide_other_apps(&self) {
self.platform.unhide_other_apps();
}
/// Returns the list of currently active displays. /// Returns the list of currently active displays.
pub fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> { pub fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
self.platform.displays() self.platform.displays()
@ -641,14 +664,19 @@ impl AppContext {
// The window might change focus multiple times in an effect cycle. // The window might change focus multiple times in an effect cycle.
// We only honor effects for the most recently focused handle. // We only honor effects for the most recently focused handle.
if cx.window.focus == focused { if cx.window.focus == focused {
// if someone calls focus multiple times in one frame with the same handle
// the first apply_focus_changed_effect will have taken the last blur already
// and run the rest of this, so we can return.
let Some(last_blur) = cx.window.last_blur.take() else {
return;
};
let focused = focused let focused = focused
.map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap()); .map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap());
let blurred = cx
.window let blurred =
.last_blur last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
.take()
.unwrap()
.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
let focus_changed = focused.is_some() || blurred.is_some(); let focus_changed = focused.is_some() || blurred.is_some();
let event = FocusEvent { focused, blurred }; let event = FocusEvent { focused, blurred };
@ -1007,6 +1035,29 @@ impl Context for AppContext {
let entity = self.entities.read(handle); let entity = self.entities.read(handle);
read(entity, self) read(entity, self)
} }
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
let window = self
.windows
.get(window.id)
.ok_or_else(|| anyhow!("window not found"))?
.as_ref()
.unwrap();
let root_view = window.root_view.clone().unwrap();
let view = root_view
.downcast::<T>()
.map_err(|_| anyhow!("root view's type has changed"))?;
Ok(read(view, self))
}
} }
/// These effects are processed at the end of each application update cycle. /// These effects are processed at the end of each application update cycle.
@ -1063,7 +1114,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
/// Contains state associated with an active drag operation, started by dragging an element /// Contains state associated with an active drag operation, started by dragging an element
/// within the window or by dragging into the app from the underlying platform. /// within the window or by dragging into the app from the underlying platform.
pub(crate) struct AnyDrag { pub struct AnyDrag {
pub view: AnyView, pub view: AnyView,
pub cursor_offset: Point<Pixels>, pub cursor_offset: Point<Pixels>,
} }

View file

@ -66,6 +66,19 @@ impl Context for AsyncAppContext {
let mut lock = app.borrow_mut(); let mut lock = app.borrow_mut();
lock.update_window(window, f) lock.update_window(window, f)
} }
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.upgrade().context("app was released")?;
let lock = app.borrow();
lock.read_window(window, read)
}
} }
impl AsyncAppContext { impl AsyncAppContext {
@ -250,6 +263,17 @@ impl Context for AsyncWindowContext {
{ {
self.app.read_model(handle, read) self.app.read_model(handle, read)
} }
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
self.app.read_window(window, read)
}
} }
impl VisualContext for AsyncWindowContext { impl VisualContext for AsyncWindowContext {

View file

@ -26,7 +26,7 @@ impl EntityId {
impl Display for EntityId { impl Display for EntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self) write!(f, "{}", self.as_u64())
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId, AnyView, AnyWindowHandle, AppContext, AsyncAppContext, Context, Effect, Entity, EntityId,
EventEmitter, Model, Subscription, Task, WeakModel, WindowContext, EventEmitter, Model, Subscription, Task, View, WeakModel, WindowContext, WindowHandle,
}; };
use anyhow::Result; use anyhow::Result;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
@ -239,6 +239,17 @@ impl<'a, T> Context for ModelContext<'a, T> {
{ {
self.app.read_model(handle, read) self.app.read_model(handle, read)
} }
fn read_window<U, R>(
&self,
window: &WindowHandle<U>,
read: impl FnOnce(View<U>, &AppContext) -> R,
) -> Result<R>
where
U: 'static,
{
self.app.read_window(window, read)
}
} }
impl<T> Borrow<AppContext> for ModelContext<'_, T> { impl<T> Borrow<AppContext> for ModelContext<'_, T> {

View file

@ -1,12 +1,12 @@
use crate::{ use crate::{
AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context, div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
Render, Result, Task, TestDispatcher, TestPlatform, ViewContext, VisualContext, WindowContext, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
WindowHandle, WindowOptions, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
}; };
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use std::{future::Future, rc::Rc, sync::Arc, time::Duration}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
#[derive(Clone)] #[derive(Clone)]
pub struct TestAppContext { pub struct TestAppContext {
@ -14,6 +14,7 @@ pub struct TestAppContext {
pub background_executor: BackgroundExecutor, pub background_executor: BackgroundExecutor,
pub foreground_executor: ForegroundExecutor, pub foreground_executor: ForegroundExecutor,
pub dispatcher: TestDispatcher, pub dispatcher: TestDispatcher,
pub test_platform: Rc<TestPlatform>,
} }
impl Context for TestAppContext { impl Context for TestAppContext {
@ -58,6 +59,18 @@ impl Context for TestAppContext {
let app = self.app.borrow(); let app = self.app.borrow();
app.read_model(handle, read) app.read_model(handle, read)
} }
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
let app = self.app.borrow();
app.read_window(window, read)
}
} }
impl TestAppContext { impl TestAppContext {
@ -65,17 +78,16 @@ impl TestAppContext {
let arc_dispatcher = Arc::new(dispatcher.clone()); let arc_dispatcher = Arc::new(dispatcher.clone());
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(arc_dispatcher); let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
let platform = Rc::new(TestPlatform::new( let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
background_executor.clone(),
foreground_executor.clone(),
));
let asset_source = Arc::new(()); let asset_source = Arc::new(());
let http_client = util::http::FakeHttpClient::with_404_response(); let http_client = util::http::FakeHttpClient::with_404_response();
Self { Self {
app: AppContext::new(platform, asset_source, http_client), app: AppContext::new(platform.clone(), asset_source, http_client),
background_executor, background_executor,
foreground_executor, foreground_executor,
dispatcher: dispatcher.clone(), dispatcher: dispatcher.clone(),
test_platform: platform,
} }
} }
@ -84,7 +96,7 @@ impl TestAppContext {
} }
pub fn quit(&self) { pub fn quit(&self) {
self.app.borrow_mut().quit(); self.app.borrow_mut().shutdown();
} }
pub fn refresh(&mut self) -> Result<()> { pub fn refresh(&mut self) -> Result<()> {
@ -93,8 +105,8 @@ impl TestAppContext {
Ok(()) Ok(())
} }
pub fn executor(&self) -> &BackgroundExecutor { pub fn executor(&self) -> BackgroundExecutor {
&self.background_executor self.background_executor.clone()
} }
pub fn foreground_executor(&self) -> &ForegroundExecutor { pub fn foreground_executor(&self) -> &ForegroundExecutor {
@ -120,6 +132,41 @@ impl TestAppContext {
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
} }
pub fn add_empty_window(&mut self) -> AnyWindowHandle {
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| {
cx.build_view(|_| EmptyView {})
})
.any_handle
}
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: Render,
{
let mut cx = self.app.borrow_mut();
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
drop(cx);
let view = window.root_view(self).unwrap();
(view, VisualTestContext::from_window(*window.deref(), self))
}
pub fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
self.test_platform.simulate_new_path_selection(select_path);
}
pub fn simulate_prompt_answer(&self, button_ix: usize) {
self.test_platform.simulate_prompt_answer(button_ix);
}
pub fn has_pending_prompt(&self) -> bool {
self.test_platform.has_pending_prompt()
}
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R> pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where where
Fut: Future<Output = R> + 'static, Fut: Future<Output = R> + 'static,
@ -146,6 +193,11 @@ impl TestAppContext {
Some(read(lock.try_global()?, &lock)) Some(read(lock.try_global()?, &lock))
} }
pub fn set_global<G: 'static>(&mut self, global: G) {
let mut lock = self.app.borrow_mut();
lock.set_global(global);
}
pub fn update_global<G: 'static, R>( pub fn update_global<G: 'static, R>(
&mut self, &mut self,
update: impl FnOnce(&mut G, &mut AppContext) -> R, update: impl FnOnce(&mut G, &mut AppContext) -> R,
@ -162,6 +214,15 @@ impl TestAppContext {
} }
} }
pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
where
A: Action,
{
window
.update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
.unwrap()
}
pub fn dispatch_keystroke( pub fn dispatch_keystroke(
&mut self, &mut self,
window: AnyWindowHandle, window: AnyWindowHandle,
@ -259,3 +320,198 @@ impl<T: Send> Model<T> {
.expect("model was dropped") .expect("model was dropped")
} }
} }
impl<V> View<V> {
pub fn condition<Evt>(
&self,
cx: &TestAppContext,
mut predicate: impl FnMut(&V, &AppContext) -> bool,
) -> impl Future<Output = ()>
where
Evt: 'static,
V: EventEmitter<Evt>,
{
use postage::prelude::{Sink as _, Stream as _};
let (tx, mut rx) = postage::mpsc::channel(1024);
let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration();
let mut cx = cx.app.borrow_mut();
let subscriptions = (
cx.observe(self, {
let mut tx = tx.clone();
move |_, _| {
tx.blocking_send(()).ok();
}
}),
cx.subscribe(self, {
let mut tx = tx.clone();
move |_, _: &Evt, _| {
tx.blocking_send(()).ok();
}
}),
);
let cx = cx.this.upgrade().unwrap();
let handle = self.downgrade();
async move {
crate::util::timeout(timeout_duration, async move {
loop {
{
let cx = cx.borrow();
let cx = &*cx;
if predicate(
handle
.upgrade()
.expect("view dropped with pending condition")
.read(cx),
cx,
) {
break;
}
}
// todo!(start_waiting)
// cx.borrow().foreground_executor().start_waiting();
rx.recv()
.await
.expect("view dropped with pending condition");
// cx.borrow().foreground_executor().finish_waiting();
}
})
.await
.expect("condition timed out");
drop(subscriptions);
}
}
}
use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut)]
pub struct VisualTestContext<'a> {
#[deref]
#[deref_mut]
cx: &'a mut TestAppContext,
window: AnyWindowHandle,
}
impl<'a> VisualTestContext<'a> {
pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
Self { cx, window }
}
pub fn dispatch_action<A>(&mut self, action: A)
where
A: Action,
{
self.cx.dispatch_action(self.window, action)
}
}
impl<'a> Context for VisualTestContext<'a> {
type Result<T> = <TestAppContext as Context>::Result<T>;
fn build_model<T: 'static>(
&mut self,
build_model: impl FnOnce(&mut ModelContext<'_, T>) -> T,
) -> Self::Result<Model<T>> {
self.cx.build_model(build_model)
}
fn update_model<T, R>(
&mut self,
handle: &Model<T>,
update: impl FnOnce(&mut T, &mut ModelContext<'_, T>) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.cx.update_model(handle, update)
}
fn read_model<T, R>(
&self,
handle: &Model<T>,
read: impl FnOnce(&T, &AppContext) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.cx.read_model(handle, read)
}
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T,
{
self.cx.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static,
{
self.cx.read_window(window, read)
}
}
impl<'a> VisualContext for VisualTestContext<'a> {
fn build_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: 'static + Render,
{
self.window
.update(self.cx, |_, cx| cx.build_view(build_view))
.unwrap()
}
fn update_view<V: 'static, R>(
&mut self,
view: &View<V>,
update: impl FnOnce(&mut V, &mut ViewContext<'_, V>) -> R,
) -> Self::Result<R> {
self.window
.update(self.cx, |_, cx| cx.update_view(view, update))
.unwrap()
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
V: Render,
{
self.window
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
.unwrap()
}
}
impl AnyWindowHandle {
pub fn build_view<V: Render + 'static>(
&self,
cx: &mut TestAppContext,
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> View<V> {
self.update(cx, |_, cx| cx.build_view(build_view)).unwrap()
}
}
pub struct EmptyView {}
impl Render for EmptyView {
type Element = Div<Self>;
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
div()
}
}

View file

@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba {
} }
} }
#[derive(Default, Copy, Clone, Debug, PartialEq)] #[derive(Default, Copy, Clone, Debug)]
#[repr(C)] #[repr(C)]
pub struct Hsla { pub struct Hsla {
pub h: f32, pub h: f32,
@ -176,10 +176,63 @@ pub struct Hsla {
pub a: f32, pub a: f32,
} }
impl PartialEq for Hsla {
fn eq(&self, other: &Self) -> bool {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
.is_eq()
}
}
impl PartialOrd for Hsla {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// SAFETY: The total ordering relies on this always being Some()
Some(
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a))),
)
}
}
impl Ord for Hsla {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// SAFETY: The partial comparison is a total comparison
unsafe { self.partial_cmp(other).unwrap_unchecked() }
}
}
impl Hsla { impl Hsla {
pub fn to_rgb(self) -> Rgba { pub fn to_rgb(self) -> Rgba {
self.into() self.into()
} }
pub fn red() -> Self {
red()
}
pub fn green() -> Self {
green()
}
pub fn blue() -> Self {
blue()
}
pub fn black() -> Self {
black()
}
pub fn white() -> Self {
white()
}
pub fn transparent_black() -> Self {
transparent_black()
}
} }
impl Eq for Hsla {} impl Eq for Hsla {}
@ -238,6 +291,24 @@ pub fn blue() -> Hsla {
} }
} }
pub fn green() -> Hsla {
Hsla {
h: 0.33,
s: 1.,
l: 0.5,
a: 1.,
}
}
pub fn yellow() -> Hsla {
Hsla {
h: 0.16,
s: 1.,
l: 0.5,
a: 1.,
}
}
impl Hsla { impl Hsla {
/// Returns true if the HSLA color is fully transparent, false otherwise. /// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool { pub fn is_transparent(&self) -> bool {

View file

@ -8,7 +8,7 @@ use std::{any::Any, mem};
pub trait Element<V: 'static> { pub trait Element<V: 'static> {
type ElementState: 'static; type ElementState: 'static;
fn id(&self) -> Option<ElementId>; fn element_id(&self) -> Option<ElementId>;
/// Called to initialize this element for the current frame. If this /// Called to initialize this element for the current frame. If this
/// element had state in a previous frame, it will be passed in for the 3rd argument. /// element had state in a previous frame, it will be passed in for the 3rd argument.
@ -38,7 +38,7 @@ pub trait Element<V: 'static> {
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>); pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
pub trait ParentElement<V: 'static> { pub trait ParentComponent<V: 'static> {
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>; fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
fn child(mut self, child: impl Component<V>) -> Self fn child(mut self, child: impl Component<V>) -> Self
@ -120,7 +120,7 @@ where
E::ElementState: 'static, E::ElementState: 'static,
{ {
fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) { fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
let frame_state = if let Some(id) = self.element.id() { let frame_state = if let Some(id) = self.element.element_id() {
cx.with_element_state(id, |element_state, cx| { cx.with_element_state(id, |element_state, cx| {
let element_state = self.element.initialize(view_state, element_state, cx); let element_state = self.element.initialize(view_state, element_state, cx);
((), element_state) ((), element_state)
@ -142,7 +142,7 @@ where
frame_state: initial_frame_state, frame_state: initial_frame_state,
} => { } => {
frame_state = initial_frame_state; frame_state = initial_frame_state;
if let Some(id) = self.element.id() { if let Some(id) = self.element.element_id() {
layout_id = cx.with_element_state(id, |element_state, cx| { layout_id = cx.with_element_state(id, |element_state, cx| {
let mut element_state = element_state.unwrap(); let mut element_state = element_state.unwrap();
let layout_id = self.element.layout(state, &mut element_state, cx); let layout_id = self.element.layout(state, &mut element_state, cx);
@ -181,7 +181,7 @@ where
.. ..
} => { } => {
let bounds = cx.layout_bounds(layout_id); let bounds = cx.layout_bounds(layout_id);
if let Some(id) = self.element.id() { if let Some(id) = self.element.element_id() {
cx.with_element_state(id, |element_state, cx| { cx.with_element_state(id, |element_state, cx| {
let mut element_state = element_state.unwrap(); let mut element_state = element_state.unwrap();
self.element self.element
@ -255,7 +255,7 @@ where
// Ignore the element offset when drawing this element, as the origin is already specified // Ignore the element offset when drawing this element, as the origin is already specified
// in absolute terms. // in absolute terms.
origin -= cx.element_offset(); origin -= cx.element_offset();
cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx)) cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
} }
} }
@ -351,7 +351,7 @@ where
{ {
type ElementState = AnyElement<V>; type ElementState = AnyElement<V>;
fn id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
None None
} }

File diff suppressed because it is too large Load diff

View file

@ -1,35 +1,28 @@
use crate::{ use crate::{
div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus, AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity, Styled, ViewContext,
StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
}; };
use futures::FutureExt; use futures::FutureExt;
use util::ResultExt; use util::ResultExt;
pub struct Img< pub struct Img<V: 'static> {
V: 'static, interactivity: Interactivity<V>,
I: ElementInteractivity<V> = StatelessInteractivity<V>,
F: ElementFocus<V> = FocusDisabled,
> {
base: Div<V, I, F>,
uri: Option<SharedString>, uri: Option<SharedString>,
grayscale: bool, grayscale: bool,
} }
pub fn img<V: 'static>() -> Img<V, StatelessInteractivity<V>, FocusDisabled> { pub fn img<V: 'static>() -> Img<V> {
Img { Img {
base: div(), interactivity: Interactivity::default(),
uri: None, uri: None,
grayscale: false, grayscale: false,
} }
} }
impl<V, I, F> Img<V, I, F> impl<V> Img<V>
where where
V: 'static, V: 'static,
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{ {
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self { pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
self.uri = Some(uri.into()); self.uri = Some(uri.into());
@ -42,145 +35,90 @@ where
} }
} }
impl<V, F> Img<V, StatelessInteractivity<V>, F> impl<V> Component<V> for Img<V> {
where
F: ElementFocus<V>,
{
pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteractivity<V>, F> {
Img {
base: self.base.id(id),
uri: self.uri,
grayscale: self.grayscale,
}
}
}
impl<V, I, F> Component<V> for Img<V, I, F>
where
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
AnyElement::new(self) AnyElement::new(self)
} }
} }
impl<V, I, F> Element<V> for Img<V, I, F> impl<V> Element<V> for Img<V> {
where type ElementState = InteractiveElementState;
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
type ElementState = DivState;
fn id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
self.base.id() self.interactivity.element_id.clone()
} }
fn initialize( fn initialize(
&mut self, &mut self,
view_state: &mut V, _view_state: &mut V,
element_state: Option<Self::ElementState>, element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) -> Self::ElementState { ) -> Self::ElementState {
self.base.initialize(view_state, element_state, cx) self.interactivity.initialize(element_state, cx)
} }
fn layout( fn layout(
&mut self, &mut self,
view_state: &mut V, _view_state: &mut V,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) -> LayoutId { ) -> LayoutId {
self.base.layout(view_state, element_state, cx) self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
} }
fn paint( fn paint(
&mut self, &mut self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
view: &mut V, _view_state: &mut V,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) { ) {
cx.with_z_index(0, |cx| { self.interactivity.paint(
self.base.paint(bounds, view, element_state, cx); bounds,
}); bounds.size,
element_state,
cx,
|style, _scroll_offset, cx| {
let corner_radii = style.corner_radii;
let style = self.base.compute_style(bounds, element_state, cx); if let Some(uri) = self.uri.clone() {
let corner_radii = style.corner_radii; // eprintln!(">>> image_cache.get({uri}");
let image_future = cx.image_cache.get(uri.clone());
if let Some(uri) = self.uri.clone() { // eprintln!("<<< image_cache.get({uri}");
// eprintln!(">>> image_cache.get({uri}"); if let Some(data) = image_future
let image_future = cx.image_cache.get(uri.clone()); .clone()
// eprintln!("<<< image_cache.get({uri}"); .now_or_never()
if let Some(data) = image_future .and_then(ResultExt::log_err)
.clone() {
.now_or_never() let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
.and_then(ResultExt::log_err) cx.with_z_index(1, |cx| {
{ cx.paint_image(bounds, corner_radii, data, self.grayscale)
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); .log_err()
cx.with_z_index(1, |cx| { });
cx.paint_image(bounds, corner_radii, data, self.grayscale) } else {
.log_err() cx.spawn(|_, mut cx| async move {
}); if image_future.await.log_err().is_some() {
} else { cx.on_next_frame(|cx| cx.notify());
cx.spawn(|_, mut cx| async move { }
if image_future.await.log_err().is_some() { })
cx.on_next_frame(|cx| cx.notify()); .detach()
} }
}) }
.detach() },
} )
}
} }
} }
impl<V, I, F> Styled for Img<V, I, F> impl<V> Styled for Img<V> {
where
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
self.base.style() &mut self.interactivity.base_style
} }
} }
impl<V, I, F> StatelessInteractive<V> for Img<V, I, F> impl<V> InteractiveComponent<V> for Img<V> {
where fn interactivity(&mut self) -> &mut Interactivity<V> {
I: ElementInteractivity<V>, &mut self.interactivity
F: ElementFocus<V>,
{
fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
self.base.stateless_interactivity()
}
}
impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteractivity<V>, F>
where
F: ElementFocus<V>,
{
fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
self.base.stateful_interactivity()
}
}
impl<V, I> Focusable<V> for Img<V, I, FocusEnabled<V>>
where
V: 'static,
I: ElementInteractivity<V>,
{
fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
self.base.focus_listeners()
}
fn set_focus_style(&mut self, style: StyleRefinement) {
self.base.set_focus_style(style)
}
fn set_focus_in_style(&mut self, style: StyleRefinement) {
self.base.set_focus_in_style(style)
}
fn set_in_focus_style(&mut self, style: StyleRefinement) {
self.base.set_in_focus_style(style)
} }
} }

View file

@ -1,157 +1,88 @@
use crate::{ use crate::{
div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId, AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels, InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, ViewContext,
StatelessInteractivity, StyleRefinement, Styled, ViewContext,
}; };
use util::ResultExt; use util::ResultExt;
pub struct Svg< pub struct Svg<V: 'static> {
V: 'static, interactivity: Interactivity<V>,
I: ElementInteractivity<V> = StatelessInteractivity<V>,
F: ElementFocus<V> = FocusDisabled,
> {
base: Div<V, I, F>,
path: Option<SharedString>, path: Option<SharedString>,
} }
pub fn svg<V: 'static>() -> Svg<V, StatelessInteractivity<V>, FocusDisabled> { pub fn svg<V: 'static>() -> Svg<V> {
Svg { Svg {
base: div(), interactivity: Interactivity::default(),
path: None, path: None,
} }
} }
impl<V, I, F> Svg<V, I, F> impl<V> Svg<V> {
where
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
pub fn path(mut self, path: impl Into<SharedString>) -> Self { pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into()); self.path = Some(path.into());
self self
} }
} }
impl<V, F> Svg<V, StatelessInteractivity<V>, F> impl<V> Component<V> for Svg<V> {
where
F: ElementFocus<V>,
{
pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteractivity<V>, F> {
Svg {
base: self.base.id(id),
path: self.path,
}
}
}
impl<V, I, F> Component<V> for Svg<V, I, F>
where
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
AnyElement::new(self) AnyElement::new(self)
} }
} }
impl<V, I, F> Element<V> for Svg<V, I, F> impl<V> Element<V> for Svg<V> {
where type ElementState = InteractiveElementState;
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
type ElementState = DivState;
fn id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<ElementId> {
self.base.id() self.interactivity.element_id.clone()
} }
fn initialize( fn initialize(
&mut self, &mut self,
view_state: &mut V, _view_state: &mut V,
element_state: Option<Self::ElementState>, element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) -> Self::ElementState { ) -> Self::ElementState {
self.base.initialize(view_state, element_state, cx) self.interactivity.initialize(element_state, cx)
} }
fn layout( fn layout(
&mut self, &mut self,
view_state: &mut V, _view_state: &mut V,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) -> LayoutId { ) -> LayoutId {
self.base.layout(view_state, element_state, cx) self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
} }
fn paint( fn paint(
&mut self, &mut self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
view: &mut V, _view_state: &mut V,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) where ) where
Self: Sized, Self: Sized,
{ {
self.base.paint(bounds, view, element_state, cx); self.interactivity
let color = self .paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
.base if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
.compute_style(bounds, element_state, cx) cx.paint_svg(bounds, path.clone(), color).log_err();
.text }
.color; })
if let Some((path, color)) = self.path.as_ref().zip(color) {
cx.paint_svg(bounds, path.clone(), color).log_err();
}
} }
} }
impl<V, I, F> Styled for Svg<V, I, F> impl<V> Styled for Svg<V> {
where
I: ElementInteractivity<V>,
F: ElementFocus<V>,
{
fn style(&mut self) -> &mut StyleRefinement { fn style(&mut self) -> &mut StyleRefinement {
self.base.style() &mut self.interactivity.base_style
} }
} }
impl<V, I, F> StatelessInteractive<V> for Svg<V, I, F> impl<V> InteractiveComponent<V> for Svg<V> {
where fn interactivity(&mut self) -> &mut Interactivity<V> {
I: ElementInteractivity<V>, &mut self.interactivity
F: ElementFocus<V>,
{
fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
self.base.stateless_interactivity()
}
}
impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteractivity<V>, F>
where
V: 'static,
F: ElementFocus<V>,
{
fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
self.base.stateful_interactivity()
}
}
impl<V: 'static, I> Focusable<V> for Svg<V, I, FocusEnabled<V>>
where
I: ElementInteractivity<V>,
{
fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
self.base.focus_listeners()
}
fn set_focus_style(&mut self, style: StyleRefinement) {
self.base.set_focus_style(style)
}
fn set_focus_in_style(&mut self, style: StyleRefinement) {
self.base.set_focus_in_style(style)
}
fn set_in_focus_style(&mut self, style: StyleRefinement) {
self.base.set_in_focus_style(style)
} }
} }

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
Size, ViewContext, Size, TextRun, ViewContext,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -11,6 +11,7 @@ impl<V: 'static> Component<V> for SharedString {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self, text: self,
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -21,6 +22,7 @@ impl<V: 'static> Component<V> for &'static str {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self.into(), text: self.into(),
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -33,6 +35,7 @@ impl<V: 'static> Component<V> for String {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
Text { Text {
text: self.into(), text: self.into(),
runs: None,
state_type: PhantomData, state_type: PhantomData,
} }
.render() .render()
@ -41,9 +44,25 @@ impl<V: 'static> Component<V> for String {
pub struct Text<V> { pub struct Text<V> {
text: SharedString, text: SharedString,
runs: Option<Vec<TextRun>>,
state_type: PhantomData<V>, state_type: PhantomData<V>,
} }
impl<V: 'static> Text<V> {
/// styled renders text that has different runs of different styles.
/// callers are responsible for setting the correct style for each run.
////
/// For uniform text you can usually just pass a string as a child, and
/// cx.text_style() will be used automatically.
pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
Text {
text,
runs: Some(runs),
state_type: Default::default(),
}
}
}
impl<V: 'static> Component<V> for Text<V> { impl<V: 'static> Component<V> for Text<V> {
fn render(self) -> AnyElement<V> { fn render(self) -> AnyElement<V> {
AnyElement::new(self) AnyElement::new(self)
@ -53,7 +72,7 @@ impl<V: 'static> Component<V> for Text<V> {
impl<V: 'static> Element<V> for Text<V> { impl<V: 'static> Element<V> for Text<V> {
type ElementState = Arc<Mutex<Option<TextElementState>>>; type ElementState = Arc<Mutex<Option<TextElementState>>>;
fn id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
None None
} }
@ -81,6 +100,13 @@ impl<V: 'static> Element<V> for Text<V> {
let text = self.text.clone(); let text = self.text.clone();
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
let runs = if let Some(runs) = self.runs.take() {
runs
} else {
vec![text_style.to_run(text.len())]
};
let layout_id = cx.request_measured_layout(Default::default(), rem_size, { let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
let element_state = element_state.clone(); let element_state = element_state.clone();
move |known_dimensions, _| { move |known_dimensions, _| {
@ -88,11 +114,15 @@ impl<V: 'static> Element<V> for Text<V> {
.layout_text( .layout_text(
&text, &text,
font_size, font_size,
&[text_style.to_run(text.len())], &runs[..],
known_dimensions.width, // Wrap if we know the width. known_dimensions.width, // Wrap if we know the width.
) )
.log_err() .log_err()
else { else {
element_state.lock().replace(TextElementState {
lines: Default::default(),
line_height,
});
return Size::default(); return Size::default();
}; };
@ -131,7 +161,8 @@ impl<V: 'static> Element<V> for Text<V> {
let element_state = element_state.lock(); let element_state = element_state.lock();
let element_state = element_state let element_state = element_state
.as_ref() .as_ref()
.expect("measurement has not been performed"); .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
.unwrap();
let line_height = element_state.line_height; let line_height = element_state.line_height;
let mut line_origin = bounds.origin; let mut line_origin = bounds.origin;

View file

@ -1,24 +1,23 @@
use crate::{ use crate::{
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size, ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity, Point, Size, StyleRefinement, Styled, ViewContext,
StyleRefinement, Styled, ViewContext,
}; };
use parking_lot::Mutex; use parking_lot::Mutex;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{cmp, ops::Range, sync::Arc}; use std::{cmp, mem, ops::Range, sync::Arc};
use taffy::style::Overflow; use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height. /// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
/// uniform_list will only render the visibile subset of items. /// uniform_list will only render the visibile subset of items.
pub fn uniform_list<Id, V, C>( pub fn uniform_list<I, V, C>(
id: Id, id: I,
item_count: usize, item_count: usize,
f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> SmallVec<[C; 64]>, f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
) -> UniformList<V> ) -> UniformList<V>
where where
Id: Into<ElementId>, I: Into<ElementId>,
V: 'static, V: 'static,
C: Component<V>, C: Component<V>,
{ {
@ -37,7 +36,10 @@ where
.map(|component| component.render()) .map(|component| component.render())
.collect() .collect()
}), }),
interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()), interactivity: Interactivity {
element_id: Some(id.into()),
..Default::default()
},
scroll_handle: None, scroll_handle: None,
} }
} }
@ -54,7 +56,7 @@ pub struct UniformList<V: 'static> {
&'a mut ViewContext<V>, &'a mut ViewContext<V>,
) -> SmallVec<[AnyElement<V>; 64]>, ) -> SmallVec<[AnyElement<V>; 64]>,
>, >,
interactivity: StatefulInteractivity<V>, interactivity: Interactivity<V>,
scroll_handle: Option<UniformListScrollHandle>, scroll_handle: Option<UniformListScrollHandle>,
} }
@ -103,7 +105,7 @@ pub struct UniformListState {
impl<V: 'static> Element<V> for UniformList<V> { impl<V: 'static> Element<V> for UniformList<V> {
type ElementState = UniformListState; type ElementState = UniformListState;
fn id(&self) -> Option<crate::ElementId> { fn element_id(&self) -> Option<crate::ElementId> {
Some(self.id.clone()) Some(self.id.clone())
} }
@ -113,13 +115,18 @@ impl<V: 'static> Element<V> for UniformList<V> {
element_state: Option<Self::ElementState>, element_state: Option<Self::ElementState>,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) -> Self::ElementState { ) -> Self::ElementState {
element_state.unwrap_or_else(|| { if let Some(mut element_state) = element_state {
element_state.interactive = self
.interactivity
.initialize(Some(element_state.interactive), cx);
element_state
} else {
let item_size = self.measure_item(view_state, None, cx); let item_size = self.measure_item(view_state, None, cx);
UniformListState { UniformListState {
interactive: InteractiveElementState::default(), interactive: self.interactivity.initialize(None, cx),
item_size, item_size,
} }
}) }
} }
fn layout( fn layout(
@ -132,35 +139,44 @@ impl<V: 'static> Element<V> for UniformList<V> {
let item_size = element_state.item_size; let item_size = element_state.item_size;
let rem_size = cx.rem_size(); let rem_size = cx.rem_size();
cx.request_measured_layout( self.interactivity
self.computed_style(), .layout(&mut element_state.interactive, cx, |style, cx| {
rem_size, cx.request_measured_layout(
move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| { style,
let desired_height = item_size.height * max_items; rem_size,
let width = known_dimensions move |known_dimensions: Size<Option<Pixels>>,
.width available_space: Size<AvailableSpace>| {
.unwrap_or(match available_space.width { let desired_height = item_size.height * max_items;
AvailableSpace::Definite(x) => x, let width = known_dimensions
AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width, .width
}); .unwrap_or(match available_space.width {
let height = match available_space.height { AvailableSpace::Definite(x) => x,
AvailableSpace::Definite(x) => desired_height.min(x), AvailableSpace::MinContent | AvailableSpace::MaxContent => {
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height, item_size.width
}; }
size(width, height) });
}, let height = match available_space.height {
) AvailableSpace::Definite(x) => desired_height.min(x),
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height
}
};
size(width, height)
},
)
})
} }
fn paint( fn paint(
&mut self, &mut self,
bounds: crate::Bounds<crate::Pixels>, bounds: Bounds<crate::Pixels>,
view_state: &mut V, view_state: &mut V,
element_state: &mut Self::ElementState, element_state: &mut Self::ElementState,
cx: &mut ViewContext<V>, cx: &mut ViewContext<V>,
) { ) {
let style = self.computed_style(); let style =
self.interactivity
.compute_style(Some(bounds), &mut element_state.interactive, cx);
let border = style.border_widths.to_pixels(cx.rem_size()); let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size()); let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
@ -170,74 +186,79 @@ impl<V: 'static> Element<V> for UniformList<V> {
- point(border.right + padding.right, border.bottom + padding.bottom), - point(border.right + padding.right, border.bottom + padding.bottom),
); );
cx.with_z_index(style.z_index.unwrap_or(0), |cx| { let item_size = element_state.item_size;
style.paint(bounds, cx); let content_size = Size {
width: padded_bounds.size.width,
height: item_size.height * self.item_count,
};
let content_size; let mut interactivity = mem::take(&mut self.interactivity);
if self.item_count > 0 { let shared_scroll_offset = element_state
let item_height = self .interactive
.measure_item(view_state, Some(padded_bounds.size.width), cx) .scroll_offset
.height; .get_or_insert_with(Arc::default)
if let Some(scroll_handle) = self.scroll_handle.clone() { .clone();
scroll_handle.0.lock().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: element_state.interactive.track_scroll_offset(),
});
}
let visible_item_count = if item_height > px(0.) {
(padded_bounds.size.height / item_height).ceil() as usize + 1
} else {
0
};
let scroll_offset = element_state
.interactive
.scroll_offset()
.map_or((0.0).into(), |offset| offset.y);
let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize;
let visible_range = first_visible_element_ix
..cmp::min(
first_visible_element_ix + visible_item_count,
self.item_count,
);
let mut items = (self.render_items)(view_state, visible_range.clone(), cx); interactivity.paint(
bounds,
content_size,
&mut element_state.interactive,
cx,
|style, scroll_offset, cx| {
let border = style.border_widths.to_pixels(cx.rem_size());
let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
content_size = Size { let padded_bounds = Bounds::from_corners(
width: padded_bounds.size.width, bounds.origin + point(border.left + padding.left, border.top + padding.top),
height: item_height * self.item_count, bounds.lower_right()
}; - point(border.right + padding.right, border.bottom + padding.bottom),
cx.with_z_index(1, |cx| {
for (item, ix) in items.iter_mut().zip(visible_range) {
let item_origin =
padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset);
let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
item.draw(item_origin, available_space, view_state, cx);
}
});
} else {
content_size = Size {
width: bounds.size.width,
height: px(0.),
};
}
let overflow = point(style.overflow.x, Overflow::Scroll);
cx.with_z_index(0, |cx| {
self.interactivity.paint(
bounds,
content_size,
overflow,
&mut element_state.interactive,
cx,
); );
});
}) cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
style.paint(bounds, cx);
if self.item_count > 0 {
let item_height = self
.measure_item(view_state, Some(padded_bounds.size.width), cx)
.height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.lock().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: shared_scroll_offset,
});
}
let visible_item_count = if item_height > px(0.) {
(padded_bounds.size.height / item_height).ceil() as usize + 1
} else {
0
};
let first_visible_element_ix =
(-scroll_offset.y / item_height).floor() as usize;
let visible_range = first_visible_element_ix
..cmp::min(
first_visible_element_ix + visible_item_count,
self.item_count,
);
let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
cx.with_z_index(1, |cx| {
for (item, ix) in items.iter_mut().zip(visible_range) {
let item_origin = padded_bounds.origin
+ point(px(0.), item_height * ix + scroll_offset.y);
let available_space = size(
AvailableSpace::Definite(padded_bounds.size.width),
AvailableSpace::Definite(item_height),
);
item.draw(item_origin, available_space, view_state, cx);
}
});
}
})
},
);
self.interactivity = interactivity;
} }
} }
@ -275,14 +296,8 @@ impl<V> UniformList<V> {
} }
} }
impl<V: 'static> StatelessInteractive<V> for UniformList<V> { impl<V> InteractiveComponent<V> for UniformList<V> {
fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> { fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
self.interactivity.as_stateless_mut()
}
}
impl<V: 'static> StatefulInteractive<V> for UniformList<V> {
fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
&mut self.interactivity &mut self.interactivity
} }
} }

View file

@ -1,252 +0,0 @@
use crate::{
Bounds, DispatchPhase, Element, FocusEvent, FocusHandle, MouseDownEvent, Pixels, Style,
StyleRefinement, ViewContext, WindowContext,
};
use refineable::Refineable;
use smallvec::SmallVec;
pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
pub type FocusListener<V> =
Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
pub trait Focusable<V: 'static>: Element<V> {
fn focus_listeners(&mut self) -> &mut FocusListeners<V>;
fn set_focus_style(&mut self, style: StyleRefinement);
fn set_focus_in_style(&mut self, style: StyleRefinement);
fn set_in_focus_style(&mut self, style: StyleRefinement);
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.set_focus_style(f(StyleRefinement::default()));
self
}
fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.set_focus_in_style(f(StyleRefinement::default()));
self
}
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
{
self.set_in_focus_style(f(StyleRefinement::default()));
self
}
fn on_focus(
mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
) -> Self
where
Self: Sized,
{
self.focus_listeners()
.push(Box::new(move |view, focus_handle, event, cx| {
if event.focused.as_ref() == Some(focus_handle) {
listener(view, event, cx)
}
}));
self
}
fn on_blur(
mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
) -> Self
where
Self: Sized,
{
self.focus_listeners()
.push(Box::new(move |view, focus_handle, event, cx| {
if event.blurred.as_ref() == Some(focus_handle) {
listener(view, event, cx)
}
}));
self
}
fn on_focus_in(
mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
) -> Self
where
Self: Sized,
{
self.focus_listeners()
.push(Box::new(move |view, focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
.map_or(false, |blurred| focus_handle.contains(blurred, cx));
let descendant_focused = event
.focused
.as_ref()
.map_or(false, |focused| focus_handle.contains(focused, cx));
if !descendant_blurred && descendant_focused {
listener(view, event, cx)
}
}));
self
}
fn on_focus_out(
mut self,
listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
) -> Self
where
Self: Sized,
{
self.focus_listeners()
.push(Box::new(move |view, focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
.map_or(false, |blurred| focus_handle.contains(blurred, cx));
let descendant_focused = event
.focused
.as_ref()
.map_or(false, |focused| focus_handle.contains(focused, cx));
if descendant_blurred && !descendant_focused {
listener(view, event, cx)
}
}));
self
}
}
pub trait ElementFocus<V: 'static>: 'static {
fn as_focusable(&self) -> Option<&FocusEnabled<V>>;
fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>>;
fn initialize<R>(
&mut self,
focus_handle: Option<FocusHandle>,
cx: &mut ViewContext<V>,
f: impl FnOnce(Option<FocusHandle>, &mut ViewContext<V>) -> R,
) -> R {
if let Some(focusable) = self.as_focusable_mut() {
let focus_handle = focusable
.focus_handle
.get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle()))
.clone();
for listener in focusable.focus_listeners.drain(..) {
let focus_handle = focus_handle.clone();
cx.on_focus_changed(move |view, event, cx| {
listener(view, &focus_handle, event, cx)
});
}
cx.with_focus(focus_handle.clone(), |cx| f(Some(focus_handle), cx))
} else {
f(None, cx)
}
}
fn refine_style(&self, style: &mut Style, cx: &WindowContext) {
if let Some(focusable) = self.as_focusable() {
let focus_handle = focusable
.focus_handle
.as_ref()
.expect("must call initialize before refine_style");
if focus_handle.contains_focused(cx) {
style.refine(&focusable.focus_in_style);
}
if focus_handle.within_focused(cx) {
style.refine(&focusable.in_focus_style);
}
if focus_handle.is_focused(cx) {
style.refine(&focusable.focus_style);
}
}
}
fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
if let Some(focusable) = self.as_focusable() {
let focus_handle = focusable
.focus_handle
.clone()
.expect("must call initialize before paint");
cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
if !cx.default_prevented() {
cx.focus(&focus_handle);
cx.prevent_default();
}
}
})
}
}
}
pub struct FocusEnabled<V> {
pub focus_handle: Option<FocusHandle>,
pub focus_listeners: FocusListeners<V>,
pub focus_style: StyleRefinement,
pub focus_in_style: StyleRefinement,
pub in_focus_style: StyleRefinement,
}
impl<V> FocusEnabled<V> {
pub fn new() -> Self {
Self {
focus_handle: None,
focus_listeners: FocusListeners::default(),
focus_style: StyleRefinement::default(),
focus_in_style: StyleRefinement::default(),
in_focus_style: StyleRefinement::default(),
}
}
pub fn tracked(handle: &FocusHandle) -> Self {
Self {
focus_handle: Some(handle.clone()),
focus_listeners: FocusListeners::default(),
focus_style: StyleRefinement::default(),
focus_in_style: StyleRefinement::default(),
in_focus_style: StyleRefinement::default(),
}
}
}
impl<V: 'static> ElementFocus<V> for FocusEnabled<V> {
fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
Some(self)
}
fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
Some(self)
}
}
impl<V> From<FocusHandle> for FocusEnabled<V> {
fn from(value: FocusHandle) -> Self {
Self {
focus_handle: Some(value),
focus_listeners: FocusListeners::default(),
focus_style: StyleRefinement::default(),
focus_in_style: StyleRefinement::default(),
in_focus_style: StyleRefinement::default(),
}
}
}
pub struct FocusDisabled;
impl<V: 'static> ElementFocus<V> for FocusDisabled {
fn as_focusable(&self) -> Option<&FocusEnabled<V>> {
None
}
fn as_focusable_mut(&mut self) -> Option<&mut FocusEnabled<V>> {
None
}
}

View file

@ -6,13 +6,14 @@ mod color;
mod element; mod element;
mod elements; mod elements;
mod executor; mod executor;
mod focusable;
mod geometry; mod geometry;
mod image_cache; mod image_cache;
mod input; mod input;
mod interactive; mod interactive;
mod key_dispatch;
mod keymap; mod keymap;
mod platform; mod platform;
pub mod prelude;
mod scene; mod scene;
mod style; mod style;
mod styled; mod styled;
@ -41,12 +42,12 @@ pub use ctor::ctor;
pub use element::*; pub use element::*;
pub use elements::*; pub use elements::*;
pub use executor::*; pub use executor::*;
pub use focusable::*;
pub use geometry::*; pub use geometry::*;
pub use gpui2_macros::*; pub use gpui2_macros::*;
pub use image_cache::*; pub use image_cache::*;
pub use input::*; pub use input::*;
pub use interactive::*; pub use interactive::*;
pub use key_dispatch::*;
pub use keymap::*; pub use keymap::*;
pub use platform::*; pub use platform::*;
use private::Sealed; use private::Sealed;
@ -104,6 +105,14 @@ pub trait Context {
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T> fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
where where
F: FnOnce(AnyView, &mut WindowContext<'_>) -> T; F: FnOnce(AnyView, &mut WindowContext<'_>) -> T;
fn read_window<T, R>(
&self,
window: &WindowHandle<T>,
read: impl FnOnce(View<T>, &AppContext) -> R,
) -> Result<R>
where
T: 'static;
} }
pub trait VisualContext: Context { pub trait VisualContext: Context {
@ -147,7 +156,7 @@ pub enum GlobalKey {
} }
pub trait BorrowAppContext { pub trait BorrowAppContext {
fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where where
F: FnOnce(&mut Self) -> R; F: FnOnce(&mut Self) -> R;
@ -158,14 +167,18 @@ impl<C> BorrowAppContext for C
where where
C: BorrowMut<AppContext>, C: BorrowMut<AppContext>,
{ {
fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
where where
F: FnOnce(&mut Self) -> R, F: FnOnce(&mut Self) -> R,
{ {
self.borrow_mut().push_text_style(style); if let Some(style) = style {
let result = f(self); self.borrow_mut().push_text_style(style);
self.borrow_mut().pop_text_style(); let result = f(self);
result self.borrow_mut().pop_text_style();
result
} else {
f(self)
}
} }
fn set_global<G: 'static>(&mut self, global: G) { fn set_global<G: 'static>(&mut self, global: G) {

View file

@ -45,7 +45,7 @@ impl<V: 'static> ElementInputHandler<V> {
/// containing view. /// containing view.
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self { pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
ElementInputHandler { ElementInputHandler {
view: cx.view(), view: cx.view().clone(),
element_bounds, element_bounds,
cx: cx.to_async(), cx: cx.to_async(),
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,204 @@
use crate::{
build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
Keymap, Keystroke, KeystrokeMatcher, WindowContext,
};
use collections::HashMap;
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
rc::Rc,
sync::Arc,
};
use util::ResultExt;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
context_stack: Vec<KeyContext>,
nodes: Vec<DispatchNode>,
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Arc<Mutex<Keymap>>,
}
#[derive(Default)]
pub(crate) struct DispatchNode {
pub key_listeners: SmallVec<[KeyListener; 2]>,
pub action_listeners: SmallVec<[DispatchActionListener; 16]>,
pub context: KeyContext,
parent: Option<DispatchNodeId>,
}
type KeyListener = Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>;
#[derive(Clone)]
pub(crate) struct DispatchActionListener {
pub(crate) action_type: TypeId,
pub(crate) listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
}
impl DispatchTree {
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
Self {
node_stack: Vec::new(),
context_stack: Vec::new(),
nodes: Vec::new(),
focusable_node_ids: HashMap::default(),
keystroke_matchers: HashMap::default(),
keymap,
}
}
pub fn clear(&mut self) {
self.node_stack.clear();
self.nodes.clear();
self.context_stack.clear();
self.focusable_node_ids.clear();
self.keystroke_matchers.clear();
}
pub fn push_node(&mut self, context: KeyContext, old_dispatcher: &mut Self) {
let parent = self.node_stack.last().copied();
let node_id = DispatchNodeId(self.nodes.len());
self.nodes.push(DispatchNode {
parent,
..Default::default()
});
self.node_stack.push(node_id);
if !context.is_empty() {
self.active_node().context = context.clone();
self.context_stack.push(context);
if let Some((context_stack, matcher)) = old_dispatcher
.keystroke_matchers
.remove_entry(self.context_stack.as_slice())
{
self.keystroke_matchers.insert(context_stack, matcher);
}
}
}
pub fn pop_node(&mut self) {
let node_id = self.node_stack.pop().unwrap();
if !self.nodes[node_id.0].context.is_empty() {
self.context_stack.pop();
}
}
pub fn on_key_event(&mut self, listener: KeyListener) {
self.active_node().key_listeners.push(listener);
}
pub fn on_action(
&mut self,
action_type: TypeId,
listener: Rc<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext)>,
) {
self.active_node()
.action_listeners
.push(DispatchActionListener {
action_type,
listener,
});
}
pub fn make_focusable(&mut self, focus_id: FocusId) {
self.focusable_node_ids
.insert(focus_id, self.active_node_id());
}
pub fn focus_contains(&self, parent: FocusId, child: FocusId) -> bool {
if parent == child {
return true;
}
if let Some(parent_node_id) = self.focusable_node_ids.get(&parent) {
let mut current_node_id = self.focusable_node_ids.get(&child).copied();
while let Some(node_id) = current_node_id {
if node_id == *parent_node_id {
return true;
}
current_node_id = self.nodes[node_id.0].parent;
}
}
false
}
pub fn available_actions(&self, target: FocusId) -> Vec<Box<dyn Action>> {
let mut actions = Vec::new();
if let Some(node) = self.focusable_node_ids.get(&target) {
for node_id in self.dispatch_path(*node) {
let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners {
actions.extend(build_action_from_type(action_type).log_err());
}
}
}
actions
}
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.keymap
.lock()
.bindings_for_action(action.type_id())
.filter(|candidate| candidate.action.partial_eq(action))
.cloned()
.collect()
}
pub fn dispatch_key(
&mut self,
keystroke: &Keystroke,
context: &[KeyContext],
) -> Option<Box<dyn Action>> {
if !self.keystroke_matchers.contains_key(context) {
let keystroke_contexts = context.iter().cloned().collect();
self.keystroke_matchers.insert(
keystroke_contexts,
KeystrokeMatcher::new(self.keymap.clone()),
);
}
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
if let KeyMatch::Some(action) = keystroke_matcher.match_keystroke(keystroke, context) {
// Clear all pending keystrokes when an action has been found.
for keystroke_matcher in self.keystroke_matchers.values_mut() {
keystroke_matcher.clear_pending();
}
Some(action)
} else {
None
}
}
pub fn dispatch_path(&self, target: DispatchNodeId) -> SmallVec<[DispatchNodeId; 32]> {
let mut dispatch_path: SmallVec<[DispatchNodeId; 32]> = SmallVec::new();
let mut current_node_id = Some(target);
while let Some(node_id) = current_node_id {
dispatch_path.push(node_id);
current_node_id = self.nodes[node_id.0].parent;
}
dispatch_path.reverse(); // Reverse the path so it goes from the root to the focused node.
dispatch_path
}
pub fn node(&self, node_id: DispatchNodeId) -> &DispatchNode {
&self.nodes[node_id.0]
}
fn active_node(&mut self) -> &mut DispatchNode {
let active_node_id = self.active_node_id();
&mut self.nodes[active_node_id.0]
}
pub fn focusable_node_id(&self, target: FocusId) -> Option<DispatchNodeId> {
self.focusable_node_ids.get(&target).copied()
}
fn active_node_id(&self) -> DispatchNodeId {
*self.node_stack.last().unwrap()
}
}

View file

@ -1,11 +1,21 @@
use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke}; use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke};
use anyhow::Result; use anyhow::Result;
use smallvec::SmallVec; use smallvec::SmallVec;
pub struct KeyBinding { pub struct KeyBinding {
action: Box<dyn Action>, pub(crate) action: Box<dyn Action>,
pub(super) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(super) context_predicate: Option<DispatchContextPredicate>, pub(crate) context_predicate: Option<KeyBindingContextPredicate>,
}
impl Clone for KeyBinding {
fn clone(&self) -> Self {
KeyBinding {
action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
}
}
} }
impl KeyBinding { impl KeyBinding {
@ -15,7 +25,7 @@ impl KeyBinding {
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> { pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
let context = if let Some(context) = context { let context = if let Some(context) = context {
Some(DispatchContextPredicate::parse(context)?) Some(KeyBindingContextPredicate::parse(context)?)
} else { } else {
None None
}; };
@ -32,7 +42,7 @@ impl KeyBinding {
}) })
} }
pub fn matches_context(&self, contexts: &[&DispatchContext]) -> bool { pub fn matches_context(&self, contexts: &[KeyContext]) -> bool {
self.context_predicate self.context_predicate
.as_ref() .as_ref()
.map(|predicate| predicate.eval(contexts)) .map(|predicate| predicate.eval(contexts))
@ -42,7 +52,7 @@ impl KeyBinding {
pub fn match_keystrokes( pub fn match_keystrokes(
&self, &self,
pending_keystrokes: &[Keystroke], pending_keystrokes: &[Keystroke],
contexts: &[&DispatchContext], contexts: &[KeyContext],
) -> KeyMatch { ) -> KeyMatch {
if self.keystrokes.as_ref().starts_with(&pending_keystrokes) if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
&& self.matches_context(contexts) && self.matches_context(contexts)
@ -61,7 +71,7 @@ impl KeyBinding {
pub fn keystrokes_for_action( pub fn keystrokes_for_action(
&self, &self,
action: &dyn Action, action: &dyn Action,
contexts: &[&DispatchContext], contexts: &[KeyContext],
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
if self.action.partial_eq(action) && self.matches_context(contexts) { if self.action.partial_eq(action) && self.matches_context(contexts) {
Some(self.keystrokes.clone()) Some(self.keystrokes.clone())

View file

@ -0,0 +1,449 @@
use crate::SharedString;
use anyhow::{anyhow, Result};
use smallvec::SmallVec;
use std::fmt;
#[derive(Clone, Default, Eq, PartialEq, Hash)]
pub struct KeyContext(SmallVec<[ContextEntry; 8]>);
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct ContextEntry {
key: SharedString,
value: Option<SharedString>,
}
impl<'a> TryFrom<&'a str> for KeyContext {
type Error = anyhow::Error;
fn try_from(value: &'a str) -> Result<Self> {
Self::parse(value)
}
}
impl KeyContext {
pub fn parse(source: &str) -> Result<Self> {
let mut context = Self::default();
let source = skip_whitespace(source);
Self::parse_expr(&source, &mut context)?;
Ok(context)
}
fn parse_expr(mut source: &str, context: &mut Self) -> Result<()> {
if source.is_empty() {
return Ok(());
}
let key = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[key.len()..]);
if let Some(suffix) = source.strip_prefix('=') {
source = skip_whitespace(suffix);
let value = source
.chars()
.take_while(|c| is_identifier_char(*c))
.collect::<String>();
source = skip_whitespace(&source[value.len()..]);
context.set(key, value);
} else {
context.add(key);
}
Self::parse_expr(source, context)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn clear(&mut self) {
self.0.clear();
}
pub fn extend(&mut self, other: &Self) {
for entry in &other.0 {
if !self.contains(&entry.key) {
self.0.push(entry.clone());
}
}
}
pub fn add<I: Into<SharedString>>(&mut self, identifier: I) {
let key = identifier.into();
if !self.contains(&key) {
self.0.push(ContextEntry { key, value: None })
}
}
pub fn set<S1: Into<SharedString>, S2: Into<SharedString>>(&mut self, key: S1, value: S2) {
let key = key.into();
if !self.contains(&key) {
self.0.push(ContextEntry {
key,
value: Some(value.into()),
})
}
}
pub fn contains(&self, key: &str) -> bool {
self.0.iter().any(|entry| entry.key.as_ref() == key)
}
pub fn get(&self, key: &str) -> Option<&SharedString> {
self.0
.iter()
.find(|entry| entry.key.as_ref() == key)?
.value
.as_ref()
}
}
impl fmt::Debug for KeyContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut entries = self.0.iter().peekable();
while let Some(entry) = entries.next() {
if let Some(ref value) = entry.value {
write!(f, "{}={}", entry.key, value)?;
} else {
write!(f, "{}", entry.key)?;
}
if entries.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum KeyBindingContextPredicate {
Identifier(SharedString),
Equal(SharedString, SharedString),
NotEqual(SharedString, SharedString),
Child(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
Not(Box<KeyBindingContextPredicate>),
And(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
Or(
Box<KeyBindingContextPredicate>,
Box<KeyBindingContextPredicate>,
),
}
impl KeyBindingContextPredicate {
pub fn parse(source: &str) -> Result<Self> {
let source = skip_whitespace(source);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if let Some(next) = rest.chars().next() {
Err(anyhow!("unexpected character {next:?}"))
} else {
Ok(predicate)
}
}
pub fn eval(&self, contexts: &[KeyContext]) -> bool {
let Some(context) = contexts.last() else {
return false;
};
match self {
Self::Identifier(name) => context.contains(name),
Self::Equal(left, right) => context
.get(left)
.map(|value| value == right)
.unwrap_or(false),
Self::NotEqual(left, right) => context
.get(left)
.map(|value| value != right)
.unwrap_or(true),
Self::Not(pred) => !pred.eval(contexts),
Self::Child(parent, child) => {
parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts)
}
Self::And(left, right) => left.eval(contexts) && right.eval(contexts),
Self::Or(left, right) => left.eval(contexts) || right.eval(contexts),
}
}
fn parse_expr(mut source: &str, min_precedence: u32) -> anyhow::Result<(Self, &str)> {
type Op = fn(
KeyBindingContextPredicate,
KeyBindingContextPredicate,
) -> Result<KeyBindingContextPredicate>;
let (mut predicate, rest) = Self::parse_primary(source)?;
source = rest;
'parse: loop {
for (operator, precedence, constructor) in [
(">", PRECEDENCE_CHILD, Self::new_child as Op),
("&&", PRECEDENCE_AND, Self::new_and as Op),
("||", PRECEDENCE_OR, Self::new_or as Op),
("==", PRECEDENCE_EQ, Self::new_eq as Op),
("!=", PRECEDENCE_EQ, Self::new_neq as Op),
] {
if source.starts_with(operator) && precedence >= min_precedence {
source = skip_whitespace(&source[operator.len()..]);
let (right, rest) = Self::parse_expr(source, precedence + 1)?;
predicate = constructor(predicate, right)?;
source = rest;
continue 'parse;
}
}
break;
}
Ok((predicate, source))
}
fn parse_primary(mut source: &str) -> anyhow::Result<(Self, &str)> {
let next = source
.chars()
.next()
.ok_or_else(|| anyhow!("unexpected eof"))?;
match next {
'(' => {
source = skip_whitespace(&source[1..]);
let (predicate, rest) = Self::parse_expr(source, 0)?;
if rest.starts_with(')') {
source = skip_whitespace(&rest[1..]);
Ok((predicate, source))
} else {
Err(anyhow!("expected a ')'"))
}
}
'!' => {
let source = skip_whitespace(&source[1..]);
let (predicate, source) = Self::parse_expr(&source, PRECEDENCE_NOT)?;
Ok((KeyBindingContextPredicate::Not(Box::new(predicate)), source))
}
_ if is_identifier_char(next) => {
let len = source
.find(|c: char| !is_identifier_char(c))
.unwrap_or(source.len());
let (identifier, rest) = source.split_at(len);
source = skip_whitespace(rest);
Ok((
KeyBindingContextPredicate::Identifier(identifier.to_string().into()),
source,
))
}
_ => Err(anyhow!("unexpected character {next:?}")),
}
}
fn new_or(self, other: Self) -> Result<Self> {
Ok(Self::Or(Box::new(self), Box::new(other)))
}
fn new_and(self, other: Self) -> Result<Self> {
Ok(Self::And(Box::new(self), Box::new(other)))
}
fn new_child(self, other: Self) -> Result<Self> {
Ok(Self::Child(Box::new(self), Box::new(other)))
}
fn new_eq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::Equal(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
fn new_neq(self, other: Self) -> Result<Self> {
if let (Self::Identifier(left), Self::Identifier(right)) = (self, other) {
Ok(Self::NotEqual(left, right))
} else {
Err(anyhow!("operands must be identifiers"))
}
}
}
const PRECEDENCE_CHILD: u32 = 1;
const PRECEDENCE_OR: u32 = 2;
const PRECEDENCE_AND: u32 = 3;
const PRECEDENCE_EQ: u32 = 4;
const PRECEDENCE_NOT: u32 = 5;
fn is_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
.unwrap_or(source.len());
&source[len..]
}
#[cfg(test)]
mod tests {
use super::*;
use crate as gpui;
use KeyBindingContextPredicate::*;
#[test]
fn test_actions_definition() {
{
actions!(A, B, C, D, E, F, G);
}
{
actions!(
A,
B,
C,
D,
E,
F,
G, // Don't wrap, test the trailing comma
);
}
}
#[test]
fn test_parse_context() {
let mut expected = KeyContext::default();
expected.add("baz");
expected.set("foo", "bar");
assert_eq!(KeyContext::parse("baz foo=bar").unwrap(), expected);
assert_eq!(KeyContext::parse("baz foo = bar").unwrap(), expected);
assert_eq!(
KeyContext::parse(" baz foo = bar baz").unwrap(),
expected
);
assert_eq!(KeyContext::parse(" baz foo = bar").unwrap(), expected);
}
#[test]
fn test_parse_identifiers() {
// Identifiers
assert_eq!(
KeyBindingContextPredicate::parse("abc12").unwrap(),
Identifier("abc12".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("_1a").unwrap(),
Identifier("_1a".into())
);
}
#[test]
fn test_parse_negations() {
assert_eq!(
KeyBindingContextPredicate::parse("!abc").unwrap(),
Not(Box::new(Identifier("abc".into())))
);
assert_eq!(
KeyBindingContextPredicate::parse(" ! ! abc").unwrap(),
Not(Box::new(Not(Box::new(Identifier("abc".into())))))
);
}
#[test]
fn test_parse_equality_operators() {
assert_eq!(
KeyBindingContextPredicate::parse("a == b").unwrap(),
Equal("a".into(), "b".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("c!=d").unwrap(),
NotEqual("c".into(), "d".into())
);
assert_eq!(
KeyBindingContextPredicate::parse("c == !d")
.unwrap_err()
.to_string(),
"operands must be identifiers"
);
}
#[test]
fn test_parse_boolean_operators() {
assert_eq!(
KeyBindingContextPredicate::parse("a || b").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a || !b && c").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(And(
Box::new(Not(Box::new(Identifier("b".into())))),
Box::new(Identifier("c".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a && b || c&&d").unwrap(),
Or(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(And(
Box::new(Identifier("c".into())),
Box::new(Identifier("d".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a == b && c || d == e && f").unwrap(),
Or(
Box::new(And(
Box::new(Equal("a".into(), "b".into())),
Box::new(Identifier("c".into()))
)),
Box::new(And(
Box::new(Equal("d".into(), "e".into())),
Box::new(Identifier("f".into()))
))
)
);
assert_eq!(
KeyBindingContextPredicate::parse("a && b && c && d").unwrap(),
And(
Box::new(And(
Box::new(And(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into()))
)),
Box::new(Identifier("c".into())),
)),
Box::new(Identifier("d".into()))
),
);
}
#[test]
fn test_parse_parenthesized_expressions() {
assert_eq!(
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
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!(
KeyBindingContextPredicate::parse(" ( a || b ) ").unwrap(),
Or(
Box::new(Identifier("a".into())),
Box::new(Identifier("b".into())),
)
);
}
}

View file

@ -1,4 +1,4 @@
use crate::{DispatchContextPredicate, KeyBinding, Keystroke}; use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
use collections::HashSet; use collections::HashSet;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{any::TypeId, collections::HashMap}; use std::{any::TypeId, collections::HashMap};
@ -11,7 +11,7 @@ pub struct Keymap {
bindings: Vec<KeyBinding>, bindings: Vec<KeyBinding>,
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>, binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
disabled_keystrokes: disabled_keystrokes:
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<DispatchContextPredicate>>>, HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
version: KeymapVersion, version: KeymapVersion,
} }

View file

@ -1,15 +1,15 @@
use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke}; use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
use parking_lot::Mutex; use parking_lot::Mutex;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::sync::Arc; use std::sync::Arc;
pub struct KeyMatcher { pub struct KeystrokeMatcher {
pending_keystrokes: Vec<Keystroke>, pending_keystrokes: Vec<Keystroke>,
keymap: Arc<Mutex<Keymap>>, keymap: Arc<Mutex<Keymap>>,
keymap_version: KeymapVersion, keymap_version: KeymapVersion,
} }
impl KeyMatcher { impl KeystrokeMatcher {
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self { pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
let keymap_version = keymap.lock().version(); let keymap_version = keymap.lock().version();
Self { Self {
@ -44,7 +44,7 @@ impl KeyMatcher {
pub fn match_keystroke( pub fn match_keystroke(
&mut self, &mut self,
keystroke: &Keystroke, keystroke: &Keystroke,
context_stack: &[&DispatchContext], context_stack: &[KeyContext],
) -> KeyMatch { ) -> KeyMatch {
let keymap = self.keymap.lock(); let keymap = self.keymap.lock();
// Clear pending keystrokes if the keymap has changed since the last matched keystroke. // Clear pending keystrokes if the keymap has changed since the last matched keystroke.
@ -86,7 +86,7 @@ impl KeyMatcher {
pub fn keystrokes_for_action( pub fn keystrokes_for_action(
&self, &self,
action: &dyn Action, action: &dyn Action,
contexts: &[&DispatchContext], contexts: &[KeyContext],
) -> Option<SmallVec<[Keystroke; 2]>> { ) -> Option<SmallVec<[Keystroke; 2]>> {
self.keymap self.keymap
.lock() .lock()
@ -97,6 +97,7 @@ impl KeyMatcher {
} }
} }
#[derive(Debug)]
pub enum KeyMatch { pub enum KeyMatch {
None, None,
Pending, Pending,

View file

@ -1,7 +1,9 @@
mod binding; mod binding;
mod context;
mod keymap; mod keymap;
mod matcher; mod matcher;
pub use binding::*; pub use binding::*;
pub use context::*;
pub use keymap::*; pub use keymap::*;
pub use matcher::*; pub use matcher::*;

View file

@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>; fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>; fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>; fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)>; fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout; fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
fn wrap_line( fn wrap_line(
&self, &self,

View file

@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem {
}, },
)?; )?;
Ok(candidates[ix]) let font_id = candidates[ix];
lock.font_selections.insert(font.clone(), font_id);
Ok(font_id)
} }
} }
@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem {
fn rasterize_glyph( fn rasterize_glyph(
&self, &self,
glyph_id: &RenderGlyphParams, glyph_id: &RenderGlyphParams,
raster_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> { ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.0.read().rasterize_glyph(glyph_id) self.0.read().rasterize_glyph(glyph_id, raster_bounds)
} }
fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout { fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
@ -247,8 +250,11 @@ impl MacTextSystemState {
.into()) .into())
} }
fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> { fn rasterize_glyph(
let glyph_bounds = self.raster_bounds(params)?; &self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 { if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
Err(anyhow!("glyph bounds are empty")) Err(anyhow!("glyph bounds are empty"))
} else { } else {
@ -260,6 +266,7 @@ impl MacTextSystemState {
if params.subpixel_variant.y > 0 { if params.subpixel_variant.y > 0 {
bitmap_size.height += DevicePixels(1); bitmap_size.height += DevicePixels(1);
} }
let bitmap_size = bitmap_size;
let mut bytes; let mut bytes;
let cx; let cx;

View file

@ -3,8 +3,15 @@ use crate::{
PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use collections::VecDeque;
use futures::channel::oneshot;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{rc::Rc, sync::Arc}; use std::{
cell::RefCell,
path::PathBuf,
rc::{Rc, Weak},
sync::Arc,
};
pub struct TestPlatform { pub struct TestPlatform {
background_executor: BackgroundExecutor, background_executor: BackgroundExecutor,
@ -13,18 +20,60 @@ pub struct TestPlatform {
active_window: Arc<Mutex<Option<AnyWindowHandle>>>, active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_display: Rc<dyn PlatformDisplay>, active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>, active_cursor: Mutex<CursorStyle>,
pub(crate) prompts: RefCell<TestPrompts>,
weak: Weak<Self>,
}
#[derive(Default)]
pub(crate) struct TestPrompts {
multiple_choice: VecDeque<oneshot::Sender<usize>>,
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
} }
impl TestPlatform { impl TestPlatform {
pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self { pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
TestPlatform { Rc::new_cyclic(|weak| TestPlatform {
background_executor: executor, background_executor: executor,
foreground_executor, foreground_executor,
prompts: Default::default(),
active_cursor: Default::default(), active_cursor: Default::default(),
active_display: Rc::new(TestDisplay::new()), active_display: Rc::new(TestDisplay::new()),
active_window: Default::default(), active_window: Default::default(),
} weak: weak.clone(),
})
}
pub(crate) fn simulate_new_path_selection(
&self,
select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
) {
let (path, tx) = self
.prompts
.borrow_mut()
.new_path
.pop_front()
.expect("no pending new path prompt");
tx.send(select_path(&path)).ok();
}
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
let tx = self
.prompts
.borrow_mut()
.multiple_choice
.pop_front()
.expect("no pending multiple choice prompt");
tx.send(response_ix).ok();
}
pub(crate) fn has_pending_prompt(&self) -> bool {
!self.prompts.borrow().multiple_choice.is_empty()
}
pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
let (tx, rx) = oneshot::channel();
self.prompts.borrow_mut().multiple_choice.push_back(tx);
rx
} }
} }
@ -46,9 +95,7 @@ impl Platform for TestPlatform {
unimplemented!() unimplemented!()
} }
fn quit(&self) { fn quit(&self) {}
unimplemented!()
}
fn restart(&self) { fn restart(&self) {
unimplemented!() unimplemented!()
@ -88,7 +135,11 @@ impl Platform for TestPlatform {
options: WindowOptions, options: WindowOptions,
) -> Box<dyn crate::PlatformWindow> { ) -> Box<dyn crate::PlatformWindow> {
*self.active_window.lock() = Some(handle); *self.active_window.lock() = Some(handle);
Box::new(TestWindow::new(options, self.active_display.clone())) Box::new(TestWindow::new(
options,
self.weak.clone(),
self.active_display.clone(),
))
} }
fn set_display_link_output_callback( fn set_display_link_output_callback(
@ -118,15 +169,20 @@ impl Platform for TestPlatform {
fn prompt_for_paths( fn prompt_for_paths(
&self, &self,
_options: crate::PathPromptOptions, _options: crate::PathPromptOptions,
) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> { ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
unimplemented!() unimplemented!()
} }
fn prompt_for_new_path( fn prompt_for_new_path(
&self, &self,
_directory: &std::path::Path, directory: &std::path::Path,
) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> { ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
unimplemented!() let (tx, rx) = oneshot::channel();
self.prompts
.borrow_mut()
.new_path
.push_back((directory.to_path_buf(), tx));
rx
} }
fn reveal_path(&self, _path: &std::path::Path) { fn reveal_path(&self, _path: &std::path::Path) {
@ -141,9 +197,7 @@ impl Platform for TestPlatform {
unimplemented!() unimplemented!()
} }
fn on_quit(&self, _callback: Box<dyn FnMut()>) { fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
unimplemented!()
}
fn on_reopen(&self, _callback: Box<dyn FnMut()>) { fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
unimplemented!() unimplemented!()

View file

@ -1,11 +1,14 @@
use std::{rc::Rc, sync::Arc};
use parking_lot::Mutex;
use crate::{ use crate::{
px, Pixels, PlatformAtlas, PlatformDisplay, PlatformWindow, Point, Scene, Size, px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
WindowAppearance, WindowBounds, WindowOptions, WindowAppearance, WindowBounds, WindowOptions,
}; };
use collections::HashMap;
use parking_lot::Mutex;
use std::{
rc::{Rc, Weak},
sync::{self, Arc},
};
#[derive(Default)] #[derive(Default)]
struct Handlers { struct Handlers {
@ -19,18 +22,25 @@ pub struct TestWindow {
bounds: WindowBounds, bounds: WindowBounds,
current_scene: Mutex<Option<Scene>>, current_scene: Mutex<Option<Scene>>,
display: Rc<dyn PlatformDisplay>, display: Rc<dyn PlatformDisplay>,
input_handler: Option<Box<dyn PlatformInputHandler>>,
handlers: Mutex<Handlers>, handlers: Mutex<Handlers>,
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>, sprite_atlas: Arc<dyn PlatformAtlas>,
} }
impl TestWindow { impl TestWindow {
pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self { pub fn new(
options: WindowOptions,
platform: Weak<TestPlatform>,
display: Rc<dyn PlatformDisplay>,
) -> Self {
Self { Self {
bounds: options.bounds, bounds: options.bounds,
current_scene: Default::default(), current_scene: Default::default(),
display, display,
platform,
sprite_atlas: Arc::new(TestAtlas), input_handler: None,
sprite_atlas: Arc::new(TestAtlas::new()),
handlers: Default::default(), handlers: Default::default(),
} }
} }
@ -73,8 +83,8 @@ impl PlatformWindow for TestWindow {
todo!() todo!()
} }
fn set_input_handler(&mut self, _input_handler: Box<dyn crate::PlatformInputHandler>) { fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
todo!() self.input_handler = Some(input_handler);
} }
fn prompt( fn prompt(
@ -83,7 +93,7 @@ impl PlatformWindow for TestWindow {
_msg: &str, _msg: &str,
_answers: &[&str], _answers: &[&str],
) -> futures::channel::oneshot::Receiver<usize> { ) -> futures::channel::oneshot::Receiver<usize> {
todo!() self.platform.upgrade().expect("platform dropped").prompt()
} }
fn activate(&self) { fn activate(&self) {
@ -154,26 +164,71 @@ impl PlatformWindow for TestWindow {
self.current_scene.lock().replace(scene); self.current_scene.lock().replace(scene);
} }
fn sprite_atlas(&self) -> std::sync::Arc<dyn crate::PlatformAtlas> { fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
self.sprite_atlas.clone() self.sprite_atlas.clone()
} }
} }
pub struct TestAtlas; pub struct TestAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
pub struct TestAtlas(Mutex<TestAtlasState>);
impl TestAtlas {
pub fn new() -> Self {
TestAtlas(Mutex::new(TestAtlasState {
next_id: 0,
tiles: HashMap::default(),
}))
}
}
impl PlatformAtlas for TestAtlas { impl PlatformAtlas for TestAtlas {
fn get_or_insert_with<'a>( fn get_or_insert_with<'a>(
&self, &self,
_key: &crate::AtlasKey, key: &crate::AtlasKey,
_build: &mut dyn FnMut() -> anyhow::Result<( build: &mut dyn FnMut() -> anyhow::Result<(
Size<crate::DevicePixels>, Size<crate::DevicePixels>,
std::borrow::Cow<'a, [u8]>, std::borrow::Cow<'a, [u8]>,
)>, )>,
) -> anyhow::Result<crate::AtlasTile> { ) -> anyhow::Result<crate::AtlasTile> {
todo!() let mut state = self.0.lock();
if let Some(tile) = state.tiles.get(key) {
return Ok(tile.clone());
}
state.next_id += 1;
let texture_id = state.next_id;
state.next_id += 1;
let tile_id = state.next_id;
drop(state);
let (size, _) = build()?;
let mut state = self.0.lock();
state.tiles.insert(
key.clone(),
crate::AtlasTile {
texture_id: AtlasTextureId {
index: texture_id,
kind: crate::AtlasTextureKind::Path,
},
tile_id: TileId(tile_id),
bounds: crate::Bounds {
origin: Point::zero(),
size,
},
},
);
Ok(state.tiles[key].clone())
} }
fn clear(&self) { fn clear(&self) {
todo!() let mut state = self.0.lock();
state.tiles = HashMap::default();
state.next_id = 0;
} }
} }

View file

@ -0,0 +1,4 @@
pub use crate::{
BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
};

View file

@ -1,8 +1,8 @@
use crate::{ use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
}; };
use refineable::{Cascade, Refineable}; use refineable::{Cascade, Refineable};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -157,7 +157,7 @@ impl Default for TextStyle {
} }
impl TextStyle { impl TextStyle {
pub fn highlight(mut self, style: HighlightStyle) -> Result<Self> { pub fn highlight(mut self, style: HighlightStyle) -> Self {
if let Some(weight) = style.font_weight { if let Some(weight) = style.font_weight {
self.font_weight = weight; self.font_weight = weight;
} }
@ -177,7 +177,7 @@ impl TextStyle {
self.underline = Some(underline); self.underline = Some(underline);
} }
Ok(self) self
} }
pub fn font(&self) -> Font { pub fn font(&self) -> Font {
@ -220,7 +220,7 @@ pub struct HighlightStyle {
impl Eq for HighlightStyle {} impl Eq for HighlightStyle {}
impl Style { impl Style {
pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> { pub fn text_style(&self) -> Option<&TextStyleRefinement> {
if self.text.is_some() { if self.text.is_some() {
Some(&self.text) Some(&self.text)
} else { } else {
@ -228,13 +228,47 @@ impl Style {
} }
} }
pub fn overflow_mask(&self, bounds: Bounds<Pixels>) -> Option<ContentMask<Pixels>> {
match self.overflow {
Point {
x: Overflow::Visible,
y: Overflow::Visible,
} => None,
_ => {
let current_mask = bounds;
let min = current_mask.origin;
let max = current_mask.lower_right();
let bounds = match (
self.overflow.x == Overflow::Visible,
self.overflow.y == Overflow::Visible,
) {
// x and y both visible
(true, true) => return None,
// x visible, y hidden
(true, false) => Bounds::from_corners(
point(min.x, bounds.origin.y),
point(max.x, bounds.lower_right().y),
),
// x hidden, y visible
(false, true) => Bounds::from_corners(
point(bounds.origin.x, min.y),
point(bounds.lower_right().x, max.y),
),
// both hidden
(false, false) => bounds,
};
Some(ContentMask { bounds })
}
}
}
pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
where where
C: BorrowAppContext, C: BorrowAppContext,
F: FnOnce(&mut C) -> R, F: FnOnce(&mut C) -> R,
{ {
if self.text.is_some() { if self.text.is_some() {
cx.with_text_style(self.text.clone(), f) cx.with_text_style(Some(self.text.clone()), f)
} else { } else {
f(cx) f(cx)
} }
@ -274,7 +308,7 @@ impl Style {
bounds: mask_bounds, bounds: mask_bounds,
}; };
cx.with_content_mask(mask, f) cx.with_content_mask(Some(mask), f)
} }
/// Paints the background of an element styled with this style. /// Paints the background of an element styled with this style.

View file

@ -1,26 +1,24 @@
use crate::{ use crate::{
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
SharedString, Style, StyleRefinement, Visibility, SharedString, StyleRefinement, Visibility,
}; };
use crate::{BoxShadow, TextStyleRefinement}; use crate::{BoxShadow, TextStyleRefinement};
use refineable::Refineable;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use taffy::style::Overflow;
pub trait Styled { pub trait Styled: Sized {
fn style(&mut self) -> &mut StyleRefinement; fn style(&mut self) -> &mut StyleRefinement;
fn computed_style(&mut self) -> Style {
Style::default().refined(self.style().clone())
}
gpui2_macros::style_helpers!(); gpui2_macros::style_helpers!();
fn z_index(mut self, z_index: u32) -> Self {
self.style().z_index = Some(z_index);
self
}
/// Sets the size of the element to the full width and height. /// Sets the size of the element to the full width and height.
fn full(mut self) -> Self fn full(mut self) -> Self {
where
Self: Sized,
{
self.style().size.width = Some(relative(1.).into()); self.style().size.width = Some(relative(1.).into());
self.style().size.height = Some(relative(1.).into()); self.style().size.height = Some(relative(1.).into());
self self
@ -28,118 +26,98 @@ pub trait Styled {
/// Sets the position of the element to `relative`. /// Sets the position of the element to `relative`.
/// [Docs](https://tailwindcss.com/docs/position) /// [Docs](https://tailwindcss.com/docs/position)
fn relative(mut self) -> Self fn relative(mut self) -> Self {
where
Self: Sized,
{
self.style().position = Some(Position::Relative); self.style().position = Some(Position::Relative);
self self
} }
/// Sets the position of the element to `absolute`. /// Sets the position of the element to `absolute`.
/// [Docs](https://tailwindcss.com/docs/position) /// [Docs](https://tailwindcss.com/docs/position)
fn absolute(mut self) -> Self fn absolute(mut self) -> Self {
where
Self: Sized,
{
self.style().position = Some(Position::Absolute); self.style().position = Some(Position::Absolute);
self self
} }
/// Sets the display type of the element to `block`. /// Sets the display type of the element to `block`.
/// [Docs](https://tailwindcss.com/docs/display) /// [Docs](https://tailwindcss.com/docs/display)
fn block(mut self) -> Self fn block(mut self) -> Self {
where
Self: Sized,
{
self.style().display = Some(Display::Block); self.style().display = Some(Display::Block);
self self
} }
/// Sets the display type of the element to `flex`. /// Sets the display type of the element to `flex`.
/// [Docs](https://tailwindcss.com/docs/display) /// [Docs](https://tailwindcss.com/docs/display)
fn flex(mut self) -> Self fn flex(mut self) -> Self {
where
Self: Sized,
{
self.style().display = Some(Display::Flex); self.style().display = Some(Display::Flex);
self self
} }
/// Sets the visibility of the element to `visible`. /// Sets the visibility of the element to `visible`.
/// [Docs](https://tailwindcss.com/docs/visibility) /// [Docs](https://tailwindcss.com/docs/visibility)
fn visible(mut self) -> Self fn visible(mut self) -> Self {
where
Self: Sized,
{
self.style().visibility = Some(Visibility::Visible); self.style().visibility = Some(Visibility::Visible);
self self
} }
/// Sets the visibility of the element to `hidden`. /// Sets the visibility of the element to `hidden`.
/// [Docs](https://tailwindcss.com/docs/visibility) /// [Docs](https://tailwindcss.com/docs/visibility)
fn invisible(mut self) -> Self fn invisible(mut self) -> Self {
where
Self: Sized,
{
self.style().visibility = Some(Visibility::Hidden); self.style().visibility = Some(Visibility::Hidden);
self self
} }
fn cursor(mut self, cursor: CursorStyle) -> Self fn overflow_hidden(mut self) -> Self {
where self.style().overflow.x = Some(Overflow::Hidden);
Self: Sized, self.style().overflow.y = Some(Overflow::Hidden);
{ self
}
fn overflow_hidden_x(mut self) -> Self {
self.style().overflow.x = Some(Overflow::Hidden);
self
}
fn overflow_hidden_y(mut self) -> Self {
self.style().overflow.y = Some(Overflow::Hidden);
self
}
fn cursor(mut self, cursor: CursorStyle) -> Self {
self.style().mouse_cursor = Some(cursor); self.style().mouse_cursor = Some(cursor);
self self
} }
/// Sets the cursor style when hovering an element to `default`. /// Sets the cursor style when hovering an element to `default`.
/// [Docs](https://tailwindcss.com/docs/cursor) /// [Docs](https://tailwindcss.com/docs/cursor)
fn cursor_default(mut self) -> Self fn cursor_default(mut self) -> Self {
where
Self: Sized,
{
self.style().mouse_cursor = Some(CursorStyle::Arrow); self.style().mouse_cursor = Some(CursorStyle::Arrow);
self self
} }
/// Sets the cursor style when hovering an element to `pointer`. /// Sets the cursor style when hovering an element to `pointer`.
/// [Docs](https://tailwindcss.com/docs/cursor) /// [Docs](https://tailwindcss.com/docs/cursor)
fn cursor_pointer(mut self) -> Self fn cursor_pointer(mut self) -> Self {
where
Self: Sized,
{
self.style().mouse_cursor = Some(CursorStyle::PointingHand); self.style().mouse_cursor = Some(CursorStyle::PointingHand);
self self
} }
/// Sets the flex direction of the element to `column`. /// Sets the flex direction of the element to `column`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column) /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
fn flex_col(mut self) -> Self fn flex_col(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_direction = Some(FlexDirection::Column); self.style().flex_direction = Some(FlexDirection::Column);
self self
} }
/// Sets the flex direction of the element to `row`. /// Sets the flex direction of the element to `row`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#row) /// [Docs](https://tailwindcss.com/docs/flex-direction#row)
fn flex_row(mut self) -> Self fn flex_row(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_direction = Some(FlexDirection::Row); self.style().flex_direction = Some(FlexDirection::Row);
self self
} }
/// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
/// [Docs](https://tailwindcss.com/docs/flex#flex-1) /// [Docs](https://tailwindcss.com/docs/flex#flex-1)
fn flex_1(mut self) -> Self fn flex_1(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_grow = Some(1.); self.style().flex_grow = Some(1.);
self.style().flex_shrink = Some(1.); self.style().flex_shrink = Some(1.);
self.style().flex_basis = Some(relative(0.).into()); self.style().flex_basis = Some(relative(0.).into());
@ -148,10 +126,7 @@ pub trait Styled {
/// Sets the element to allow a flex item to grow and shrink, taking into account its initial size. /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size.
/// [Docs](https://tailwindcss.com/docs/flex#auto) /// [Docs](https://tailwindcss.com/docs/flex#auto)
fn flex_auto(mut self) -> Self fn flex_auto(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_grow = Some(1.); self.style().flex_grow = Some(1.);
self.style().flex_shrink = Some(1.); self.style().flex_shrink = Some(1.);
self.style().flex_basis = Some(Length::Auto); self.style().flex_basis = Some(Length::Auto);
@ -160,10 +135,7 @@ pub trait Styled {
/// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size. /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size.
/// [Docs](https://tailwindcss.com/docs/flex#initial) /// [Docs](https://tailwindcss.com/docs/flex#initial)
fn flex_initial(mut self) -> Self fn flex_initial(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_grow = Some(0.); self.style().flex_grow = Some(0.);
self.style().flex_shrink = Some(1.); self.style().flex_shrink = Some(1.);
self.style().flex_basis = Some(Length::Auto); self.style().flex_basis = Some(Length::Auto);
@ -172,10 +144,7 @@ pub trait Styled {
/// Sets the element to prevent a flex item from growing or shrinking. /// Sets the element to prevent a flex item from growing or shrinking.
/// [Docs](https://tailwindcss.com/docs/flex#none) /// [Docs](https://tailwindcss.com/docs/flex#none)
fn flex_none(mut self) -> Self fn flex_none(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_grow = Some(0.); self.style().flex_grow = Some(0.);
self.style().flex_shrink = Some(0.); self.style().flex_shrink = Some(0.);
self self
@ -183,40 +152,28 @@ pub trait Styled {
/// Sets the element to allow a flex item to grow to fill any available space. /// Sets the element to allow a flex item to grow to fill any available space.
/// [Docs](https://tailwindcss.com/docs/flex-grow) /// [Docs](https://tailwindcss.com/docs/flex-grow)
fn grow(mut self) -> Self fn grow(mut self) -> Self {
where
Self: Sized,
{
self.style().flex_grow = Some(1.); self.style().flex_grow = Some(1.);
self self
} }
/// Sets the element to align flex items to the start of the container's cross axis. /// Sets the element to align flex items to the start of the container's cross axis.
/// [Docs](https://tailwindcss.com/docs/align-items#start) /// [Docs](https://tailwindcss.com/docs/align-items#start)
fn items_start(mut self) -> Self fn items_start(mut self) -> Self {
where
Self: Sized,
{
self.style().align_items = Some(AlignItems::FlexStart); self.style().align_items = Some(AlignItems::FlexStart);
self self
} }
/// Sets the element to align flex items to the end of the container's cross axis. /// Sets the element to align flex items to the end of the container's cross axis.
/// [Docs](https://tailwindcss.com/docs/align-items#end) /// [Docs](https://tailwindcss.com/docs/align-items#end)
fn items_end(mut self) -> Self fn items_end(mut self) -> Self {
where
Self: Sized,
{
self.style().align_items = Some(AlignItems::FlexEnd); self.style().align_items = Some(AlignItems::FlexEnd);
self self
} }
/// Sets the element to align flex items along the center of the container's cross axis. /// Sets the element to align flex items along the center of the container's cross axis.
/// [Docs](https://tailwindcss.com/docs/align-items#center) /// [Docs](https://tailwindcss.com/docs/align-items#center)
fn items_center(mut self) -> Self fn items_center(mut self) -> Self {
where
Self: Sized,
{
self.style().align_items = Some(AlignItems::Center); self.style().align_items = Some(AlignItems::Center);
self self
} }
@ -224,40 +181,28 @@ pub trait Styled {
/// Sets the element to justify flex items along the container's main axis /// Sets the element to justify flex items along the container's main axis
/// such that there is an equal amount of space between each item. /// such that there is an equal amount of space between each item.
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between) /// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
fn justify_between(mut self) -> Self fn justify_between(mut self) -> Self {
where
Self: Sized,
{
self.style().justify_content = Some(JustifyContent::SpaceBetween); self.style().justify_content = Some(JustifyContent::SpaceBetween);
self self
} }
/// Sets the element to justify flex items along the center of the container's main axis. /// Sets the element to justify flex items along the center of the container's main axis.
/// [Docs](https://tailwindcss.com/docs/justify-content#center) /// [Docs](https://tailwindcss.com/docs/justify-content#center)
fn justify_center(mut self) -> Self fn justify_center(mut self) -> Self {
where
Self: Sized,
{
self.style().justify_content = Some(JustifyContent::Center); self.style().justify_content = Some(JustifyContent::Center);
self self
} }
/// Sets the element to justify flex items against the start of the container's main axis. /// Sets the element to justify flex items against the start of the container's main axis.
/// [Docs](https://tailwindcss.com/docs/justify-content#start) /// [Docs](https://tailwindcss.com/docs/justify-content#start)
fn justify_start(mut self) -> Self fn justify_start(mut self) -> Self {
where
Self: Sized,
{
self.style().justify_content = Some(JustifyContent::Start); self.style().justify_content = Some(JustifyContent::Start);
self self
} }
/// Sets the element to justify flex items against the end of the container's main axis. /// Sets the element to justify flex items against the end of the container's main axis.
/// [Docs](https://tailwindcss.com/docs/justify-content#end) /// [Docs](https://tailwindcss.com/docs/justify-content#end)
fn justify_end(mut self) -> Self fn justify_end(mut self) -> Self {
where
Self: Sized,
{
self.style().justify_content = Some(JustifyContent::End); self.style().justify_content = Some(JustifyContent::End);
self self
} }
@ -265,10 +210,7 @@ pub trait Styled {
/// Sets the element to justify items along the container's main axis such /// Sets the element to justify items along the container's main axis such
/// that there is an equal amount of space on each side of each item. /// that there is an equal amount of space on each side of each item.
/// [Docs](https://tailwindcss.com/docs/justify-content#space-around) /// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
fn justify_around(mut self) -> Self fn justify_around(mut self) -> Self {
where
Self: Sized,
{
self.style().justify_content = Some(JustifyContent::SpaceAround); self.style().justify_content = Some(JustifyContent::SpaceAround);
self self
} }
@ -295,30 +237,21 @@ pub trait Styled {
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(shadows); self.style().box_shadow = Some(shadows);
self self
} }
/// Clears the box shadow of the element. /// Clears the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_none(mut self) -> Self fn shadow_none(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(Default::default()); self.style().box_shadow = Some(Default::default());
self self
} }
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_sm(mut self) -> Self fn shadow_sm(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(smallvec::smallvec![BoxShadow { self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
color: hsla(0., 0., 0., 0.05), color: hsla(0., 0., 0., 0.05),
offset: point(px(0.), px(1.)), offset: point(px(0.), px(1.)),
@ -330,10 +263,7 @@ pub trait Styled {
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_md(mut self) -> Self fn shadow_md(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(smallvec![ self.style().box_shadow = Some(smallvec![
BoxShadow { BoxShadow {
color: hsla(0.5, 0., 0., 0.1), color: hsla(0.5, 0., 0., 0.1),
@ -353,10 +283,7 @@ pub trait Styled {
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_lg(mut self) -> Self fn shadow_lg(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(smallvec![ self.style().box_shadow = Some(smallvec![
BoxShadow { BoxShadow {
color: hsla(0., 0., 0., 0.1), color: hsla(0., 0., 0., 0.1),
@ -376,10 +303,7 @@ pub trait Styled {
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_xl(mut self) -> Self fn shadow_xl(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(smallvec![ self.style().box_shadow = Some(smallvec![
BoxShadow { BoxShadow {
color: hsla(0., 0., 0., 0.1), color: hsla(0., 0., 0., 0.1),
@ -399,10 +323,7 @@ pub trait Styled {
/// Sets the box shadow of the element. /// Sets the box shadow of the element.
/// [Docs](https://tailwindcss.com/docs/box-shadow) /// [Docs](https://tailwindcss.com/docs/box-shadow)
fn shadow_2xl(mut self) -> Self fn shadow_2xl(mut self) -> Self {
where
Self: Sized,
{
self.style().box_shadow = Some(smallvec![BoxShadow { self.style().box_shadow = Some(smallvec![BoxShadow {
color: hsla(0., 0., 0., 0.25), color: hsla(0., 0., 0., 0.25),
offset: point(px(0.), px(25.)), offset: point(px(0.), px(25.)),
@ -417,198 +338,138 @@ pub trait Styled {
&mut style.text &mut style.text
} }
fn text_color(mut self, color: impl Into<Hsla>) -> Self fn text_color(mut self, color: impl Into<Hsla>) -> Self {
where
Self: Sized,
{
self.text_style().get_or_insert_with(Default::default).color = Some(color.into()); self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
self self
} }
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(size.into()); .font_size = Some(size.into());
self self
} }
fn text_xs(mut self) -> Self fn text_xs(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(0.75).into()); .font_size = Some(rems(0.75).into());
self self
} }
fn text_sm(mut self) -> Self fn text_sm(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(0.875).into()); .font_size = Some(rems(0.875).into());
self self
} }
fn text_base(mut self) -> Self fn text_base(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(1.0).into()); .font_size = Some(rems(1.0).into());
self self
} }
fn text_lg(mut self) -> Self fn text_lg(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(1.125).into()); .font_size = Some(rems(1.125).into());
self self
} }
fn text_xl(mut self) -> Self fn text_xl(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(1.25).into()); .font_size = Some(rems(1.25).into());
self self
} }
fn text_2xl(mut self) -> Self fn text_2xl(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(1.5).into()); .font_size = Some(rems(1.5).into());
self self
} }
fn text_3xl(mut self) -> Self fn text_3xl(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_size = Some(rems(1.875).into()); .font_size = Some(rems(1.875).into());
self self
} }
fn text_decoration_none(mut self) -> Self fn text_decoration_none(mut self) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.underline = None; .underline = None;
self self
} }
fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.color = Some(color.into()); underline.color = Some(color.into());
self self
} }
fn text_decoration_solid(mut self) -> Self fn text_decoration_solid(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.wavy = false; underline.wavy = false;
self self
} }
fn text_decoration_wavy(mut self) -> Self fn text_decoration_wavy(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.wavy = true; underline.wavy = true;
self self
} }
fn text_decoration_0(mut self) -> Self fn text_decoration_0(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.thickness = px(0.); underline.thickness = px(0.);
self self
} }
fn text_decoration_1(mut self) -> Self fn text_decoration_1(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.thickness = px(1.); underline.thickness = px(1.);
self self
} }
fn text_decoration_2(mut self) -> Self fn text_decoration_2(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.thickness = px(2.); underline.thickness = px(2.);
self self
} }
fn text_decoration_4(mut self) -> Self fn text_decoration_4(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.thickness = px(4.); underline.thickness = px(4.);
self self
} }
fn text_decoration_8(mut self) -> Self fn text_decoration_8(mut self) -> Self {
where
Self: Sized,
{
let style = self.text_style().get_or_insert_with(Default::default); let style = self.text_style().get_or_insert_with(Default::default);
let underline = style.underline.get_or_insert_with(Default::default); let underline = style.underline.get_or_insert_with(Default::default);
underline.thickness = px(8.); underline.thickness = px(8.);
self self
} }
fn font(mut self, family_name: impl Into<SharedString>) -> Self fn font(mut self, family_name: impl Into<SharedString>) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.font_family = Some(family_name.into()); .font_family = Some(family_name.into());
self self
} }
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
where
Self: Sized,
{
self.text_style() self.text_style()
.get_or_insert_with(Default::default) .get_or_insert_with(Default::default)
.line_height = Some(line_height.into()); .line_height = Some(line_height.into());

View file

@ -39,6 +39,7 @@ pub struct TextSystem {
platform_text_system: Arc<dyn PlatformTextSystem>, platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<HashMap<Font, FontId>>, font_ids_by_font: RwLock<HashMap<Font, FontId>>,
font_metrics: RwLock<HashMap<FontId, FontMetrics>>, font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>, wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
font_runs_pool: Mutex<Vec<Vec<FontRun>>>, font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
} }
@ -48,10 +49,11 @@ impl TextSystem {
TextSystem { TextSystem {
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())), line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
platform_text_system, platform_text_system,
font_metrics: RwLock::new(HashMap::default()), font_metrics: RwLock::default(),
font_ids_by_font: RwLock::new(HashMap::default()), raster_bounds: RwLock::default(),
wrapper_pool: Mutex::new(HashMap::default()), font_ids_by_font: RwLock::default(),
font_runs_pool: Default::default(), wrapper_pool: Mutex::default(),
font_runs_pool: Mutex::default(),
} }
} }
@ -252,14 +254,24 @@ impl TextSystem {
} }
pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> { pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
self.platform_text_system.glyph_raster_bounds(params) let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(bounds.clone())
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
} }
pub fn rasterize_glyph( pub fn rasterize_glyph(
&self, &self,
glyph_id: &RenderGlyphParams, params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> { ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
self.platform_text_system.rasterize_glyph(glyph_id) let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
} }
} }
@ -368,6 +380,7 @@ impl Display for FontStyle {
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextRun { pub struct TextRun {
// number of utf8 bytes
pub len: usize, pub len: usize,
pub font: Font, pub font: Font,
pub color: Hsla, pub color: Hsla,

View file

@ -68,7 +68,8 @@ impl LineLayout {
prev_x = glyph.position.x; prev_x = glyph.position.x;
} }
} }
prev_index
self.len
} }
pub fn x_for_index(&self, index: usize) -> Pixels { pub fn x_for_index(&self, index: usize) -> Pixels {

View file

@ -1,16 +1,26 @@
#[cfg(any(test, feature = "test-support"))]
use std::time::Duration;
#[cfg(any(test, feature = "test-support"))]
use futures::Future;
#[cfg(any(test, feature = "test-support"))]
use smol::future::FutureExt;
pub use util::*; pub use util::*;
// pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()> #[cfg(any(test, feature = "test-support"))]
// where pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
// F: Future<Output = T>, where
// { F: Future<Output = T>,
// let timer = async { {
// smol::Timer::after(timeout).await; let timer = async {
// Err(()) smol::Timer::after(timeout).await;
// }; Err(())
// let future = async move { Ok(f.await) }; };
// timer.race(future).await let future = async move { Ok(f.await) };
// } timer.race(future).await
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace); pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels, BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
Size, ViewContext, VisualContext, WeakModel, WindowContext, Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
}; };
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{ use std::{
@ -184,10 +184,6 @@ impl AnyView {
.compute_layout(layout_id, available_space); .compute_layout(layout_id, available_space);
(self.paint)(self, &mut rendered_element, cx); (self.paint)(self, &mut rendered_element, cx);
} }
pub(crate) fn draw_dispatch_stack(&self, cx: &mut WindowContext) {
(self.initialize)(self, cx);
}
} }
impl<V: 'static> Component<V> for AnyView { impl<V: 'static> Component<V> for AnyView {
@ -210,7 +206,7 @@ impl<V: Render> From<View<V>> for AnyView {
impl<ParentViewState: 'static> Element<ParentViewState> for AnyView { impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
type ElementState = Box<dyn Any>; type ElementState = Box<dyn Any>;
fn id(&self) -> Option<ElementId> { fn element_id(&self) -> Option<ElementId> {
Some(self.model.entity_id.into()) Some(self.model.entity_id.into())
} }
@ -285,12 +281,90 @@ where
} }
} }
pub struct RenderView<C, V> {
view: View<V>,
component: Option<C>,
}
impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderView<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
ViewState: 'static,
{
fn render(self) -> AnyElement<ParentViewState> {
AnyElement::new(self)
}
}
impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderView<C, ViewState>
where
C: 'static + Component<ViewState>,
ParentViewState: 'static,
ViewState: 'static,
{
type ElementState = AnyElement<ViewState>;
fn element_id(&self) -> Option<ElementId> {
Some(self.view.entity_id().into())
}
fn initialize(
&mut self,
_: &mut ParentViewState,
_: Option<Self::ElementState>,
cx: &mut ViewContext<ParentViewState>,
) -> Self::ElementState {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| {
let mut element = self.component.take().unwrap().render();
element.initialize(view, cx);
element
})
})
}
fn layout(
&mut self,
_: &mut ParentViewState,
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) -> LayoutId {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.layout(view, cx))
})
}
fn paint(
&mut self,
_: Bounds<Pixels>,
_: &mut ParentViewState,
element: &mut Self::ElementState,
cx: &mut ViewContext<ParentViewState>,
) {
cx.with_element_id(Some(self.view.entity_id()), |cx| {
self.view.update(cx, |view, cx| element.paint(view, cx))
})
}
}
pub fn render_view<C, V>(view: &View<V>, component: C) -> RenderView<C, V>
where
C: 'static + Component<V>,
V: 'static,
{
RenderView {
view: view.clone(),
component: Some(component),
}
}
mod any_view { mod any_view {
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
use std::any::Any; use std::any::Any;
pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> { pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
cx.with_element_id(view.model.entity_id, |_, cx| { cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap(); let view = view.clone().downcast::<V>().unwrap();
let element = view.update(cx, |view, cx| { let element = view.update(cx, |view, cx| {
let mut element = AnyElement::new(view.render(cx)); let mut element = AnyElement::new(view.render(cx));
@ -306,7 +380,7 @@ mod any_view {
element: &mut Box<dyn Any>, element: &mut Box<dyn Any>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> LayoutId { ) -> LayoutId {
cx.with_element_id(view.model.entity_id, |_, cx| { cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap(); let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap(); let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.layout(view, cx)) view.update(cx, |view, cx| element.layout(view, cx))
@ -318,7 +392,7 @@ mod any_view {
element: &mut Box<dyn Any>, element: &mut Box<dyn Any>,
cx: &mut WindowContext, cx: &mut WindowContext,
) { ) {
cx.with_element_id(view.model.entity_id, |_, cx| { cx.with_element_id(Some(view.model.entity_id), |cx| {
let view = view.clone().downcast::<V>().unwrap(); let view = view.clone().downcast::<V>().unwrap();
let element = element.downcast_mut::<AnyElement<V>>().unwrap(); let element = element.downcast_mut::<AnyElement<V>>().unwrap();
view.update(cx, |view, cx| element.paint(view, cx)) view.update(cx, |view, cx| element.paint(view, cx))

File diff suppressed because it is too large Load diff

View file

@ -34,13 +34,21 @@ pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
let visibility = input.vis; let visibility = input.vis;
let output = match input.data { let output = match input.data {
syn::Data::Struct(ref struct_data) => { syn::Data::Struct(ref struct_data) => match &struct_data.fields {
let fields = &struct_data.fields; syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
quote! { let fields = &struct_data.fields;
#attributes quote! {
#visibility struct #name #fields #attributes
#visibility struct #name #fields
}
} }
} syn::Fields::Unit => {
quote! {
#attributes
#visibility struct #name;
}
}
},
syn::Data::Enum(ref enum_data) => { syn::Data::Enum(ref enum_data) => {
let variants = &enum_data.variants; let variants = &enum_data.variants;
quote! { quote! {

View file

@ -130,7 +130,7 @@ fn generate_predefined_setter(
let method = quote! { let method = quote! {
#[doc = #doc_string] #[doc = #doc_string]
fn #method_name(mut self) -> Self where Self: std::marker::Sized { fn #method_name(mut self) -> Self {
let style = self.style(); let style = self.style();
#(#field_assignments)* #(#field_assignments)*
self self
@ -163,7 +163,7 @@ fn generate_custom_value_setter(
let method = quote! { let method = quote! {
#[doc = #doc_string] #[doc = #doc_string]
fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self where Self: std::marker::Sized { fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
let style = self.style(); let style = self.style();
#(#field_assignments)* #(#field_assignments)*
self self

View file

@ -14,5 +14,6 @@ test-support = []
smol.workspace = true smol.workspace = true
anyhow.workspace = true anyhow.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true
gpui = { package = "gpui2", path = "../gpui2" } gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" } util = { path = "../util" }

View file

@ -1,10 +1,9 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use gpui::AsyncAppContext; use gpui::{actions, AsyncAppContext};
use std::path::Path; use std::path::Path;
use util::ResultExt; use util::ResultExt;
// todo!() actions!(Install);
// actions!(cli, [Install]);
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;

View file

@ -1858,7 +1858,7 @@ mod tests {
async fn test_first_line_pattern(cx: &mut TestAppContext) { async fn test_first_line_pattern(cx: &mut TestAppContext) {
let mut languages = LanguageRegistry::test(); let mut languages = LanguageRegistry::test();
languages.set_executor(cx.executor().clone()); languages.set_executor(cx.executor());
let languages = Arc::new(languages); let languages = Arc::new(languages);
languages.register( languages.register(
"/javascript", "/javascript",
@ -1895,7 +1895,7 @@ mod tests {
#[gpui::test(iterations = 10)] #[gpui::test(iterations = 10)]
async fn test_language_loading(cx: &mut TestAppContext) { async fn test_language_loading(cx: &mut TestAppContext) {
let mut languages = LanguageRegistry::test(); let mut languages = LanguageRegistry::test();
languages.set_executor(cx.executor().clone()); languages.set_executor(cx.executor());
let languages = Arc::new(languages); let languages = Arc::new(languages);
languages.register( languages.register(
"/JSON", "/JSON",

View file

@ -167,7 +167,7 @@ fn main() {
panic!("unexpected message"); panic!("unexpected message");
} }
cx.update(|cx| cx.quit()).ok(); cx.update(|cx| cx.shutdown()).ok();
}) })
.detach(); .detach();
}); });

View file

@ -1,9 +1,13 @@
use gpui::actions; use gpui::actions;
// todo!(remove this) // If the zed binary doesn't use anything in this crate, it will be optimized away
// and the actions won't initialize. So we just provide an empty initialization function
// to be called from main.
//
// These may provide relevant context:
// https://github.com/rust-lang/rust/issues/47384 // https://github.com/rust-lang/rust/issues/47384
// https://github.com/mmastrac/rust-ctor/issues/280 // https://github.com/mmastrac/rust-ctor/issues/280
pub fn unused() {} pub fn init() {}
actions!( actions!(
Cancel, Cancel,

View file

@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
use serde::Deserialize; use serde::Deserialize;
use smol::{fs, io::BufReader, process::Command}; use smol::{fs, io::BufReader, lock::Mutex, process::Command};
use std::process::{Output, Stdio}; use std::process::{Output, Stdio};
use std::{ use std::{
env::consts, env::consts,
@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync {
pub struct RealNodeRuntime { pub struct RealNodeRuntime {
http: Arc<dyn HttpClient>, http: Arc<dyn HttpClient>,
installation_lock: Mutex<()>,
} }
impl RealNodeRuntime { impl RealNodeRuntime {
pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> { pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
Arc::new(RealNodeRuntime { http }) Arc::new(RealNodeRuntime {
http,
installation_lock: Mutex::new(()),
})
} }
async fn install_if_needed(&self) -> Result<PathBuf> { async fn install_if_needed(&self) -> Result<PathBuf> {
let _lock = self.installation_lock.lock().await;
log::info!("Node runtime install_if_needed"); log::info!("Node runtime install_if_needed");
let arch = match consts::ARCH { let arch = match consts::ARCH {
@ -73,6 +78,9 @@ impl RealNodeRuntime {
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.args(["--cache".into(), node_dir.join("cache")])
.args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
.args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
.status() .status()
.await; .await;
let valid = matches!(result, Ok(status) if status.success()); let valid = matches!(result, Ok(status) if status.success());
@ -96,6 +104,11 @@ impl RealNodeRuntime {
archive.unpack(&node_containing_dir).await?; archive.unpack(&node_containing_dir).await?;
} }
// Note: Not in the `if !valid {}` so we can populate these for existing installations
_ = fs::create_dir(node_dir.join("cache")).await;
_ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
_ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
anyhow::Ok(node_dir) anyhow::Ok(node_dir)
} }
} }
@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime {
let mut command = Command::new(node_binary); let mut command = Command::new(node_binary);
command.env("PATH", env_path); command.env("PATH", env_path);
command.arg(npm_file).arg(subcommand).args(args); command.arg(npm_file).arg(subcommand);
command.args(["--cache".into(), installation_path.join("cache")]);
command.args([
"--userconfig".into(),
installation_path.join("blank_user_npmrc"),
]);
command.args([
"--globalconfig".into(),
installation_path.join("blank_global_npmrc"),
]);
command.args(args);
if let Some(directory) = directory { if let Some(directory) = directory {
command.current_dir(directory); command.current_dir(directory);

View file

@ -1,17 +1,17 @@
use editor::Editor; use editor::Editor;
use gpui::{ use gpui::{
div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity, div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, UniformListScrollHandle, View, ViewContext, WindowContext,
WindowContext,
}; };
use std::cmp; use std::{cmp, sync::Arc};
use ui::{prelude::*, v_stack, Divider}; use ui::{prelude::*, v_stack, Divider, Label, TextColor};
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
pub delegate: D, pub delegate: D,
scroll_handle: UniformListScrollHandle, scroll_handle: UniformListScrollHandle,
editor: View<Editor>, editor: View<Editor>,
pending_update_matches: Option<Task<Option<()>>>, pending_update_matches: Option<Task<()>>,
confirm_on_update: Option<bool>,
} }
pub trait PickerDelegate: Sized + 'static { pub trait PickerDelegate: Sized + 'static {
@ -21,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static {
fn selected_index(&self) -> usize; fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>); fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
// fn placeholder_text(&self) -> Arc<str>; fn placeholder_text(&self) -> Arc<str>;
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>; fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>); fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
@ -37,21 +37,28 @@ pub trait PickerDelegate: Sized + 'static {
impl<D: PickerDelegate> Picker<D> { impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self { pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
let editor = cx.build_view(|cx| Editor::single_line(cx)); let editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text(delegate.placeholder_text(), cx);
editor
});
cx.subscribe(&editor, Self::on_input_editor_event).detach(); cx.subscribe(&editor, Self::on_input_editor_event).detach();
Self { let mut this = Self {
delegate, delegate,
editor,
scroll_handle: UniformListScrollHandle::new(), scroll_handle: UniformListScrollHandle::new(),
pending_update_matches: None, pending_update_matches: None,
editor, confirm_on_update: None,
} };
this.update_matches("".to_string(), cx);
this
} }
pub fn focus(&self, cx: &mut WindowContext) { pub fn focus(&self, cx: &mut WindowContext) {
self.editor.update(cx, |editor, cx| editor.focus(cx)); self.editor.update(cx, |editor, cx| editor.focus(cx));
} }
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) { pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count(); let count = self.delegate.match_count();
if count > 0 { if count > 0 {
let index = self.delegate.selected_index(); let index = self.delegate.selected_index();
@ -91,16 +98,40 @@ impl<D: PickerDelegate> Picker<D> {
} }
} }
pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
let index = self.delegate.selected_index();
let new_index = if index + 1 == count { 0 } else { index + 1 };
self.delegate.set_selected_index(new_index, cx);
self.scroll_handle.scroll_to_item(new_index);
cx.notify();
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.delegate.dismissed(cx); self.delegate.dismissed(cx);
} }
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
self.delegate.confirm(false, cx); if self.pending_update_matches.is_some() {
self.confirm_on_update = Some(false)
} else {
self.delegate.confirm(false, cx);
}
} }
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) { fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
self.delegate.confirm(true, cx); if self.pending_update_matches.is_some() {
self.confirm_on_update = Some(true)
} else {
self.delegate.confirm(true, cx);
}
}
fn handle_click(&mut self, ix: usize, secondary: bool, cx: &mut ViewContext<Self>) {
cx.stop_propagation();
cx.prevent_default();
self.delegate.set_selected_index(ix, cx);
self.delegate.confirm(secondary, cx);
} }
fn on_input_editor_event( fn on_input_editor_event(
@ -115,6 +146,11 @@ impl<D: PickerDelegate> Picker<D> {
} }
} }
pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
let query = self.editor.read(cx).text(cx);
self.update_matches(query, cx);
}
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) { pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
let update = self.delegate.update_matches(query, cx); let update = self.delegate.update_matches(query, cx);
self.matches_updated(cx); self.matches_updated(cx);
@ -123,7 +159,7 @@ impl<D: PickerDelegate> Picker<D> {
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.matches_updated(cx); this.matches_updated(cx);
}) })
.ok() .ok();
})); }));
} }
@ -131,18 +167,19 @@ impl<D: PickerDelegate> Picker<D> {
let index = self.delegate.selected_index(); let index = self.delegate.selected_index();
self.scroll_handle.scroll_to_item(index); self.scroll_handle.scroll_to_item(index);
self.pending_update_matches = None; self.pending_update_matches = None;
if let Some(secondary) = self.confirm_on_update.take() {
self.delegate.confirm(secondary, cx);
}
cx.notify(); cx.notify();
} }
} }
impl<D: PickerDelegate> Render for Picker<D> { impl<D: PickerDelegate> Render for Picker<D> {
type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>; type Element = Div<Self>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div() div()
.context("picker") .key_context("picker")
.id("picker-container")
.focusable()
.size_full() .size_full()
.elevation_2(cx) .elevation_2(cx)
.on_action(Self::select_next) .on_action(Self::select_next)
@ -159,23 +196,51 @@ impl<D: PickerDelegate> Render for Picker<D> {
.child(div().px_1().py_0p5().child(self.editor.clone())), .child(div().px_1().py_0p5().child(self.editor.clone())),
) )
.child(Divider::horizontal()) .child(Divider::horizontal())
.child( .when(self.delegate.match_count() > 0, |el| {
v_stack() el.child(
.p_1() v_stack()
.grow() .p_1()
.child( .grow()
uniform_list("candidates", self.delegate.match_count(), { .child(
move |this: &mut Self, visible_range, cx| { uniform_list("candidates", self.delegate.match_count(), {
let selected_ix = this.delegate.selected_index(); move |this: &mut Self, visible_range, cx| {
visible_range let selected_ix = this.delegate.selected_index();
.map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx)) visible_range
.collect() .map(|ix| {
} div()
}) .on_mouse_down(
.track_scroll(self.scroll_handle.clone()), MouseButton::Left,
) move |this: &mut Self, event, cx| {
.max_h_72() this.handle_click(
.overflow_hidden(), ix,
) event.modifiers.command,
cx,
)
},
)
.child(this.delegate.render_match(
ix,
ix == selected_ix,
cx,
))
})
.collect()
}
})
.track_scroll(self.scroll_handle.clone()),
)
.max_h_72()
.overflow_hidden(),
)
})
.when(self.delegate.match_count() == 0, |el| {
el.child(
v_stack().p_1().grow().child(
div()
.px_1()
.child(Label::new("No matches").color(TextColor::Muted)),
),
)
})
} }
} }

View file

@ -497,7 +497,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -573,7 +573,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -638,7 +638,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -731,7 +731,7 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -812,7 +812,7 @@ mod tests {
async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) { ) {
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({

View file

@ -863,7 +863,7 @@ impl Project {
cx: &mut gpui::TestAppContext, cx: &mut gpui::TestAppContext,
) -> Model<Project> { ) -> Model<Project> {
let mut languages = LanguageRegistry::test(); let mut languages = LanguageRegistry::test();
languages.set_executor(cx.executor().clone()); languages.set_executor(cx.executor());
let http_client = util::http::FakeHttpClient::with_404_response(); let http_client = util::http::FakeHttpClient::with_404_response();
let client = cx.update(|cx| client::Client::new(http_client.clone(), cx)); let client = cx.update(|cx| client::Client::new(http_client.clone(), cx));
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx)); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));

View file

@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/the-root", "/the-root",
json!({ json!({
@ -189,7 +189,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/the-root", "/the-root",
json!({ json!({
@ -547,7 +547,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/the-root", "/the-root",
json!({ json!({
@ -734,7 +734,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -826,7 +826,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ json!({
@ -914,7 +914,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -1046,7 +1046,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await; fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@ -1125,7 +1125,7 @@ async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAp
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "x" })).await; fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@ -1215,7 +1215,7 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await; fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@ -1279,7 +1279,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" })) fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
.await; .await;
@ -1401,7 +1401,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
" "
.unindent(); .unindent();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": text })).await; fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@ -1671,7 +1671,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
"let three = 3;\n", "let three = 3;\n",
); );
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": text })).await; fs.insert_tree("/dir", json!({ "a.rs": text })).await;
let project = Project::test(fs, ["/dir".as_ref()], cx).await; let project = Project::test(fs, ["/dir".as_ref()], cx).await;
@ -1734,7 +1734,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
.await; .await;
@ -1813,7 +1813,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
" "
.unindent(); .unindent();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -1959,7 +1959,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp
" "
.unindent(); .unindent();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2067,7 +2067,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
" "
.unindent(); .unindent();
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2187,7 +2187,7 @@ async fn test_definition(cx: &mut gpui::TestAppContext) {
); );
let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2299,7 +2299,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2396,7 +2396,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2451,7 +2451,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
); );
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await; let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2559,7 +2559,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
async fn test_save_file(cx: &mut gpui::TestAppContext) { async fn test_save_file(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2591,7 +2591,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2622,7 +2622,7 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
async fn test_save_as(cx: &mut gpui::TestAppContext) { async fn test_save_as(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree("/dir", json!({})).await; fs.insert_tree("/dir", json!({})).await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
@ -2830,7 +2830,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) { async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2881,7 +2881,7 @@ async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -2927,7 +2927,7 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3074,7 +3074,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let initial_contents = "aaa\nbbbbb\nc\n"; let initial_contents = "aaa\nbbbbb\nc\n";
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3154,7 +3154,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3216,7 +3216,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/the-dir", "/the-dir",
json!({ json!({
@ -3479,7 +3479,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
})) }))
.await; .await;
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3596,7 +3596,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
async fn test_search(cx: &mut gpui::TestAppContext) { async fn test_search(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3655,7 +3655,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
let search_query = "file"; let search_query = "file";
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3767,7 +3767,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
let search_query = "file"; let search_query = "file";
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({
@ -3878,7 +3878,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
let search_query = "file"; let search_query = "file";
let fs = FakeFs::new(cx.executor().clone()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/dir", "/dir",
json!({ json!({

View file

@ -0,0 +1,41 @@
[package]
name = "project_panel2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/project_panel.rs"
doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
collections = { path = "../collections" }
db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" }
gpui = { path = "../gpui2", package = "gpui2" }
menu = { path = "../menu2", package = "menu2" }
project = { path = "../project2", package = "project2" }
settings = { path = "../settings2", package = "settings2" }
theme = { path = "../theme2", package = "theme2" }
ui = { path = "../ui2", package = "ui2" }
util = { path = "../util" }
workspace = { path = "../workspace2", package = "workspace2" }
anyhow.workspace = true
postage.workspace = true
futures.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
schemars.workspace = true
smallvec.workspace = true
pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
client = { path = "../client2", package = "client2", features = ["test-support"] }
language = { path = "../language2", package = "language2", features = ["test-support"] }
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
serde_json.workspace = true

View file

@ -0,0 +1,96 @@
use std::{path::Path, str, sync::Arc};
use collections::HashMap;
use gpui::{AppContext, AssetSource};
use serde_derive::Deserialize;
use util::{maybe, paths::PathExt};
#[derive(Deserialize, Debug)]
struct TypeConfig {
icon: Arc<str>,
}
#[derive(Deserialize, Debug)]
pub struct FileAssociations {
suffixes: HashMap<String, String>,
types: HashMap<String, TypeConfig>,
}
const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
cx.set_global(FileAssociations::new(assets))
}
impl FileAssociations {
pub fn new(assets: impl AssetSource) -> Self {
assets
.load("icons/file_icons/file_types.json")
.and_then(|file| {
serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
.map_err(Into::into)
})
.unwrap_or_else(|_| FileAssociations {
suffixes: HashMap::default(),
types: HashMap::default(),
})
}
pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
// FIXME: Associate a type with the languages and have the file's langauge
// override these associations
maybe!({
let suffix = path.icon_suffix()?;
this.suffixes
.get(suffix)
.and_then(|type_str| this.types.get(type_str))
.map(|type_config| type_config.icon.clone())
})
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_DIRECTORY_TYPE
} else {
COLLAPSED_DIRECTORY_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
maybe!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_CHEVRON_TYPE
} else {
COLLAPSED_CHEVRON_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,45 @@
use anyhow;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPanelDockPosition {
Left,
Right,
}
#[derive(Deserialize, Debug)]
pub struct ProjectPanelSettings {
pub default_width: f32,
pub dock: ProjectPanelDockPosition,
pub file_icons: bool,
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent {
pub default_width: Option<f32>,
pub dock: Option<ProjectPanelDockPosition>,
pub file_icons: Option<bool>,
pub folder_icons: Option<bool>,
pub git_status: Option<bool>,
pub indent_size: Option<f32>,
}
impl Settings for ProjectPanelSettings {
const KEY: Option<&'static str> = Some("project_panel");
type FileContent = ProjectPanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View file

@ -577,18 +577,18 @@ mod tests {
let client2 = Peer::new(0); let client2 = Peer::new(0);
let (client1_to_server_conn, server_to_client_1_conn, _kill) = let (client1_to_server_conn, server_to_client_1_conn, _kill) =
Connection::in_memory(cx.executor().clone()); Connection::in_memory(cx.executor());
let (client1_conn_id, io_task1, client1_incoming) = let (client1_conn_id, io_task1, client1_incoming) =
client1.add_test_connection(client1_to_server_conn, cx.executor().clone()); client1.add_test_connection(client1_to_server_conn, cx.executor());
let (_, io_task2, server_incoming1) = let (_, io_task2, server_incoming1) =
server.add_test_connection(server_to_client_1_conn, cx.executor().clone()); server.add_test_connection(server_to_client_1_conn, cx.executor());
let (client2_to_server_conn, server_to_client_2_conn, _kill) = let (client2_to_server_conn, server_to_client_2_conn, _kill) =
Connection::in_memory(cx.executor().clone()); Connection::in_memory(cx.executor());
let (client2_conn_id, io_task3, client2_incoming) = let (client2_conn_id, io_task3, client2_incoming) =
client2.add_test_connection(client2_to_server_conn, cx.executor().clone()); client2.add_test_connection(client2_to_server_conn, cx.executor());
let (_, io_task4, server_incoming2) = let (_, io_task4, server_incoming2) =
server.add_test_connection(server_to_client_2_conn, cx.executor().clone()); server.add_test_connection(server_to_client_2_conn, cx.executor());
executor.spawn(io_task1).detach(); executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach(); executor.spawn(io_task2).detach();
@ -763,16 +763,16 @@ mod tests {
#[gpui::test(iterations = 50)] #[gpui::test(iterations = 50)]
async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { async fn test_dropping_request_before_completion(cx: &mut TestAppContext) {
let executor = cx.executor().clone(); let executor = cx.executor();
let server = Peer::new(0); let server = Peer::new(0);
let client = Peer::new(0); let client = Peer::new(0);
let (client_to_server_conn, server_to_client_conn, _kill) = let (client_to_server_conn, server_to_client_conn, _kill) =
Connection::in_memory(cx.executor().clone()); Connection::in_memory(cx.executor());
let (client_to_server_conn_id, io_task1, mut client_incoming) = let (client_to_server_conn_id, io_task1, mut client_incoming) =
client.add_test_connection(client_to_server_conn, cx.executor().clone()); client.add_test_connection(client_to_server_conn, cx.executor());
let (server_to_client_conn_id, io_task2, mut server_incoming) = let (server_to_client_conn_id, io_task2, mut server_incoming) =
server.add_test_connection(server_to_client_conn, cx.executor().clone()); server.add_test_connection(server_to_client_conn, cx.executor());
executor.spawn(io_task1).detach(); executor.spawn(io_task1).detach();
executor.spawn(io_task2).detach(); executor.spawn(io_task2).detach();

View file

@ -10,16 +10,15 @@ use collections::HashMap;
use editor::Editor; use editor::Editor;
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{ use gpui::{
action, actions, blue, div, red, rems, white, Action, AnyElement, AnyView, AppContext, action, actions, div, red, Action, AppContext, Component, Div, EventEmitter,
Component, Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription, ParentComponent as _, Render, Styled, Subscription, Task, View, ViewContext,
Svg, Task, View, ViewContext, VisualContext as _, WindowContext, VisualContext as _, WindowContext,
}; };
use project::search::SearchQuery; use project::search::SearchQuery;
use serde::Deserialize;
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, Button, ButtonGroup, Icon, IconButton, IconElement, Label, StyledExt}; use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,

View file

@ -1,17 +1,10 @@
use std::{borrow::Cow, sync::Arc}; use std::{borrow::Cow, sync::Arc};
use gpui::{ use gpui::{div, Action, Component, ViewContext};
div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent, use ui::{Button, ButtonVariant};
ParentElement as _, StatelessInteractive, Styled, Svg, View, ViewContext,
};
use theme::ActiveTheme;
use ui::{v_stack, Button, ButtonVariant, Label};
use workspace::searchable::Direction; use workspace::searchable::Direction;
use crate::{ use crate::mode::{SearchMode, Side};
mode::{SearchMode, Side},
SelectNextMatch, SelectPrevMatch,
};
pub(super) fn render_nav_button<V: 'static>( pub(super) fn render_nav_button<V: 'static>(
icon: &'static str, icon: &'static str,

View file

@ -33,7 +33,7 @@ lazy_static.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
async-trait.workspace = true async-trait.workspace = true
tiktoken-rs = "0.5.0" tiktoken-rs.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
rand.workspace = true rand.workspace = true
schemars.workspace = true schemars.workspace = true

View file

@ -9,7 +9,7 @@ use schemars::{
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use util::{asset_str, ResultExt}; use util::asset_str;
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)] #[serde(transparent)]
@ -86,7 +86,9 @@ impl KeymapFile {
"invalid binding value for keystroke {keystroke}, context {context:?}" "invalid binding value for keystroke {keystroke}, context {context:?}"
) )
}) })
.log_err() // todo!()
.ok()
// .log_err()
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
}) })
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;

View file

@ -1,5 +1,5 @@
use crate::story::Story; use crate::story::Story;
use gpui::{px, Div, Render}; use gpui::{prelude::*, px, Div, Render};
use theme2::{default_color_scales, ColorScaleStep}; use theme2::{default_color_scales, ColorScaleStep};
use ui::prelude::*; use ui::prelude::*;

View file

@ -1,12 +1,15 @@
use gpui::{ use gpui::{
actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render, actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext, WindowContext,
}; };
use theme2::ActiveTheme; use theme2::ActiveTheme;
actions!(ActionA, ActionB, ActionC); actions!(ActionA, ActionB, ActionC);
pub struct FocusStory {} pub struct FocusStory {
child_1_focus: FocusHandle,
child_2_focus: FocusHandle,
}
impl FocusStory { impl FocusStory {
pub fn view(cx: &mut WindowContext) -> View<Self> { pub fn view(cx: &mut WindowContext) -> View<Self> {
@ -16,12 +19,15 @@ impl FocusStory {
KeyBinding::new("cmd-c", ActionC, None), KeyBinding::new("cmd-c", ActionC, None),
]); ]);
cx.build_view(move |cx| Self {}) cx.build_view(move |cx| Self {
child_1_focus: cx.focus_handle(),
child_2_focus: cx.focus_handle(),
})
} }
} }
impl Render for FocusStory { impl Render for FocusStory {
type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>; type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme(); let theme = cx.theme();
@ -31,18 +37,16 @@ impl Render for FocusStory {
let color_4 = theme.status().conflict; let color_4 = theme.status().conflict;
let color_5 = theme.status().ignored; let color_5 = theme.status().ignored;
let color_6 = theme.status().renamed; let color_6 = theme.status().renamed;
let child_1 = cx.focus_handle();
let child_2 = cx.focus_handle();
div() div()
.id("parent") .id("parent")
.focusable() .focusable()
.context("parent") .key_context("parent")
.on_action(|_, action: &ActionA, cx| { .on_action(|_, action: &ActionA, cx| {
println!("Action A dispatched on parent during"); println!("Action A dispatched on parent");
}) })
.on_action(|_, action: &ActionB, cx| { .on_action(|_, action: &ActionB, cx| {
println!("Action B dispatched on parent during"); println!("Action B dispatched on parent");
}) })
.on_focus(|_, _, _| println!("Parent focused")) .on_focus(|_, _, _| println!("Parent focused"))
.on_blur(|_, _, _| println!("Parent blurred")) .on_blur(|_, _, _| println!("Parent blurred"))
@ -56,8 +60,8 @@ impl Render for FocusStory {
.focus_in(|style| style.bg(color_3)) .focus_in(|style| style.bg(color_3))
.child( .child(
div() div()
.track_focus(&child_1) .track_focus(&self.child_1_focus)
.context("child-1") .key_context("child-1")
.on_action(|_, action: &ActionB, cx| { .on_action(|_, action: &ActionB, cx| {
println!("Action B dispatched on child 1 during"); println!("Action B dispatched on child 1 during");
}) })
@ -76,10 +80,10 @@ impl Render for FocusStory {
) )
.child( .child(
div() div()
.track_focus(&child_2) .track_focus(&self.child_2_focus)
.context("child-2") .key_context("child-2")
.on_action(|_, action: &ActionC, cx| { .on_action(|_, action: &ActionC, cx| {
println!("Action C dispatched on child 2 during"); println!("Action C dispatched on child 2");
}) })
.w_full() .w_full()
.h_6() .h_6()

View file

@ -1,5 +1,5 @@
use crate::{story::Story, story_selector::ComponentStory}; use crate::{story::Story, story_selector::ComponentStory};
use gpui::{Div, Render, StatefulInteractivity, View, VisualContext}; use gpui::{prelude::*, Div, Render, Stateful, View};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use ui::prelude::*; use ui::prelude::*;
@ -12,7 +12,7 @@ impl KitchenSinkStory {
} }
impl Render for KitchenSinkStory { impl Render for KitchenSinkStory {
type Element = Div<Self, StatefulInteractivity<Self>>; type Element = Stateful<Self, Div<Self>>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let component_stories = ComponentStory::iter() let component_stories = ComponentStory::iter()

View file

@ -1,11 +1,7 @@
use std::sync::Arc;
use fuzzy::StringMatchCandidate; use fuzzy::StringMatchCandidate;
use gpui::{ use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task,
View, VisualContext, WindowContext,
};
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use theme2::ActiveTheme; use theme2::ActiveTheme;
pub struct PickerStory { pub struct PickerStory {
@ -44,6 +40,10 @@ impl PickerDelegate for Delegate {
self.candidates.len() self.candidates.len()
} }
fn placeholder_text(&self) -> Arc<str> {
"Test".into()
}
fn render_match( fn render_match(
&self, &self,
ix: usize, ix: usize,

Some files were not shown because too many files have changed in this diff Show more