mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
Merge branch 'main' into search2
This commit is contained in:
commit
b11bfa8821
67
Cargo.lock
generated
67
Cargo.lock
generated
@ -2012,7 +2012,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme",
|
||||
"theme2",
|
||||
"util",
|
||||
]
|
||||
|
||||
@ -2768,7 +2768,6 @@ dependencies = [
|
||||
"copilot2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"drag_and_drop",
|
||||
"env_logger 0.9.3",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
@ -3062,6 +3061,31 @@ dependencies = [
|
||||
"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]]
|
||||
name = "filetime"
|
||||
version = "0.2.22"
|
||||
@ -4199,6 +4223,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"gpui2",
|
||||
"log",
|
||||
"serde",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
@ -6609,6 +6634,36 @@ dependencies = [
|
||||
"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]]
|
||||
name = "project_symbols"
|
||||
version = "0.1.0"
|
||||
@ -9286,9 +9341,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.5.4"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9ae5a3c24361e5f038af22517ba7f8e3af4099e30e78a3d56f86b48238fce9d"
|
||||
checksum = "a4427b6b1c6b38215b92dd47a83a0ecc6735573d0a5a4c14acc0ac5b33b28adb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.4",
|
||||
@ -11423,6 +11478,7 @@ dependencies = [
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"feature_flags2",
|
||||
"file_finder2",
|
||||
"fs2",
|
||||
"fsevent",
|
||||
"futures 0.3.28",
|
||||
@ -11432,7 +11488,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"image",
|
||||
"indexmap 1.9.3",
|
||||
"install_cli",
|
||||
"install_cli2",
|
||||
"isahc",
|
||||
"journal2",
|
||||
"language2",
|
||||
@ -11447,6 +11503,7 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope2",
|
||||
|
@ -80,6 +80,7 @@ members = [
|
||||
"crates/project",
|
||||
"crates/project2",
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/recent_projects",
|
||||
"crates/rope",
|
||||
@ -155,6 +156,7 @@ tempdir = { version = "0.3.7" }
|
||||
thiserror = { version = "1.0.29" }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
toml = { version = "0.5" }
|
||||
tiktoken-rs = "0.5.7"
|
||||
tree-sitter = "0.20"
|
||||
unindent = { version = "0.1.7" }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
@ -10,6 +10,7 @@
|
||||
"bindings": {
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
@ -18,7 +19,7 @@
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
"cmd--": "editor::Fold",
|
||||
"cmd-=": "editor::UnfoldLines",
|
||||
"cmd-+": "editor::UnfoldLines",
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-g": [
|
||||
"editor::SelectNext",
|
||||
|
@ -174,7 +174,8 @@
|
||||
//
|
||||
// 1. "gpt-3.5-turbo-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.
|
||||
"show_call_status_icon": true,
|
||||
@ -270,9 +271,7 @@
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
"disabled_globs": [".env"]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
@ -381,12 +380,7 @@
|
||||
// Default directories to search for virtual environments, relative
|
||||
// to the current working directory. We recommend overriding this
|
||||
// in your project's settings, rather than globally.
|
||||
"directories": [
|
||||
".env",
|
||||
"env",
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
"directories": [".env", "env", ".venv", "venv"],
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
|
@ -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"] }
|
@ -29,7 +29,7 @@ postage.workspace = true
|
||||
rand.workspace = true
|
||||
log.workspace = true
|
||||
parse_duration = "2.1.1"
|
||||
tiktoken-rs = "0.5.0"
|
||||
tiktoken-rs.workspace = true
|
||||
matrixmultiply = "0.3.7"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
bincode = "1.3.3"
|
||||
|
@ -29,7 +29,7 @@ postage.workspace = true
|
||||
rand.workspace = true
|
||||
log.workspace = true
|
||||
parse_duration = "2.1.1"
|
||||
tiktoken-rs = "0.5.0"
|
||||
tiktoken-rs.workspace = true
|
||||
matrixmultiply = "0.3.7"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
bincode = "1.3.3"
|
||||
|
@ -40,7 +40,7 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs = "0.5"
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -9,6 +9,8 @@ pub enum OpenAIModel {
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
@ -16,6 +18,7 @@ impl OpenAIModel {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,13 +26,15 @@ impl OpenAIModel {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::ThreePointFiveTurbo,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ impl FakeServer {
|
||||
peer: Peer::new(0),
|
||||
state: Default::default(),
|
||||
user_id: client_user_id,
|
||||
executor: cx.executor().clone(),
|
||||
executor: cx.executor(),
|
||||
};
|
||||
|
||||
client
|
||||
|
@ -510,7 +510,7 @@ fn test_fuzzy_like_string() {
|
||||
|
||||
#[gpui::test]
|
||||
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();
|
||||
for (i, github_login) in [
|
||||
"California",
|
||||
|
@ -4,6 +4,7 @@ use gpui::{Model, TestAppContext};
|
||||
mod channel_buffer_tests;
|
||||
mod channel_message_tests;
|
||||
mod channel_tests;
|
||||
mod editor_tests;
|
||||
mod following_tests;
|
||||
mod integration_tests;
|
||||
mod notification_tests;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,32 @@
|
||||
// use editor::{
|
||||
// test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion,
|
||||
// ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, Undo,
|
||||
//todo(partially ported)
|
||||
// use std::{
|
||||
// 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)]
|
||||
// async fn test_host_disconnect(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -11,7 +34,7 @@
|
||||
// cx_b: &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_b = server.create_client(cx_b, "user_b").await;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@ -25,7 +48,7 @@
|
||||
// .fs()
|
||||
// .insert_tree(
|
||||
// "/a",
|
||||
// json!({
|
||||
// serde_json::json!({
|
||||
// "a.txt": "a-contents",
|
||||
// "b.txt": "b-contents",
|
||||
// }),
|
||||
@ -35,7 +58,7 @@
|
||||
// let active_call_a = cx_a.read(ActiveCall::global);
|
||||
// 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
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
@ -46,21 +69,25 @@
|
||||
|
||||
// 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));
|
||||
// let workspace_b = window_b.root(cx_b);
|
||||
// let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
|
||||
|
||||
// let editor_b = workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
// })
|
||||
// .unwrap()
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .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));
|
||||
// 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.
|
||||
// server.forbid_connections();
|
||||
@ -77,10 +104,10 @@
|
||||
|
||||
// // 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!(!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.
|
||||
// let can_close = workspace_b
|
||||
@ -120,7 +147,6 @@
|
||||
// project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
|
||||
// }
|
||||
|
||||
//todo!(editor)
|
||||
// #[gpui::test]
|
||||
// async fn test_newline_above_or_below_does_not_move_guest_cursor(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -152,12 +178,14 @@
|
||||
// .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), 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), cx));
|
||||
// let window_a = cx_a.add_empty_window();
|
||||
// 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 {
|
||||
// cx: cx_a,
|
||||
// window: window_a.into(),
|
||||
// editor: editor_a,
|
||||
// assertion_cx: AssertionContextManager::new(),
|
||||
// };
|
||||
|
||||
// // Open a buffer as client B
|
||||
@ -165,12 +193,14 @@
|
||||
// .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), 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), cx));
|
||||
// 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), cx));
|
||||
// let mut editor_cx_b = EditorTestContext {
|
||||
// cx: cx_b,
|
||||
// window: window_b.into(),
|
||||
// editor: editor_b,
|
||||
// assertion_cx: AssertionContextManager::new(),
|
||||
// };
|
||||
|
||||
// // Test newline above
|
||||
@ -214,7 +244,6 @@
|
||||
// "});
|
||||
// }
|
||||
|
||||
//todo!(editor)
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_collaborating_with_completion(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -275,8 +304,8 @@
|
||||
// .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| {
|
||||
// let window_b = cx_b.add_empty_window();
|
||||
// let editor_b = window_b.build_view(cx_b, |cx| {
|
||||
// Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
|
||||
// });
|
||||
|
||||
@ -384,7 +413,7 @@
|
||||
// );
|
||||
|
||||
// // The additional edit is applied.
|
||||
// cx_a.foreground().run_until_parked();
|
||||
// cx_a.executor().run_until_parked();
|
||||
|
||||
// buffer_a.read_with(cx_a, |buffer, _| {
|
||||
// assert_eq!(
|
||||
@ -400,7 +429,7 @@
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
//todo!(editor)
|
||||
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_collaborating_with_code_actions(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -619,7 +648,6 @@
|
||||
// });
|
||||
// }
|
||||
|
||||
//todo!(editor)
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_collaborating_with_renames(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -813,7 +841,6 @@
|
||||
// })
|
||||
// }
|
||||
|
||||
//todo!(editor)
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_language_server_statuses(
|
||||
// executor: BackgroundExecutor,
|
||||
@ -937,8 +964,8 @@
|
||||
// cx_b: &mut TestAppContext,
|
||||
// cx_c: &mut TestAppContext,
|
||||
// ) {
|
||||
// let window_b = cx_b.add_window(|_| EmptyView);
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let window_b = cx_b.add_empty_window();
|
||||
// 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;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@ -1052,7 +1079,7 @@
|
||||
// .await
|
||||
// .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
|
||||
// executor.run_until_parked();
|
||||
@ -1106,3 +1133,757 @@
|
||||
// == 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
|
||||
// }
|
||||
|
@ -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
|
||||
// }
|
||||
|
@ -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 workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
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 {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
|
@ -1,14 +1,17 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke,
|
||||
ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext,
|
||||
WeakView, WindowContext,
|
||||
actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
|
||||
Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{v_stack, Label, StyledExt};
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
@ -127,16 +130,7 @@ impl CommandPaletteDelegate {
|
||||
) -> Self {
|
||||
Self {
|
||||
command_palette,
|
||||
matches: commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, command)| StringMatch {
|
||||
candidate_id: i,
|
||||
string: command.name.clone(),
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect(),
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
previous_focus_handle,
|
||||
@ -147,6 +141,10 @@ impl CommandPaletteDelegate {
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
type ListItem = Div<Picker<Self>>;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
@ -296,11 +294,10 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Self::ListItem {
|
||||
let colors = cx.theme().colors();
|
||||
let Some(command) = self
|
||||
.matches
|
||||
.get(ix)
|
||||
.and_then(|m| self.commands.get(m.candidate_id))
|
||||
else {
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return div();
|
||||
};
|
||||
let Some(command) = self.commands.get(r#match.candidate_id) else {
|
||||
return div();
|
||||
};
|
||||
|
||||
@ -312,63 +309,16 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.rounded_md()
|
||||
.when(selected, |this| this.bg(colors.ghost_element_selected))
|
||||
.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 {
|
||||
|
@ -24,7 +24,7 @@ collections = { path = "../collections" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { path = "../theme" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
|
@ -351,28 +351,29 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// pub fn fake(cx: &mut gpui::TestAppContext) -> (ModelHandle<Self>, lsp::FakeLanguageServer) {
|
||||
// use node_runtime::FakeNodeRuntime;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn fake(cx: &mut gpui::TestAppContext) -> (Model<Self>, lsp::FakeLanguageServer) {
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
// let (server, fake_server) =
|
||||
// LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
|
||||
// let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
// let node_runtime = FakeNodeRuntime::new();
|
||||
// let this = cx.add_model(|_| Self {
|
||||
// server_id: LanguageServerId(0),
|
||||
// http: http.clone(),
|
||||
// node_runtime,
|
||||
// server: CopilotServer::Running(RunningCopilotServer {
|
||||
// name: LanguageServerName(Arc::from("copilot")),
|
||||
// lsp: Arc::new(server),
|
||||
// sign_in_status: SignInStatus::Authorized,
|
||||
// registered_buffers: Default::default(),
|
||||
// }),
|
||||
// buffers: Default::default(),
|
||||
// });
|
||||
// (this, fake_server)
|
||||
// }
|
||||
let (server, fake_server) =
|
||||
LanguageServer::fake("copilot".into(), Default::default(), cx.to_async());
|
||||
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
let this = cx.build_model(|cx| Self {
|
||||
server_id: LanguageServerId(0),
|
||||
http: http.clone(),
|
||||
node_runtime,
|
||||
server: CopilotServer::Running(RunningCopilotServer {
|
||||
name: LanguageServerName(Arc::from("copilot")),
|
||||
lsp: Arc::new(server),
|
||||
sign_in_status: SignInStatus::Authorized,
|
||||
registered_buffers: Default::default(),
|
||||
}),
|
||||
_subscription: cx.on_app_quit(Self::shutdown_language_server),
|
||||
buffers: Default::default(),
|
||||
});
|
||||
(this, fake_server)
|
||||
}
|
||||
|
||||
fn start_language_server(
|
||||
new_server_id: LanguageServerId,
|
||||
|
@ -27,7 +27,6 @@ client = { package = "client2", path = "../client2" }
|
||||
clock = { path = "../clock" }
|
||||
copilot = { package="copilot2", path = "../copilot2" }
|
||||
db = { package="db2", path = "../db2" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
|
@ -578,12 +578,7 @@ impl DisplaySnapshot {
|
||||
line.push_str(chunk.chunk);
|
||||
|
||||
let text_style = if let Some(style) = chunk.style {
|
||||
editor_style
|
||||
.text
|
||||
.clone()
|
||||
.highlight(style)
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
|
||||
Cow::Owned(editor_style.text.clone().highlight(style))
|
||||
} else {
|
||||
Cow::Borrowed(&editor_style.text)
|
||||
};
|
||||
|
@ -2,9 +2,9 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
Highlights,
|
||||
};
|
||||
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{AnyElement, ViewContext};
|
||||
use gpui::{AnyElement, Pixels, ViewContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@ -82,13 +82,13 @@ pub enum BlockStyle {
|
||||
|
||||
pub struct BlockContext<'a, 'b> {
|
||||
pub view_context: &'b mut ViewContext<'a, Editor>,
|
||||
pub anchor_x: f32,
|
||||
pub scroll_x: f32,
|
||||
pub gutter_width: f32,
|
||||
pub gutter_padding: f32,
|
||||
pub em_width: f32,
|
||||
pub line_height: f32,
|
||||
pub anchor_x: Pixels,
|
||||
pub gutter_width: Pixels,
|
||||
pub gutter_padding: Pixels,
|
||||
pub em_width: Pixels,
|
||||
pub line_height: Pixels,
|
||||
pub block_id: usize,
|
||||
pub editor_style: &'b EditorStyle,
|
||||
}
|
||||
|
||||
#[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
@ -1220,8 +1220,6 @@ pub mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
// todo!()
|
||||
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
|
||||
#[gpui::test]
|
||||
async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
|
||||
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]
|
||||
async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@ -1458,8 +1454,6 @@ pub mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
// todo!()
|
||||
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
|
||||
#[gpui::test]
|
||||
async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@ -1668,8 +1662,6 @@ pub mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
// todo!()
|
||||
#[ignore = "fails due to unimplemented `impl PlatformAtlas for TestAtlas` method"]
|
||||
#[gpui::test]
|
||||
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
|
||||
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]
|
||||
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
|
||||
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)]
|
||||
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
|
||||
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)]
|
||||
async fn test_multiple_excerpts_large_multibuffer(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@ -2455,14 +2441,9 @@ pub mod tests {
|
||||
project.update(cx, |project, _| {
|
||||
project.languages().add(Arc::clone(&language))
|
||||
});
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().read_with(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
@ -2620,6 +2601,10 @@ pub mod tests {
|
||||
"main hint #1".to_string(),
|
||||
"main hint #2".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!(
|
||||
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]
|
||||
async fn test_excerpts_removed(cx: &mut gpui::TestAppContext) {
|
||||
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.languages().add(Arc::clone(&language))
|
||||
});
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().read_with(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees().next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let buffer_1 = project
|
||||
.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]
|
||||
async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
|
||||
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]
|
||||
async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
|
@ -9,7 +9,7 @@ use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
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,
|
||||
};
|
||||
use language::{
|
||||
@ -30,6 +30,7 @@ use std::{
|
||||
};
|
||||
use text::Selection;
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use ui::{Label, TextColor};
|
||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
|
||||
use workspace::{
|
||||
@ -595,16 +596,19 @@ impl Item for Editor {
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(self.title(cx).to_string())
|
||||
.child(Label::new(self.title(cx).to_string()))
|
||||
.children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
|
||||
Some(
|
||||
div()
|
||||
.text_color(theme.colors().text_muted)
|
||||
.text_xs()
|
||||
.child(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)),
|
||||
div().child(
|
||||
Label::new(util::truncate_and_trailoff(
|
||||
&description,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
))
|
||||
.color(TextColor::Muted),
|
||||
),
|
||||
)
|
||||
})),
|
||||
)
|
||||
|
@ -11,19 +11,18 @@ pub enum ScrollAmount {
|
||||
|
||||
impl ScrollAmount {
|
||||
pub fn lines(&self, editor: &mut Editor) -> f32 {
|
||||
todo!()
|
||||
// match self {
|
||||
// Self::Line(count) => *count,
|
||||
// Self::Page(count) => editor
|
||||
// .visible_line_count()
|
||||
// .map(|mut l| {
|
||||
// // for full pages subtract one to leave an anchor line
|
||||
// if count.abs() == 1.0 {
|
||||
// l -= 1.0
|
||||
// }
|
||||
// (l * count).trunc()
|
||||
// })
|
||||
// .unwrap_or(0.),
|
||||
// }
|
||||
match self {
|
||||
Self::Line(count) => *count,
|
||||
Self::Page(count) => editor
|
||||
.visible_line_count()
|
||||
.map(|mut l| {
|
||||
// for full pages subtract one to leave an anchor line
|
||||
if count.abs() == 1.0 {
|
||||
l -= 1.0
|
||||
}
|
||||
(l * count).trunc()
|
||||
})
|
||||
.unwrap_or(0.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -315,11 +315,14 @@ impl SelectionsCollection {
|
||||
|
||||
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;
|
||||
if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
|
||||
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 = DisplayPoint::new(row, end_col);
|
||||
dbg!(start_col, end_col);
|
||||
|
||||
Some(Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
|
@ -1,81 +1,74 @@
|
||||
pub mod editor_lsp_test_context;
|
||||
pub mod editor_test_context;
|
||||
|
||||
// todo!()
|
||||
// use crate::{
|
||||
// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
// DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
// };
|
||||
use crate::{
|
||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
||||
DisplayPoint, Editor, EditorMode, MultiBuffer,
|
||||
};
|
||||
|
||||
// use gpui::{Model, ViewContext};
|
||||
use gpui::{Context, Model, Pixels, ViewContext};
|
||||
|
||||
// use project::Project;
|
||||
// use util::test::{marked_text_offsets, marked_text_ranges};
|
||||
use project::Project;
|
||||
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||
|
||||
// #[cfg(test)]
|
||||
// #[ctor::ctor]
|
||||
// fn init_logger() {
|
||||
// if std::env::var("RUST_LOG").is_ok() {
|
||||
// env_logger::init();
|
||||
// }
|
||||
// }
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
|
||||
// // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
|
||||
// pub fn marked_display_snapshot(
|
||||
// text: &str,
|
||||
// cx: &mut gpui::AppContext,
|
||||
// ) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
||||
// let (unmarked_text, markers) = marked_text_offsets(text);
|
||||
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
|
||||
pub fn marked_display_snapshot(
|
||||
text: &str,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
|
||||
let (unmarked_text, markers) = marked_text_offsets(text);
|
||||
|
||||
// let family_id = cx
|
||||
// .font_cache()
|
||||
// .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 font = cx.text_style().font();
|
||||
let font_size: Pixels = 14.into();
|
||||
|
||||
// let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||
// let display_map =
|
||||
// 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 markers = markers
|
||||
// .into_iter()
|
||||
// .map(|offset| offset.to_display_point(&snapshot))
|
||||
// .collect();
|
||||
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
|
||||
let display_map = cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
|
||||
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let markers = markers
|
||||
.into_iter()
|
||||
.map(|offset| offset.to_display_point(&snapshot))
|
||||
.collect();
|
||||
|
||||
// (snapshot, markers)
|
||||
// }
|
||||
(snapshot, markers)
|
||||
}
|
||||
|
||||
// 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);
|
||||
// assert_eq!(editor.text(cx), unmarked_text);
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
|
||||
// }
|
||||
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);
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
|
||||
}
|
||||
|
||||
// pub fn assert_text_with_selections(
|
||||
// editor: &mut Editor,
|
||||
// marked_text: &str,
|
||||
// cx: &mut ViewContext<Editor>,
|
||||
// ) {
|
||||
// let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
// assert_eq!(editor.text(cx), unmarked_text);
|
||||
// assert_eq!(editor.selections.ranges(cx), text_ranges);
|
||||
// }
|
||||
pub fn assert_text_with_selections(
|
||||
editor: &mut Editor,
|
||||
marked_text: &str,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
|
||||
assert_eq!(editor.text(cx), unmarked_text);
|
||||
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
|
||||
// #[allow(dead_code)]
|
||||
// #[cfg(any(test, feature = "test-support"))]
|
||||
// pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
||||
// Editor::new(EditorMode::Full, buffer, None, None, cx)
|
||||
// }
|
||||
// RA thinks this is dead code even though it is used in a whole lot of tests
|
||||
#[allow(dead_code)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
|
||||
// todo!()
|
||||
Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
|
||||
}
|
||||
|
||||
// pub(crate) fn build_editor_with_project(
|
||||
// project: Model<Project>,
|
||||
// buffer: Model<MultiBuffer>,
|
||||
// cx: &mut ViewContext<Editor>,
|
||||
// ) -> Editor {
|
||||
// Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
|
||||
// }
|
||||
pub(crate) fn build_editor_with_project(
|
||||
project: Model<Project>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Editor {
|
||||
// todo!()
|
||||
Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
|
||||
}
|
||||
|
@ -1,297 +1,298 @@
|
||||
// use std::{
|
||||
// borrow::Cow,
|
||||
// ops::{Deref, DerefMut, Range},
|
||||
// sync::Arc,
|
||||
// };
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
// use anyhow::Result;
|
||||
use anyhow::Result;
|
||||
use serde_json::json;
|
||||
|
||||
// use crate::{Editor, ToPoint};
|
||||
// use collections::HashSet;
|
||||
// use futures::Future;
|
||||
// use gpui::{json, View, ViewContext};
|
||||
// use indoc::indoc;
|
||||
// use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
|
||||
// use lsp::{notification, request};
|
||||
// use multi_buffer::ToPointUtf16;
|
||||
// use project::Project;
|
||||
// use smol::stream::StreamExt;
|
||||
// use workspace::{AppState, Workspace, WorkspaceHandle};
|
||||
use crate::{Editor, ToPoint};
|
||||
use collections::HashSet;
|
||||
use futures::Future;
|
||||
use gpui::{View, ViewContext, VisualTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
|
||||
use lsp::{notification, request};
|
||||
use multi_buffer::ToPointUtf16;
|
||||
use project::Project;
|
||||
use smol::stream::StreamExt;
|
||||
use workspace::{AppState, Workspace, WorkspaceHandle};
|
||||
|
||||
// use super::editor_test_context::EditorTestContext;
|
||||
use super::editor_test_context::{AssertionContextManager, EditorTestContext};
|
||||
|
||||
// pub struct EditorLspTestContext<'a> {
|
||||
// pub cx: EditorTestContext<'a>,
|
||||
// pub lsp: lsp::FakeLanguageServer,
|
||||
// pub workspace: View<Workspace>,
|
||||
// pub buffer_lsp_url: lsp::Url,
|
||||
// }
|
||||
pub struct EditorLspTestContext<'a> {
|
||||
pub cx: EditorTestContext<'a>,
|
||||
pub lsp: lsp::FakeLanguageServer,
|
||||
pub workspace: View<Workspace>,
|
||||
pub buffer_lsp_url: lsp::Url,
|
||||
}
|
||||
|
||||
// impl<'a> EditorLspTestContext<'a> {
|
||||
// pub async fn new(
|
||||
// mut language: Language,
|
||||
// capabilities: lsp::ServerCapabilities,
|
||||
// cx: &'a mut gpui::TestAppContext,
|
||||
// ) -> EditorLspTestContext<'a> {
|
||||
// use json::json;
|
||||
impl<'a> EditorLspTestContext<'a> {
|
||||
pub async fn new(
|
||||
mut language: Language,
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
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| {
|
||||
// language::init(cx);
|
||||
// crate::init(cx);
|
||||
// workspace::init(app_state.clone(), cx);
|
||||
// Project::init_settings(cx);
|
||||
// });
|
||||
let file_name = format!(
|
||||
"file.{}",
|
||||
language
|
||||
.path_suffixes()
|
||||
.first()
|
||||
.expect("language must have a path suffix for EditorLspTestContext")
|
||||
);
|
||||
|
||||
// let file_name = format!(
|
||||
// "file.{}",
|
||||
// language
|
||||
// .path_suffixes()
|
||||
// .first()
|
||||
// .expect("language must have a path suffix for EditorLspTestContext")
|
||||
// );
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities,
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
// let mut fake_servers = language
|
||||
// .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
// capabilities,
|
||||
// ..Default::default()
|
||||
// }))
|
||||
// .await;
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).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
|
||||
// .fs
|
||||
// .as_fake()
|
||||
// .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||
// .await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
|
||||
.await;
|
||||
|
||||
// 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 window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
// let item = workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// workspace.open_path(file, None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .expect("Could not open test file");
|
||||
let workspace = window.root_view(cx).unwrap();
|
||||
|
||||
// let editor = cx.update(|cx| {
|
||||
// item.act_as::<Editor>(cx)
|
||||
// .expect("Opened test file wasn't an editor")
|
||||
// });
|
||||
// editor.update(cx, |_, cx| cx.focus_self());
|
||||
let mut cx = VisualTestContext::from_window(*window.deref(), cx);
|
||||
project
|
||||
.update(&mut 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 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 {
|
||||
// cx: EditorTestContext {
|
||||
// cx,
|
||||
// window: window.into(),
|
||||
// editor,
|
||||
// },
|
||||
// lsp,
|
||||
// workspace,
|
||||
// buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
|
||||
// }
|
||||
// }
|
||||
pub async fn new_rust(
|
||||
capabilities: lsp::ServerCapabilities,
|
||||
cx: &'a mut gpui::TestAppContext,
|
||||
) -> EditorLspTestContext<'a> {
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..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(
|
||||
// capabilities: lsp::ServerCapabilities,
|
||||
// cx: &'a mut gpui::TestAppContext,
|
||||
// ) -> EditorLspTestContext<'a> {
|
||||
// let language = Language::new(
|
||||
// LanguageConfig {
|
||||
// name: "Rust".into(),
|
||||
// path_suffixes: vec!["rs".to_string()],
|
||||
// ..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
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @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");
|
||||
|
||||
// (_ "[" "]" @end) @indent
|
||||
// (_ "<" ">" @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
|
||||
}
|
||||
|
||||
// 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(
|
||||
// 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
|
||||
(_ "[" "]" @end) @indent
|
||||
(_ "<" ">" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
"#})),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("Could not parse queries");
|
||||
|
||||
// (_ "[" "]" @end) @indent
|
||||
// (_ "<" ">" @end) @indent
|
||||
// (_ "{" "}" @end) @indent
|
||||
// (_ "(" ")" @end) @indent
|
||||
// "#})),
|
||||
// ..Default::default()
|
||||
// })
|
||||
// .expect("Could not parse queries");
|
||||
Self::new(language, capabilities, cx).await
|
||||
}
|
||||
|
||||
// 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 lsp_range(&mut self, marked_text: &str) -> lsp::Range {
|
||||
// let ranges = self.ranges(marked_text);
|
||||
// self.to_lsp_range(ranges[0].clone())
|
||||
// }
|
||||
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
|
||||
// pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
|
||||
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
// let start_point = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
// let end_point = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
self.editor(|editor, cx| {
|
||||
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)),
|
||||
);
|
||||
|
||||
// self.editor(|editor, cx| {
|
||||
// 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 }
|
||||
})
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
|
||||
// let point = offset.to_point(&snapshot.buffer_snapshot);
|
||||
self.editor(|editor, cx| {
|
||||
let buffer = editor.buffer().read(cx);
|
||||
point_to_lsp(
|
||||
buffer
|
||||
.point_to_buffer_offset(point, cx)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_point_utf16(&buffer.read(cx)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// self.editor(|editor, cx| {
|
||||
// let buffer = editor.buffer().read(cx);
|
||||
// point_to_lsp(
|
||||
// buffer
|
||||
// .point_to_buffer_offset(point, cx)
|
||||
// .unwrap()
|
||||
// .1
|
||||
// .to_point_utf16(&buffer.read(cx)),
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
{
|
||||
self.workspace.update(&mut self.cx.cx, update)
|
||||
}
|
||||
|
||||
// pub fn update_workspace<F, T>(&mut self, update: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
|
||||
// {
|
||||
// self.workspace.update(self.cx.cx, update)
|
||||
// }
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
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 handle_request<T, F, Fut>(
|
||||
// &self,
|
||||
// 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) {
|
||||
self.lsp.notify::<T>(params);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn notify<T: notification::Notification>(&self, params: T::Params) {
|
||||
// self.lsp.notify::<T>(params);
|
||||
// }
|
||||
// }
|
||||
impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
type Target = EditorTestContext<'a>;
|
||||
|
||||
// impl<'a> Deref for EditorLspTestContext<'a> {
|
||||
// type Target = EditorTestContext<'a>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cx
|
||||
}
|
||||
}
|
||||
|
||||
// fn deref(&self) -> &Self::Target {
|
||||
// &self.cx
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<'a> DerefMut for EditorLspTestContext<'a> {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
@ -1,331 +1,400 @@
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use futures::Future;
|
||||
use gpui::{
|
||||
AnyWindowHandle, AppContext, ForegroundExecutor, Keystroke, ModelContext, View, ViewContext,
|
||||
VisualTestContext, WindowHandle,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, BufferSnapshot};
|
||||
use parking_lot::RwLock;
|
||||
use project::{FakeFs, Project};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use util::{
|
||||
assert_set_eq,
|
||||
test::{generate_marked_text, marked_text_ranges},
|
||||
};
|
||||
|
||||
// use super::build_editor_with_project;
|
||||
use super::build_editor_with_project;
|
||||
|
||||
// pub struct EditorTestContext<'a> {
|
||||
// pub cx: &'a mut gpui::TestAppContext,
|
||||
// pub window: AnyWindowHandle,
|
||||
// pub editor: View<Editor>,
|
||||
// }
|
||||
pub struct EditorTestContext<'a> {
|
||||
pub cx: gpui::VisualTestContext<'a>,
|
||||
pub window: AnyWindowHandle,
|
||||
pub editor: View<Editor>,
|
||||
pub assertion_cx: AssertionContextManager,
|
||||
}
|
||||
|
||||
// impl<'a> EditorTestContext<'a> {
|
||||
// pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
// let fs = FakeFs::new(cx.background());
|
||||
// // fs.insert_file("/file", "".to_owned()).await;
|
||||
// fs.insert_tree(
|
||||
// "/root",
|
||||
// gpui::serde_json::json!({
|
||||
// "file": "",
|
||||
// }),
|
||||
// )
|
||||
// .await;
|
||||
// let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
||||
// let buffer = project
|
||||
// .update(cx, |project, cx| {
|
||||
// project.open_local_buffer("/root/file", cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let window = cx.add_window(|cx| {
|
||||
// cx.focus_self();
|
||||
// build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
|
||||
// });
|
||||
// let editor = window.root(cx);
|
||||
// Self {
|
||||
// cx,
|
||||
// window: window.into(),
|
||||
// editor,
|
||||
// }
|
||||
// }
|
||||
impl<'a> EditorTestContext<'a> {
|
||||
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
// fs.insert_file("/file", "".to_owned()).await;
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
gpui::serde_json::json!({
|
||||
"file": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/root".as_ref()], cx).await;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/root/file", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let editor = cx.add_window(|cx| {
|
||||
let editor =
|
||||
build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx);
|
||||
editor.focus(cx);
|
||||
editor
|
||||
});
|
||||
let editor_view = editor.root_view(cx).unwrap();
|
||||
Self {
|
||||
cx: VisualTestContext::from_window(*editor.deref(), cx),
|
||||
window: editor.into(),
|
||||
editor: editor_view,
|
||||
assertion_cx: AssertionContextManager::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn condition(
|
||||
// &self,
|
||||
// predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
// ) -> impl Future<Output = ()> {
|
||||
// self.editor.condition(self.cx, predicate)
|
||||
// }
|
||||
pub fn condition(
|
||||
&self,
|
||||
predicate: impl FnMut(&Editor, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.editor.condition::<crate::Event>(&self.cx, predicate)
|
||||
}
|
||||
|
||||
// pub fn editor<F, T>(&self, read: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
|
||||
// {
|
||||
// self.editor.update(self.cx, read)
|
||||
// }
|
||||
#[track_caller]
|
||||
pub fn editor<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
|
||||
{
|
||||
self.editor
|
||||
.update(&mut self.cx, |this, cx| read(&this, &cx))
|
||||
}
|
||||
|
||||
// pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||
// {
|
||||
// self.editor.update(self.cx, update)
|
||||
// }
|
||||
#[track_caller]
|
||||
pub fn update_editor<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
|
||||
{
|
||||
self.editor.update(&mut self.cx, update)
|
||||
}
|
||||
|
||||
// pub fn multibuffer<F, T>(&self, read: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||
// {
|
||||
// self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||
// }
|
||||
pub fn multibuffer<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&MultiBuffer, &AppContext) -> T,
|
||||
{
|
||||
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
|
||||
}
|
||||
|
||||
// pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||
// {
|
||||
// self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||
// }
|
||||
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
|
||||
{
|
||||
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
|
||||
}
|
||||
|
||||
// pub fn buffer_text(&self) -> String {
|
||||
// self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||
// }
|
||||
pub fn buffer_text(&mut self) -> String {
|
||||
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
|
||||
}
|
||||
|
||||
// pub fn buffer<F, T>(&self, read: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&Buffer, &AppContext) -> T,
|
||||
// {
|
||||
// self.multibuffer(|multibuffer, cx| {
|
||||
// let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||
// read(buffer, cx)
|
||||
// })
|
||||
// }
|
||||
pub fn buffer<F, T>(&mut self, read: F) -> T
|
||||
where
|
||||
F: FnOnce(&Buffer, &AppContext) -> T,
|
||||
{
|
||||
self.multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap().read(cx);
|
||||
read(buffer, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||
// where
|
||||
// F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||
// {
|
||||
// self.update_multibuffer(|multibuffer, cx| {
|
||||
// let buffer = multibuffer.as_singleton().unwrap();
|
||||
// buffer.update(cx, update)
|
||||
// })
|
||||
// }
|
||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||
{
|
||||
self.update_multibuffer(|multibuffer, cx| {
|
||||
let buffer = multibuffer.as_singleton().unwrap();
|
||||
buffer.update(cx, update)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn buffer_snapshot(&self) -> BufferSnapshot {
|
||||
// self.buffer(|buffer, _| buffer.snapshot())
|
||||
// }
|
||||
pub fn buffer_snapshot(&mut self) -> BufferSnapshot {
|
||||
self.buffer(|buffer, _| buffer.snapshot())
|
||||
}
|
||||
|
||||
// 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 add_assertion_context(&self, context: String) -> ContextHandle {
|
||||
self.assertion_cx.add_context(context)
|
||||
}
|
||||
|
||||
// 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>(
|
||||
// &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!();
|
||||
// }
|
||||
self.cx.dispatch_keystroke(self.window, keystroke, false);
|
||||
|
||||
// keystrokes_under_test_handle
|
||||
// }
|
||||
keystroke_under_test_handle
|
||||
}
|
||||
|
||||
// pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
// let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
// assert_eq!(self.buffer_text(), unmarked_text);
|
||||
// ranges
|
||||
// }
|
||||
pub fn simulate_keystrokes<const COUNT: usize>(
|
||||
&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.
|
||||
self.cx.background_executor.run_until_parked();
|
||||
|
||||
// pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||
// 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)
|
||||
// }
|
||||
keystrokes_under_test_handle
|
||||
}
|
||||
|
||||
// // Returns anchors for the current buffer using `«` and `»`
|
||||
// pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
|
||||
// let ranges = self.ranges(marked_text);
|
||||
// let snapshot = self.buffer_snapshot();
|
||||
// snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||
// }
|
||||
pub fn ranges(&mut self, marked_text: &str) -> Vec<Range<usize>> {
|
||||
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
|
||||
assert_eq!(self.buffer_text(), unmarked_text);
|
||||
ranges
|
||||
}
|
||||
|
||||
// pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
|
||||
// let diff_base = diff_base.map(String::from);
|
||||
// self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
|
||||
// }
|
||||
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
|
||||
let ranges = self.ranges(marked_text);
|
||||
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
|
||||
// /// embedded range markers that represent the ranges and directions of
|
||||
// /// each selection.
|
||||
// ///
|
||||
// /// Returns a context handle so that assertion failures can print what
|
||||
// /// 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
|
||||
// }
|
||||
// Returns anchors for the current buffer using `«` and `»`
|
||||
pub fn text_anchor_range(&mut self, marked_text: &str) -> Range<language::Anchor> {
|
||||
let ranges = self.ranges(marked_text);
|
||||
let snapshot = self.buffer_snapshot();
|
||||
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
|
||||
}
|
||||
|
||||
// /// Only change the editor's selections
|
||||
// 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(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
|
||||
// }
|
||||
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
|
||||
let diff_base = diff_base.map(String::from);
|
||||
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
|
||||
}
|
||||
|
||||
// /// 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();
|
||||
/// Change the editor's text and selections using a string containing
|
||||
/// embedded range markers that represent the ranges and directions of
|
||||
/// each selection.
|
||||
///
|
||||
/// Returns a context handle so that assertion failures can print what
|
||||
/// 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(&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 {
|
||||
// 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}");
|
||||
// }
|
||||
/// Only change the editor's selections
|
||||
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 {
|
||||
// generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
|
||||
// }
|
||||
if buffer_text != unmarked_text {
|
||||
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]
|
||||
// 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);
|
||||
// }
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
// #[track_caller]
|
||||
// pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||
// 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);
|
||||
// }
|
||||
pub fn editor_state(&mut self) -> String {
|
||||
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
|
||||
}
|
||||
|
||||
// #[track_caller]
|
||||
// pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||
// let expected_marked_text =
|
||||
// generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||
// self.assert_selections(expected_selections, expected_marked_text)
|
||||
// }
|
||||
#[track_caller]
|
||||
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);
|
||||
}
|
||||
|
||||
// fn editor_selections(&self) -> Vec<Range<usize>> {
|
||||
// self.editor
|
||||
// .read_with(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<_>>()
|
||||
// }
|
||||
#[track_caller]
|
||||
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
|
||||
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]
|
||||
// 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! {"
|
||||
#[track_caller]
|
||||
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
|
||||
let expected_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &expected_selections, true);
|
||||
self.assert_selections(expected_selections, expected_marked_text)
|
||||
}
|
||||
|
||||
// {}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:
|
||||
// {}
|
||||
// "},
|
||||
// self.assertion_context(),
|
||||
// expected_marked_text,
|
||||
// actual_marked_text,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl<'a> Deref for EditorTestContext<'a> {
|
||||
// type Target = gpui::TestAppContext;
|
||||
{}Editor has unexpected selections.
|
||||
|
||||
// fn deref(&self) -> &Self::Target {
|
||||
// self.cx
|
||||
// }
|
||||
// }
|
||||
Expected selections:
|
||||
{}
|
||||
|
||||
// impl<'a> DerefMut for EditorTestContext<'a> {
|
||||
// fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
// &mut self.cx
|
||||
// }
|
||||
// }
|
||||
Actual 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 {
|
||||
&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);
|
||||
}
|
||||
}
|
||||
|
37
crates/file_finder2/Cargo.toml
Normal file
37
crates/file_finder2/Cargo.toml
Normal 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
|
1973
crates/file_finder2/src/file_finder.rs
Normal file
1973
crates/file_finder2/src/file_finder.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,11 @@
|
||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString,
|
||||
StatefulInteractivity, StatelessInteractive, Styled, Subscription, View, ViewContext,
|
||||
VisualContext, WindowContext,
|
||||
actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
|
||||
Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use text::{Bias, Point};
|
||||
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 workspace::{Modal, ModalEvent, Workspace};
|
||||
|
||||
@ -146,11 +145,12 @@ impl 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 {
|
||||
modal(cx)
|
||||
.id("go to line")
|
||||
div()
|
||||
.elevation_2(cx)
|
||||
.key_context("GoToLine")
|
||||
.on_action(Self::cancel)
|
||||
.on_action(Self::confirm)
|
||||
.w_96()
|
||||
@ -176,7 +176,7 @@ impl Render for GoToLine {
|
||||
.justify_between()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(Label::new(self.current_text.clone()).color(LabelColor::Muted)),
|
||||
.child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -2112,6 +2112,10 @@ impl AppContext {
|
||||
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) {
|
||||
self.platform.write_to_clipboard(item);
|
||||
}
|
||||
|
1
crates/gpui/src/dispatch.rs
Normal file
1
crates/gpui/src/dispatch.rs
Normal file
@ -0,0 +1 @@
|
||||
|
41
crates/gpui2/docs/contexts.md
Normal file
41
crates/gpui2/docs/contexts.md
Normal 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.
|
101
crates/gpui2/docs/key_dispatch.md
Normal file
101
crates/gpui2/docs/key_dispatch.md
Normal 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}]
|
||||
}
|
||||
}
|
||||
|
||||
```
|
@ -1,6 +1,6 @@
|
||||
use crate::SharedString;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{HashMap, HashSet};
|
||||
use collections::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
|
||||
use serde::Deserialize;
|
||||
@ -68,8 +68,12 @@ where
|
||||
A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
|
||||
{
|
||||
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
|
||||
type_name::<A>().replace("2::", "::").into()
|
||||
name[name_start_ix..].replace("2::", "::").into()
|
||||
}
|
||||
|
||||
fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
|
||||
@ -176,8 +180,7 @@ macro_rules! actions {
|
||||
() => {};
|
||||
|
||||
( $name:ident ) => {
|
||||
#[gpui::register_action]
|
||||
#[derive(::std::clone::Clone, ::std::default::Default, ::std::fmt::Debug, ::std::cmp::PartialEq, $crate::serde::Deserialize)]
|
||||
#[gpui::action]
|
||||
pub struct $name;
|
||||
};
|
||||
|
||||
@ -186,401 +189,3 @@ macro_rules! actions {
|
||||
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())),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -234,10 +234,10 @@ impl AppContext {
|
||||
app_version: platform.app_version().ok(),
|
||||
};
|
||||
|
||||
Rc::new_cyclic(|this| AppCell {
|
||||
let app = Rc::new_cyclic(|this| AppCell {
|
||||
app: RefCell::new(AppContext {
|
||||
this: this.clone(),
|
||||
platform,
|
||||
platform: platform.clone(),
|
||||
app_metadata,
|
||||
text_system,
|
||||
flushing_effects: false,
|
||||
@ -269,12 +269,21 @@ impl AppContext {
|
||||
layout_id_buffer: Default::default(),
|
||||
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`
|
||||
/// will be given 100ms to complete before exiting.
|
||||
pub fn quit(&mut self) {
|
||||
pub fn shutdown(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
|
||||
for observer in self.quit_observers.remove(&()) {
|
||||
@ -292,8 +301,10 @@ impl AppContext {
|
||||
{
|
||||
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 {
|
||||
@ -431,6 +442,18 @@ impl AppContext {
|
||||
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.
|
||||
pub fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
|
||||
self.platform.displays()
|
||||
@ -641,14 +664,19 @@ impl AppContext {
|
||||
// The window might change focus multiple times in an effect cycle.
|
||||
// We only honor effects for the most recently focused handle.
|
||||
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
|
||||
.map(|id| FocusHandle::for_id(id, &cx.window.focus_handles).unwrap());
|
||||
let blurred = cx
|
||||
.window
|
||||
.last_blur
|
||||
.take()
|
||||
.unwrap()
|
||||
.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
|
||||
|
||||
let blurred =
|
||||
last_blur.and_then(|id| FocusHandle::for_id(id, &cx.window.focus_handles));
|
||||
|
||||
let focus_changed = focused.is_some() || blurred.is_some();
|
||||
let event = FocusEvent { focused, blurred };
|
||||
|
||||
@ -1007,6 +1035,29 @@ impl Context for AppContext {
|
||||
let entity = self.entities.read(handle);
|
||||
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.
|
||||
@ -1063,7 +1114,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
|
||||
|
||||
/// 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.
|
||||
pub(crate) struct AnyDrag {
|
||||
pub struct AnyDrag {
|
||||
pub view: AnyView,
|
||||
pub cursor_offset: Point<Pixels>,
|
||||
}
|
||||
|
@ -66,6 +66,19 @@ impl Context for AsyncAppContext {
|
||||
let mut lock = app.borrow_mut();
|
||||
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 {
|
||||
@ -250,6 +263,17 @@ impl Context for AsyncWindowContext {
|
||||
{
|
||||
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 {
|
||||
|
@ -26,7 +26,7 @@ impl EntityId {
|
||||
|
||||
impl Display for EntityId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self)
|
||||
write!(f, "{}", self.as_u64())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
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 derive_more::{Deref, DerefMut};
|
||||
@ -239,6 +239,17 @@ impl<'a, T> Context for ModelContext<'a, T> {
|
||||
{
|
||||
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> {
|
||||
|
@ -1,12 +1,12 @@
|
||||
use crate::{
|
||||
AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, Context,
|
||||
EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext,
|
||||
Render, Result, Task, TestDispatcher, TestPlatform, ViewContext, VisualContext, WindowContext,
|
||||
WindowHandle, WindowOptions,
|
||||
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
|
||||
BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
|
||||
Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
|
||||
ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
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)]
|
||||
pub struct TestAppContext {
|
||||
@ -14,6 +14,7 @@ pub struct TestAppContext {
|
||||
pub background_executor: BackgroundExecutor,
|
||||
pub foreground_executor: ForegroundExecutor,
|
||||
pub dispatcher: TestDispatcher,
|
||||
pub test_platform: Rc<TestPlatform>,
|
||||
}
|
||||
|
||||
impl Context for TestAppContext {
|
||||
@ -58,6 +59,18 @@ impl Context for TestAppContext {
|
||||
let app = self.app.borrow();
|
||||
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 {
|
||||
@ -65,17 +78,16 @@ impl TestAppContext {
|
||||
let arc_dispatcher = Arc::new(dispatcher.clone());
|
||||
let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
|
||||
let platform = Rc::new(TestPlatform::new(
|
||||
background_executor.clone(),
|
||||
foreground_executor.clone(),
|
||||
));
|
||||
let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
|
||||
let asset_source = Arc::new(());
|
||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||
|
||||
Self {
|
||||
app: AppContext::new(platform, asset_source, http_client),
|
||||
app: AppContext::new(platform.clone(), asset_source, http_client),
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
dispatcher: dispatcher.clone(),
|
||||
test_platform: platform,
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +96,7 @@ impl TestAppContext {
|
||||
}
|
||||
|
||||
pub fn quit(&self) {
|
||||
self.app.borrow_mut().quit();
|
||||
self.app.borrow_mut().shutdown();
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self) -> Result<()> {
|
||||
@ -93,8 +105,8 @@ impl TestAppContext {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn executor(&self) -> &BackgroundExecutor {
|
||||
&self.background_executor
|
||||
pub fn executor(&self) -> BackgroundExecutor {
|
||||
self.background_executor.clone()
|
||||
}
|
||||
|
||||
pub fn foreground_executor(&self) -> &ForegroundExecutor {
|
||||
@ -120,6 +132,41 @@ impl TestAppContext {
|
||||
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>
|
||||
where
|
||||
Fut: Future<Output = R> + 'static,
|
||||
@ -146,6 +193,11 @@ impl TestAppContext {
|
||||
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>(
|
||||
&mut self,
|
||||
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(
|
||||
&mut self,
|
||||
window: AnyWindowHandle,
|
||||
@ -259,3 +320,198 @@ impl<T: Send> Model<T> {
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
@ -167,7 +167,7 @@ impl TryFrom<&'_ str> for Rgba {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Default, Copy, Clone, Debug)]
|
||||
#[repr(C)]
|
||||
pub struct Hsla {
|
||||
pub h: f32,
|
||||
@ -176,10 +176,63 @@ pub struct Hsla {
|
||||
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 {
|
||||
pub fn to_rgb(self) -> Rgba {
|
||||
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 {}
|
||||
@ -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 {
|
||||
/// Returns true if the HSLA color is fully transparent, false otherwise.
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
|
@ -8,7 +8,7 @@ use std::{any::Any, mem};
|
||||
pub trait Element<V: '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
|
||||
/// 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)]
|
||||
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 child(mut self, child: impl Component<V>) -> Self
|
||||
@ -120,7 +120,7 @@ where
|
||||
E::ElementState: 'static,
|
||||
{
|
||||
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| {
|
||||
let element_state = self.element.initialize(view_state, element_state, cx);
|
||||
((), element_state)
|
||||
@ -142,7 +142,7 @@ where
|
||||
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| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
let layout_id = self.element.layout(state, &mut element_state, cx);
|
||||
@ -181,7 +181,7 @@ where
|
||||
..
|
||||
} => {
|
||||
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| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
self.element
|
||||
@ -255,7 +255,7 @@ where
|
||||
// Ignore the element offset when drawing this element, as the origin is already specified
|
||||
// in absolute terms.
|
||||
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>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,35 +1,28 @@
|
||||
use crate::{
|
||||
div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementFocus,
|
||||
ElementId, ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable,
|
||||
LayoutId, Pixels, SharedString, StatefulInteractive, StatefulInteractivity,
|
||||
StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Img<
|
||||
V: 'static,
|
||||
I: ElementInteractivity<V> = StatelessInteractivity<V>,
|
||||
F: ElementFocus<V> = FocusDisabled,
|
||||
> {
|
||||
base: Div<V, I, F>,
|
||||
pub struct Img<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
uri: Option<SharedString>,
|
||||
grayscale: bool,
|
||||
}
|
||||
|
||||
pub fn img<V: 'static>() -> Img<V, StatelessInteractivity<V>, FocusDisabled> {
|
||||
pub fn img<V: 'static>() -> Img<V> {
|
||||
Img {
|
||||
base: div(),
|
||||
interactivity: Interactivity::default(),
|
||||
uri: None,
|
||||
grayscale: false,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, I, F> Img<V, I, F>
|
||||
impl<V> Img<V>
|
||||
where
|
||||
V: 'static,
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
|
||||
self.uri = Some(uri.into());
|
||||
@ -42,145 +35,90 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, F> Img<V, StatelessInteractivity<V>, F>
|
||||
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>,
|
||||
{
|
||||
impl<V> Component<V> for Img<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, I, F> Element<V> for Img<V, I, F>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
type ElementState = DivState;
|
||||
impl<V> Element<V> for Img<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
self.base.id()
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.base.initialize(view_state, element_state, cx)
|
||||
self.interactivity.initialize(element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
self.base.layout(view_state, element_state, cx)
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
cx.with_z_index(0, |cx| {
|
||||
self.base.paint(bounds, view, element_state, cx);
|
||||
});
|
||||
self.interactivity.paint(
|
||||
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);
|
||||
let corner_radii = style.corner_radii;
|
||||
|
||||
if let Some(uri) = self.uri.clone() {
|
||||
// eprintln!(">>> image_cache.get({uri}");
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
// eprintln!("<<< image_cache.get({uri}");
|
||||
if let Some(data) = image_future
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.and_then(ResultExt::log_err)
|
||||
{
|
||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
||||
.log_err()
|
||||
});
|
||||
} else {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if image_future.await.log_err().is_some() {
|
||||
cx.on_next_frame(|cx| cx.notify());
|
||||
if let Some(uri) = self.uri.clone() {
|
||||
// eprintln!(">>> image_cache.get({uri}");
|
||||
let image_future = cx.image_cache.get(uri.clone());
|
||||
// eprintln!("<<< image_cache.get({uri}");
|
||||
if let Some(data) = image_future
|
||||
.clone()
|
||||
.now_or_never()
|
||||
.and_then(ResultExt::log_err)
|
||||
{
|
||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||
cx.with_z_index(1, |cx| {
|
||||
cx.paint_image(bounds, corner_radii, data, self.grayscale)
|
||||
.log_err()
|
||||
});
|
||||
} else {
|
||||
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>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
impl<V> Styled for Img<V> {
|
||||
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>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
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)
|
||||
impl<V> InteractiveComponent<V> for Img<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -1,157 +1,88 @@
|
||||
use crate::{
|
||||
div, AnyElement, Bounds, Component, Div, DivState, Element, ElementFocus, ElementId,
|
||||
ElementInteractivity, FocusDisabled, FocusEnabled, FocusListeners, Focusable, LayoutId, Pixels,
|
||||
SharedString, StatefulInteractive, StatefulInteractivity, StatelessInteractive,
|
||||
StatelessInteractivity, StyleRefinement, Styled, ViewContext,
|
||||
AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
|
||||
InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
|
||||
Styled, ViewContext,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct Svg<
|
||||
V: 'static,
|
||||
I: ElementInteractivity<V> = StatelessInteractivity<V>,
|
||||
F: ElementFocus<V> = FocusDisabled,
|
||||
> {
|
||||
base: Div<V, I, F>,
|
||||
pub struct Svg<V: 'static> {
|
||||
interactivity: Interactivity<V>,
|
||||
path: Option<SharedString>,
|
||||
}
|
||||
|
||||
pub fn svg<V: 'static>() -> Svg<V, StatelessInteractivity<V>, FocusDisabled> {
|
||||
pub fn svg<V: 'static>() -> Svg<V> {
|
||||
Svg {
|
||||
base: div(),
|
||||
interactivity: Interactivity::default(),
|
||||
path: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, I, F> Svg<V, I, F>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
impl<V> Svg<V> {
|
||||
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
|
||||
self.path = Some(path.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, F> Svg<V, StatelessInteractivity<V>, F>
|
||||
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>,
|
||||
{
|
||||
impl<V> Component<V> for Svg<V> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, I, F> Element<V> for Svg<V, I, F>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
type ElementState = DivState;
|
||||
impl<V> Element<V> for Svg<V> {
|
||||
type ElementState = InteractiveElementState;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
self.base.id()
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
self.interactivity.element_id.clone()
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> Self::ElementState {
|
||||
self.base.initialize(view_state, element_state, cx)
|
||||
self.interactivity.initialize(element_state, cx)
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
view_state: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> LayoutId {
|
||||
self.base.layout(view_state, element_state, cx)
|
||||
self.interactivity.layout(element_state, cx, |style, cx| {
|
||||
cx.request_layout(&style, None)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: Bounds<Pixels>,
|
||||
view: &mut V,
|
||||
_view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
cx: &mut ViewContext<V>,
|
||||
) where
|
||||
Self: Sized,
|
||||
{
|
||||
self.base.paint(bounds, view, element_state, cx);
|
||||
let color = self
|
||||
.base
|
||||
.compute_style(bounds, element_state, cx)
|
||||
.text
|
||||
.color;
|
||||
if let Some((path, color)) = self.path.as_ref().zip(color) {
|
||||
cx.paint_svg(bounds, path.clone(), color).log_err();
|
||||
}
|
||||
self.interactivity
|
||||
.paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
|
||||
if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
|
||||
cx.paint_svg(bounds, path.clone(), color).log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<V, I, F> Styled for Svg<V, I, F>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
F: ElementFocus<V>,
|
||||
{
|
||||
impl<V> Styled for Svg<V> {
|
||||
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>
|
||||
where
|
||||
I: ElementInteractivity<V>,
|
||||
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)
|
||||
impl<V> InteractiveComponent<V> for Svg<V> {
|
||||
fn interactivity(&mut self) -> &mut Interactivity<V> {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
|
||||
Size, ViewContext,
|
||||
Size, TextRun, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
@ -11,6 +11,7 @@ impl<V: 'static> Component<V> for SharedString {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self,
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
@ -21,6 +22,7 @@ impl<V: 'static> Component<V> for &'static str {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
@ -33,6 +35,7 @@ impl<V: 'static> Component<V> for String {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
Text {
|
||||
text: self.into(),
|
||||
runs: None,
|
||||
state_type: PhantomData,
|
||||
}
|
||||
.render()
|
||||
@ -41,9 +44,25 @@ impl<V: 'static> Component<V> for String {
|
||||
|
||||
pub struct Text<V> {
|
||||
text: SharedString,
|
||||
runs: Option<Vec<TextRun>>,
|
||||
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> {
|
||||
fn render(self) -> AnyElement<V> {
|
||||
AnyElement::new(self)
|
||||
@ -53,7 +72,7 @@ impl<V: 'static> Component<V> for Text<V> {
|
||||
impl<V: 'static> Element<V> for Text<V> {
|
||||
type ElementState = Arc<Mutex<Option<TextElementState>>>;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
@ -81,6 +100,13 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||
let text = self.text.clone();
|
||||
|
||||
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 element_state = element_state.clone();
|
||||
move |known_dimensions, _| {
|
||||
@ -88,11 +114,15 @@ impl<V: 'static> Element<V> for Text<V> {
|
||||
.layout_text(
|
||||
&text,
|
||||
font_size,
|
||||
&[text_style.to_run(text.len())],
|
||||
&runs[..],
|
||||
known_dimensions.width, // Wrap if we know the width.
|
||||
)
|
||||
.log_err()
|
||||
else {
|
||||
element_state.lock().replace(TextElementState {
|
||||
lines: Default::default(),
|
||||
line_height,
|
||||
});
|
||||
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
|
||||
.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 mut line_origin = bounds.origin;
|
||||
|
@ -1,24 +1,23 @@
|
||||
use crate::{
|
||||
point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
|
||||
ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
|
||||
StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity,
|
||||
StyleRefinement, Styled, ViewContext,
|
||||
ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
|
||||
Point, Size, StyleRefinement, Styled, ViewContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp, ops::Range, sync::Arc};
|
||||
use std::{cmp, mem, ops::Range, sync::Arc};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
/// 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,
|
||||
/// uniform_list will only render the visibile subset of items.
|
||||
pub fn uniform_list<Id, V, C>(
|
||||
id: Id,
|
||||
pub fn uniform_list<I, V, C>(
|
||||
id: I,
|
||||
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>
|
||||
where
|
||||
Id: Into<ElementId>,
|
||||
I: Into<ElementId>,
|
||||
V: 'static,
|
||||
C: Component<V>,
|
||||
{
|
||||
@ -37,7 +36,10 @@ where
|
||||
.map(|component| component.render())
|
||||
.collect()
|
||||
}),
|
||||
interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()),
|
||||
interactivity: Interactivity {
|
||||
element_id: Some(id.into()),
|
||||
..Default::default()
|
||||
},
|
||||
scroll_handle: None,
|
||||
}
|
||||
}
|
||||
@ -54,7 +56,7 @@ pub struct UniformList<V: 'static> {
|
||||
&'a mut ViewContext<V>,
|
||||
) -> SmallVec<[AnyElement<V>; 64]>,
|
||||
>,
|
||||
interactivity: StatefulInteractivity<V>,
|
||||
interactivity: Interactivity<V>,
|
||||
scroll_handle: Option<UniformListScrollHandle>,
|
||||
}
|
||||
|
||||
@ -103,7 +105,7 @@ pub struct UniformListState {
|
||||
impl<V: 'static> Element<V> for UniformList<V> {
|
||||
type ElementState = UniformListState;
|
||||
|
||||
fn id(&self) -> Option<crate::ElementId> {
|
||||
fn element_id(&self) -> Option<crate::ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
@ -113,13 +115,18 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
element_state: Option<Self::ElementState>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> 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);
|
||||
UniformListState {
|
||||
interactive: InteractiveElementState::default(),
|
||||
interactive: self.interactivity.initialize(None, cx),
|
||||
item_size,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
@ -132,35 +139,44 @@ impl<V: 'static> Element<V> for UniformList<V> {
|
||||
let item_size = element_state.item_size;
|
||||
let rem_size = cx.rem_size();
|
||||
|
||||
cx.request_measured_layout(
|
||||
self.computed_style(),
|
||||
rem_size,
|
||||
move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
self.interactivity
|
||||
.layout(&mut element_state.interactive, cx, |style, cx| {
|
||||
cx.request_measured_layout(
|
||||
style,
|
||||
rem_size,
|
||||
move |known_dimensions: Size<Option<Pixels>>,
|
||||
available_space: Size<AvailableSpace>| {
|
||||
let desired_height = item_size.height * max_items;
|
||||
let width = known_dimensions
|
||||
.width
|
||||
.unwrap_or(match available_space.width {
|
||||
AvailableSpace::Definite(x) => x,
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
item_size.width
|
||||
}
|
||||
});
|
||||
let height = match available_space.height {
|
||||
AvailableSpace::Definite(x) => desired_height.min(x),
|
||||
AvailableSpace::MinContent | AvailableSpace::MaxContent => {
|
||||
desired_height
|
||||
}
|
||||
};
|
||||
size(width, height)
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: crate::Bounds<crate::Pixels>,
|
||||
bounds: Bounds<crate::Pixels>,
|
||||
view_state: &mut V,
|
||||
element_state: &mut Self::ElementState,
|
||||
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 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),
|
||||
);
|
||||
|
||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
|
||||
style.paint(bounds, cx);
|
||||
let item_size = element_state.item_size;
|
||||
let content_size = Size {
|
||||
width: padded_bounds.size.width,
|
||||
height: item_size.height * self.item_count,
|
||||
};
|
||||
|
||||
let content_size;
|
||||
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: 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 interactivity = mem::take(&mut self.interactivity);
|
||||
let shared_scroll_offset = element_state
|
||||
.interactive
|
||||
.scroll_offset
|
||||
.get_or_insert_with(Arc::default)
|
||||
.clone();
|
||||
|
||||
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 {
|
||||
width: padded_bounds.size.width,
|
||||
height: item_height * self.item_count,
|
||||
};
|
||||
|
||||
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,
|
||||
let padded_bounds = Bounds::from_corners(
|
||||
bounds.origin + point(border.left + padding.left, border.top + padding.top),
|
||||
bounds.lower_right()
|
||||
- point(border.right + padding.right, border.bottom + padding.bottom),
|
||||
);
|
||||
});
|
||||
})
|
||||
|
||||
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> {
|
||||
fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
|
||||
self.interactivity.as_stateless_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: 'static> StatefulInteractive<V> for UniformList<V> {
|
||||
fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
|
||||
impl<V> InteractiveComponent<V> for UniformList<V> {
|
||||
fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
|
||||
&mut self.interactivity
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -6,13 +6,14 @@ mod color;
|
||||
mod element;
|
||||
mod elements;
|
||||
mod executor;
|
||||
mod focusable;
|
||||
mod geometry;
|
||||
mod image_cache;
|
||||
mod input;
|
||||
mod interactive;
|
||||
mod key_dispatch;
|
||||
mod keymap;
|
||||
mod platform;
|
||||
pub mod prelude;
|
||||
mod scene;
|
||||
mod style;
|
||||
mod styled;
|
||||
@ -41,12 +42,12 @@ pub use ctor::ctor;
|
||||
pub use element::*;
|
||||
pub use elements::*;
|
||||
pub use executor::*;
|
||||
pub use focusable::*;
|
||||
pub use geometry::*;
|
||||
pub use gpui2_macros::*;
|
||||
pub use image_cache::*;
|
||||
pub use input::*;
|
||||
pub use interactive::*;
|
||||
pub use key_dispatch::*;
|
||||
pub use keymap::*;
|
||||
pub use platform::*;
|
||||
use private::Sealed;
|
||||
@ -104,6 +105,14 @@ pub trait Context {
|
||||
fn update_window<T, F>(&mut self, window: AnyWindowHandle, f: F) -> Result<T>
|
||||
where
|
||||
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 {
|
||||
@ -147,7 +156,7 @@ pub enum GlobalKey {
|
||||
}
|
||||
|
||||
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
|
||||
F: FnOnce(&mut Self) -> R;
|
||||
|
||||
@ -158,14 +167,18 @@ impl<C> BorrowAppContext for C
|
||||
where
|
||||
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
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
self.borrow_mut().push_text_style(style);
|
||||
let result = f(self);
|
||||
self.borrow_mut().pop_text_style();
|
||||
result
|
||||
if let Some(style) = style {
|
||||
self.borrow_mut().push_text_style(style);
|
||||
let result = f(self);
|
||||
self.borrow_mut().pop_text_style();
|
||||
result
|
||||
} else {
|
||||
f(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn set_global<G: 'static>(&mut self, global: G) {
|
||||
|
@ -45,7 +45,7 @@ impl<V: 'static> ElementInputHandler<V> {
|
||||
/// containing view.
|
||||
pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
|
||||
ElementInputHandler {
|
||||
view: cx.view(),
|
||||
view: cx.view().clone(),
|
||||
element_bounds,
|
||||
cx: cx.to_async(),
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
204
crates/gpui2/src/key_dispatch.rs
Normal file
204
crates/gpui2/src/key_dispatch.rs
Normal 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()
|
||||
}
|
||||
}
|
@ -1,11 +1,21 @@
|
||||
use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke};
|
||||
use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke};
|
||||
use anyhow::Result;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub struct KeyBinding {
|
||||
action: Box<dyn Action>,
|
||||
pub(super) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(super) context_predicate: Option<DispatchContextPredicate>,
|
||||
pub(crate) action: Box<dyn Action>,
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
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 {
|
||||
@ -15,7 +25,7 @@ impl KeyBinding {
|
||||
|
||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||
let context = if let Some(context) = context {
|
||||
Some(DispatchContextPredicate::parse(context)?)
|
||||
Some(KeyBindingContextPredicate::parse(context)?)
|
||||
} else {
|
||||
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
|
||||
.as_ref()
|
||||
.map(|predicate| predicate.eval(contexts))
|
||||
@ -42,7 +52,7 @@ impl KeyBinding {
|
||||
pub fn match_keystrokes(
|
||||
&self,
|
||||
pending_keystrokes: &[Keystroke],
|
||||
contexts: &[&DispatchContext],
|
||||
contexts: &[KeyContext],
|
||||
) -> KeyMatch {
|
||||
if self.keystrokes.as_ref().starts_with(&pending_keystrokes)
|
||||
&& self.matches_context(contexts)
|
||||
@ -61,7 +71,7 @@ impl KeyBinding {
|
||||
pub fn keystrokes_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
contexts: &[&DispatchContext],
|
||||
contexts: &[KeyContext],
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
if self.action.partial_eq(action) && self.matches_context(contexts) {
|
||||
Some(self.keystrokes.clone())
|
||||
|
449
crates/gpui2/src/keymap/context.rs
Normal file
449
crates/gpui2/src/keymap/context.rs
Normal 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())),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
use crate::{DispatchContextPredicate, KeyBinding, Keystroke};
|
||||
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
|
||||
use collections::HashSet;
|
||||
use smallvec::SmallVec;
|
||||
use std::{any::TypeId, collections::HashMap};
|
||||
@ -11,7 +11,7 @@ pub struct Keymap {
|
||||
bindings: Vec<KeyBinding>,
|
||||
binding_indices_by_action_id: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||
disabled_keystrokes:
|
||||
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<DispatchContextPredicate>>>,
|
||||
HashMap<SmallVec<[Keystroke; 2]>, HashSet<Option<KeyBindingContextPredicate>>>,
|
||||
version: KeymapVersion,
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
|
||||
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct KeyMatcher {
|
||||
pub struct KeystrokeMatcher {
|
||||
pending_keystrokes: Vec<Keystroke>,
|
||||
keymap: Arc<Mutex<Keymap>>,
|
||||
keymap_version: KeymapVersion,
|
||||
}
|
||||
|
||||
impl KeyMatcher {
|
||||
impl KeystrokeMatcher {
|
||||
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
|
||||
let keymap_version = keymap.lock().version();
|
||||
Self {
|
||||
@ -44,7 +44,7 @@ impl KeyMatcher {
|
||||
pub fn match_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
context_stack: &[&DispatchContext],
|
||||
context_stack: &[KeyContext],
|
||||
) -> KeyMatch {
|
||||
let keymap = self.keymap.lock();
|
||||
// Clear pending keystrokes if the keymap has changed since the last matched keystroke.
|
||||
@ -86,7 +86,7 @@ impl KeyMatcher {
|
||||
pub fn keystrokes_for_action(
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
contexts: &[&DispatchContext],
|
||||
contexts: &[KeyContext],
|
||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||
self.keymap
|
||||
.lock()
|
||||
@ -97,6 +97,7 @@ impl KeyMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum KeyMatch {
|
||||
None,
|
||||
Pending,
|
||||
|
@ -1,7 +1,9 @@
|
||||
mod binding;
|
||||
mod context;
|
||||
mod keymap;
|
||||
mod matcher;
|
||||
|
||||
pub use binding::*;
|
||||
pub use context::*;
|
||||
pub use keymap::*;
|
||||
pub use matcher::*;
|
||||
|
@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
|
||||
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_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 wrap_line(
|
||||
&self,
|
||||
|
@ -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(
|
||||
&self,
|
||||
glyph_id: &RenderGlyphParams,
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> 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 {
|
||||
@ -247,8 +250,11 @@ impl MacTextSystemState {
|
||||
.into())
|
||||
}
|
||||
|
||||
fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
|
||||
let glyph_bounds = self.raster_bounds(params)?;
|
||||
fn rasterize_glyph(
|
||||
&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 {
|
||||
Err(anyhow!("glyph bounds are empty"))
|
||||
} else {
|
||||
@ -260,6 +266,7 @@ impl MacTextSystemState {
|
||||
if params.subpixel_variant.y > 0 {
|
||||
bitmap_size.height += DevicePixels(1);
|
||||
}
|
||||
let bitmap_size = bitmap_size;
|
||||
|
||||
let mut bytes;
|
||||
let cx;
|
||||
|
@ -3,8 +3,15 @@ use crate::{
|
||||
PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
use futures::channel::oneshot;
|
||||
use parking_lot::Mutex;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
path::PathBuf,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub struct TestPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
@ -13,18 +20,60 @@ pub struct TestPlatform {
|
||||
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
|
||||
active_display: Rc<dyn PlatformDisplay>,
|
||||
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 {
|
||||
pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self {
|
||||
TestPlatform {
|
||||
pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
|
||||
Rc::new_cyclic(|weak| TestPlatform {
|
||||
background_executor: executor,
|
||||
foreground_executor,
|
||||
|
||||
prompts: Default::default(),
|
||||
active_cursor: Default::default(),
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
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!()
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn quit(&self) {}
|
||||
|
||||
fn restart(&self) {
|
||||
unimplemented!()
|
||||
@ -88,7 +135,11 @@ impl Platform for TestPlatform {
|
||||
options: WindowOptions,
|
||||
) -> Box<dyn crate::PlatformWindow> {
|
||||
*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(
|
||||
@ -118,15 +169,20 @@ impl Platform for TestPlatform {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
|
||||
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
_directory: &std::path::Path,
|
||||
) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
|
||||
unimplemented!()
|
||||
directory: &std::path::Path,
|
||||
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
|
||||
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) {
|
||||
@ -141,9 +197,7 @@ impl Platform for TestPlatform {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn on_quit(&self, _callback: Box<dyn FnMut()>) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
|
||||
|
||||
fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
|
||||
unimplemented!()
|
||||
|
@ -1,11 +1,14 @@
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
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,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
rc::{Rc, Weak},
|
||||
sync::{self, Arc},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Handlers {
|
||||
@ -19,18 +22,25 @@ pub struct TestWindow {
|
||||
bounds: WindowBounds,
|
||||
current_scene: Mutex<Option<Scene>>,
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
|
||||
input_handler: Option<Box<dyn PlatformInputHandler>>,
|
||||
handlers: Mutex<Handlers>,
|
||||
platform: Weak<TestPlatform>,
|
||||
sprite_atlas: Arc<dyn PlatformAtlas>,
|
||||
}
|
||||
|
||||
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 {
|
||||
bounds: options.bounds,
|
||||
current_scene: Default::default(),
|
||||
display,
|
||||
|
||||
sprite_atlas: Arc::new(TestAtlas),
|
||||
platform,
|
||||
input_handler: None,
|
||||
sprite_atlas: Arc::new(TestAtlas::new()),
|
||||
handlers: Default::default(),
|
||||
}
|
||||
}
|
||||
@ -73,8 +83,8 @@ impl PlatformWindow for TestWindow {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn set_input_handler(&mut self, _input_handler: Box<dyn crate::PlatformInputHandler>) {
|
||||
todo!()
|
||||
fn set_input_handler(&mut self, input_handler: Box<dyn crate::PlatformInputHandler>) {
|
||||
self.input_handler = Some(input_handler);
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
@ -83,7 +93,7 @@ impl PlatformWindow for TestWindow {
|
||||
_msg: &str,
|
||||
_answers: &[&str],
|
||||
) -> futures::channel::oneshot::Receiver<usize> {
|
||||
todo!()
|
||||
self.platform.upgrade().expect("platform dropped").prompt()
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
@ -154,26 +164,71 @@ impl PlatformWindow for TestWindow {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
_key: &crate::AtlasKey,
|
||||
_build: &mut dyn FnMut() -> anyhow::Result<(
|
||||
key: &crate::AtlasKey,
|
||||
build: &mut dyn FnMut() -> anyhow::Result<(
|
||||
Size<crate::DevicePixels>,
|
||||
std::borrow::Cow<'a, [u8]>,
|
||||
)>,
|
||||
) -> 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) {
|
||||
todo!()
|
||||
let mut state = self.0.lock();
|
||||
state.tiles = HashMap::default();
|
||||
state.next_id = 0;
|
||||
}
|
||||
}
|
||||
|
4
crates/gpui2/src/prelude.rs
Normal file
4
crates/gpui2/src/prelude.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub use crate::{
|
||||
BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
|
||||
ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
|
||||
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Result,
|
||||
Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
|
||||
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
|
||||
SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
|
||||
};
|
||||
use refineable::{Cascade, Refineable};
|
||||
use smallvec::SmallVec;
|
||||
@ -157,7 +157,7 @@ impl Default for 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 {
|
||||
self.font_weight = weight;
|
||||
}
|
||||
@ -177,7 +177,7 @@ impl TextStyle {
|
||||
self.underline = Some(underline);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
self
|
||||
}
|
||||
|
||||
pub fn font(&self) -> Font {
|
||||
@ -220,7 +220,7 @@ pub struct HighlightStyle {
|
||||
impl Eq for HighlightStyle {}
|
||||
|
||||
impl Style {
|
||||
pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
|
||||
pub fn text_style(&self) -> Option<&TextStyleRefinement> {
|
||||
if self.text.is_some() {
|
||||
Some(&self.text)
|
||||
} 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
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
F: FnOnce(&mut C) -> R,
|
||||
{
|
||||
if self.text.is_some() {
|
||||
cx.with_text_style(self.text.clone(), f)
|
||||
cx.with_text_style(Some(self.text.clone()), f)
|
||||
} else {
|
||||
f(cx)
|
||||
}
|
||||
@ -274,7 +308,7 @@ impl Style {
|
||||
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.
|
||||
|
@ -1,26 +1,24 @@
|
||||
use crate::{
|
||||
self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
|
||||
DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
|
||||
SharedString, Style, StyleRefinement, Visibility,
|
||||
SharedString, StyleRefinement, Visibility,
|
||||
};
|
||||
use crate::{BoxShadow, TextStyleRefinement};
|
||||
use refineable::Refineable;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use taffy::style::Overflow;
|
||||
|
||||
pub trait Styled {
|
||||
pub trait Styled: Sized {
|
||||
fn style(&mut self) -> &mut StyleRefinement;
|
||||
|
||||
fn computed_style(&mut self) -> Style {
|
||||
Style::default().refined(self.style().clone())
|
||||
}
|
||||
|
||||
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.
|
||||
fn full(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn full(mut self) -> Self {
|
||||
self.style().size.width = Some(relative(1.).into());
|
||||
self.style().size.height = Some(relative(1.).into());
|
||||
self
|
||||
@ -28,118 +26,98 @@ pub trait Styled {
|
||||
|
||||
/// Sets the position of the element to `relative`.
|
||||
/// [Docs](https://tailwindcss.com/docs/position)
|
||||
fn relative(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn relative(mut self) -> Self {
|
||||
self.style().position = Some(Position::Relative);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the position of the element to `absolute`.
|
||||
/// [Docs](https://tailwindcss.com/docs/position)
|
||||
fn absolute(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn absolute(mut self) -> Self {
|
||||
self.style().position = Some(Position::Absolute);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the display type of the element to `block`.
|
||||
/// [Docs](https://tailwindcss.com/docs/display)
|
||||
fn block(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn block(mut self) -> Self {
|
||||
self.style().display = Some(Display::Block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the display type of the element to `flex`.
|
||||
/// [Docs](https://tailwindcss.com/docs/display)
|
||||
fn flex(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex(mut self) -> Self {
|
||||
self.style().display = Some(Display::Flex);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the visibility of the element to `visible`.
|
||||
/// [Docs](https://tailwindcss.com/docs/visibility)
|
||||
fn visible(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn visible(mut self) -> Self {
|
||||
self.style().visibility = Some(Visibility::Visible);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the visibility of the element to `hidden`.
|
||||
/// [Docs](https://tailwindcss.com/docs/visibility)
|
||||
fn invisible(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn invisible(mut self) -> Self {
|
||||
self.style().visibility = Some(Visibility::Hidden);
|
||||
self
|
||||
}
|
||||
|
||||
fn cursor(mut self, cursor: CursorStyle) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn overflow_hidden(mut self) -> Self {
|
||||
self.style().overflow.x = Some(Overflow::Hidden);
|
||||
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
|
||||
}
|
||||
|
||||
/// Sets the cursor style when hovering an element to `default`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_default(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn cursor_default(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::Arrow);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the cursor style when hovering an element to `pointer`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
fn cursor_pointer(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn cursor_pointer(mut self) -> Self {
|
||||
self.style().mouse_cursor = Some(CursorStyle::PointingHand);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the flex direction of the element to `column`.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex-direction#column)
|
||||
fn flex_col(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_col(mut self) -> Self {
|
||||
self.style().flex_direction = Some(FlexDirection::Column);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the flex direction of the element to `row`.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex-direction#row)
|
||||
fn flex_row(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_row(mut self) -> Self {
|
||||
self.style().flex_direction = Some(FlexDirection::Row);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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)
|
||||
fn flex_1(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_1(mut self) -> Self {
|
||||
self.style().flex_grow = Some(1.);
|
||||
self.style().flex_shrink = Some(1.);
|
||||
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.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex#auto)
|
||||
fn flex_auto(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_auto(mut self) -> Self {
|
||||
self.style().flex_grow = Some(1.);
|
||||
self.style().flex_shrink = Some(1.);
|
||||
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.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex#initial)
|
||||
fn flex_initial(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_initial(mut self) -> Self {
|
||||
self.style().flex_grow = Some(0.);
|
||||
self.style().flex_shrink = Some(1.);
|
||||
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.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex#none)
|
||||
fn flex_none(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn flex_none(mut self) -> Self {
|
||||
self.style().flex_grow = Some(0.);
|
||||
self.style().flex_shrink = Some(0.);
|
||||
self
|
||||
@ -183,40 +152,28 @@ pub trait Styled {
|
||||
|
||||
/// Sets the element to allow a flex item to grow to fill any available space.
|
||||
/// [Docs](https://tailwindcss.com/docs/flex-grow)
|
||||
fn grow(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn grow(mut self) -> Self {
|
||||
self.style().flex_grow = Some(1.);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to align flex items to the start of the container's cross axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/align-items#start)
|
||||
fn items_start(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn items_start(mut self) -> Self {
|
||||
self.style().align_items = Some(AlignItems::FlexStart);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to align flex items to the end of the container's cross axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/align-items#end)
|
||||
fn items_end(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn items_end(mut self) -> Self {
|
||||
self.style().align_items = Some(AlignItems::FlexEnd);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to align flex items along the center of the container's cross axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/align-items#center)
|
||||
fn items_center(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn items_center(mut self) -> Self {
|
||||
self.style().align_items = Some(AlignItems::Center);
|
||||
self
|
||||
}
|
||||
@ -224,40 +181,28 @@ pub trait Styled {
|
||||
/// 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.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
|
||||
fn justify_between(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn justify_between(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::SpaceBetween);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items along the center of the container's main axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#center)
|
||||
fn justify_center(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn justify_center(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::Center);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items against the start of the container's main axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#start)
|
||||
fn justify_start(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn justify_start(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::Start);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items against the end of the container's main axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#end)
|
||||
fn justify_end(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn justify_end(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::End);
|
||||
self
|
||||
}
|
||||
@ -265,10 +210,7 @@ pub trait Styled {
|
||||
/// 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.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
|
||||
fn justify_around(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn justify_around(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::SpaceAround);
|
||||
self
|
||||
}
|
||||
@ -295,30 +237,21 @@ pub trait Styled {
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self {
|
||||
self.style().box_shadow = Some(shadows);
|
||||
self
|
||||
}
|
||||
|
||||
/// Clears the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_none(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_none(mut self) -> Self {
|
||||
self.style().box_shadow = Some(Default::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_sm(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_sm(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.05),
|
||||
offset: point(px(0.), px(1.)),
|
||||
@ -330,10 +263,7 @@ pub trait Styled {
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_md(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_md(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0.5, 0., 0., 0.1),
|
||||
@ -353,10 +283,7 @@ pub trait Styled {
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_lg(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_lg(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
@ -376,10 +303,7 @@ pub trait Styled {
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_xl(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_xl(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![
|
||||
BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.1),
|
||||
@ -399,10 +323,7 @@ pub trait Styled {
|
||||
|
||||
/// Sets the box shadow of the element.
|
||||
/// [Docs](https://tailwindcss.com/docs/box-shadow)
|
||||
fn shadow_2xl(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn shadow_2xl(mut self) -> Self {
|
||||
self.style().box_shadow = Some(smallvec![BoxShadow {
|
||||
color: hsla(0., 0., 0., 0.25),
|
||||
offset: point(px(0.), px(25.)),
|
||||
@ -417,198 +338,138 @@ pub trait Styled {
|
||||
&mut style.text
|
||||
}
|
||||
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_xs(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_xs(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(0.75).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_sm(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_sm(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(0.875).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_base(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_base(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(1.0).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_lg(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_lg(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(1.125).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_xl(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(1.25).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_2xl(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_2xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(1.5).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_3xl(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_3xl(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_size = Some(rems(1.875).into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_none(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_none(mut self) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.underline = None;
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_solid(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_solid(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.wavy = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_wavy(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_wavy(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.wavy = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_0(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_0(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.thickness = px(0.);
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_1(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_1(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.thickness = px(1.);
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_2(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_2(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.thickness = px(2.);
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_4(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_4(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.thickness = px(4.);
|
||||
self
|
||||
}
|
||||
|
||||
fn text_decoration_8(mut self) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn text_decoration_8(mut self) -> Self {
|
||||
let style = self.text_style().get_or_insert_with(Default::default);
|
||||
let underline = style.underline.get_or_insert_with(Default::default);
|
||||
underline.thickness = px(8.);
|
||||
self
|
||||
}
|
||||
|
||||
fn font(mut self, family_name: impl Into<SharedString>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn font(mut self, family_name: impl Into<SharedString>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.font_family = Some(family_name.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
|
||||
self.text_style()
|
||||
.get_or_insert_with(Default::default)
|
||||
.line_height = Some(line_height.into());
|
||||
|
@ -39,6 +39,7 @@ pub struct TextSystem {
|
||||
platform_text_system: Arc<dyn PlatformTextSystem>,
|
||||
font_ids_by_font: RwLock<HashMap<Font, FontId>>,
|
||||
font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
|
||||
raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
|
||||
wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
|
||||
font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
|
||||
}
|
||||
@ -48,10 +49,11 @@ impl TextSystem {
|
||||
TextSystem {
|
||||
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
|
||||
platform_text_system,
|
||||
font_metrics: RwLock::new(HashMap::default()),
|
||||
font_ids_by_font: RwLock::new(HashMap::default()),
|
||||
wrapper_pool: Mutex::new(HashMap::default()),
|
||||
font_runs_pool: Default::default(),
|
||||
font_metrics: RwLock::default(),
|
||||
raster_bounds: RwLock::default(),
|
||||
font_ids_by_font: RwLock::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>> {
|
||||
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(
|
||||
&self,
|
||||
glyph_id: &RenderGlyphParams,
|
||||
params: &RenderGlyphParams,
|
||||
) -> 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)]
|
||||
pub struct TextRun {
|
||||
// number of utf8 bytes
|
||||
pub len: usize,
|
||||
pub font: Font,
|
||||
pub color: Hsla,
|
||||
|
@ -68,7 +68,8 @@ impl LineLayout {
|
||||
prev_x = glyph.position.x;
|
||||
}
|
||||
}
|
||||
prev_index
|
||||
|
||||
self.len
|
||||
}
|
||||
|
||||
pub fn x_for_index(&self, index: usize) -> Pixels {
|
||||
|
@ -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 async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
|
||||
// where
|
||||
// F: Future<Output = T>,
|
||||
// {
|
||||
// let timer = async {
|
||||
// smol::Timer::after(timeout).await;
|
||||
// Err(())
|
||||
// };
|
||||
// let future = async move { Ok(f.await) };
|
||||
// timer.race(future).await
|
||||
// }
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
let timer = async {
|
||||
smol::Timer::after(timeout).await;
|
||||
Err(())
|
||||
};
|
||||
let future = async move { Ok(f.await) };
|
||||
timer.race(future).await
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
|
||||
Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId, Model, Pixels,
|
||||
Size, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, LayoutId,
|
||||
Model, Pixels, Size, ViewContext, VisualContext, WeakModel, WindowContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
@ -184,10 +184,6 @@ impl AnyView {
|
||||
.compute_layout(layout_id, available_space);
|
||||
(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 {
|
||||
@ -210,7 +206,7 @@ impl<V: Render> From<View<V>> for AnyView {
|
||||
impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
|
||||
type ElementState = Box<dyn Any>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
fn element_id(&self) -> Option<ElementId> {
|
||||
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 {
|
||||
use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
|
||||
use std::any::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 element = view.update(cx, |view, cx| {
|
||||
let mut element = AnyElement::new(view.render(cx));
|
||||
@ -306,7 +380,7 @@ mod any_view {
|
||||
element: &mut Box<dyn Any>,
|
||||
cx: &mut WindowContext,
|
||||
) -> 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 element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
view.update(cx, |view, cx| element.layout(view, cx))
|
||||
@ -318,7 +392,7 @@ mod any_view {
|
||||
element: &mut Box<dyn Any>,
|
||||
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 element = element.downcast_mut::<AnyElement<V>>().unwrap();
|
||||
view.update(cx, |view, cx| element.paint(view, cx))
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -34,13 +34,21 @@ pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let visibility = input.vis;
|
||||
|
||||
let output = match input.data {
|
||||
syn::Data::Struct(ref struct_data) => {
|
||||
let fields = &struct_data.fields;
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility struct #name #fields
|
||||
syn::Data::Struct(ref struct_data) => match &struct_data.fields {
|
||||
syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
|
||||
let fields = &struct_data.fields;
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility struct #name #fields
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Fields::Unit => {
|
||||
quote! {
|
||||
#attributes
|
||||
#visibility struct #name;
|
||||
}
|
||||
}
|
||||
},
|
||||
syn::Data::Enum(ref enum_data) => {
|
||||
let variants = &enum_data.variants;
|
||||
quote! {
|
||||
|
@ -130,7 +130,7 @@ fn generate_predefined_setter(
|
||||
|
||||
let method = quote! {
|
||||
#[doc = #doc_string]
|
||||
fn #method_name(mut self) -> Self where Self: std::marker::Sized {
|
||||
fn #method_name(mut self) -> Self {
|
||||
let style = self.style();
|
||||
#(#field_assignments)*
|
||||
self
|
||||
@ -163,7 +163,7 @@ fn generate_custom_value_setter(
|
||||
|
||||
let method = quote! {
|
||||
#[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();
|
||||
#(#field_assignments)*
|
||||
self
|
||||
|
@ -14,5 +14,6 @@ test-support = []
|
||||
smol.workspace = true
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
util = { path = "../util" }
|
||||
|
@ -1,10 +1,9 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::AsyncAppContext;
|
||||
use gpui::{actions, AsyncAppContext};
|
||||
use std::path::Path;
|
||||
use util::ResultExt;
|
||||
|
||||
// todo!()
|
||||
// actions!(cli, [Install]);
|
||||
actions!(Install);
|
||||
|
||||
pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
|
||||
let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;
|
||||
|
@ -1858,7 +1858,7 @@ mod tests {
|
||||
async fn test_first_line_pattern(cx: &mut TestAppContext) {
|
||||
let mut languages = LanguageRegistry::test();
|
||||
|
||||
languages.set_executor(cx.executor().clone());
|
||||
languages.set_executor(cx.executor());
|
||||
let languages = Arc::new(languages);
|
||||
languages.register(
|
||||
"/javascript",
|
||||
@ -1895,7 +1895,7 @@ mod tests {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_language_loading(cx: &mut TestAppContext) {
|
||||
let mut languages = LanguageRegistry::test();
|
||||
languages.set_executor(cx.executor().clone());
|
||||
languages.set_executor(cx.executor());
|
||||
let languages = Arc::new(languages);
|
||||
languages.register(
|
||||
"/JSON",
|
||||
|
@ -167,7 +167,7 @@ fn main() {
|
||||
panic!("unexpected message");
|
||||
}
|
||||
|
||||
cx.update(|cx| cx.quit()).ok();
|
||||
cx.update(|cx| cx.shutdown()).ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
@ -1,9 +1,13 @@
|
||||
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/mmastrac/rust-ctor/issues/280
|
||||
pub fn unused() {}
|
||||
pub fn init() {}
|
||||
|
||||
actions!(
|
||||
Cancel,
|
||||
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
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::{
|
||||
env::consts,
|
||||
@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync {
|
||||
|
||||
pub struct RealNodeRuntime {
|
||||
http: Arc<dyn HttpClient>,
|
||||
installation_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
impl RealNodeRuntime {
|
||||
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> {
|
||||
let _lock = self.installation_lock.lock().await;
|
||||
log::info!("Node runtime install_if_needed");
|
||||
|
||||
let arch = match consts::ARCH {
|
||||
@ -73,6 +78,9 @@ impl RealNodeRuntime {
|
||||
.stdin(Stdio::null())
|
||||
.stdout(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()
|
||||
.await;
|
||||
let valid = matches!(result, Ok(status) if status.success());
|
||||
@ -96,6 +104,11 @@ impl RealNodeRuntime {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime {
|
||||
|
||||
let mut command = Command::new(node_binary);
|
||||
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 {
|
||||
command.current_dir(directory);
|
||||
|
@ -1,17 +1,17 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, uniform_list, Component, Div, FocusEnabled, ParentElement, Render, StatefulInteractivity,
|
||||
StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
|
||||
UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::cmp;
|
||||
use ui::{prelude::*, v_stack, Divider};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Divider, Label, TextColor};
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
pub delegate: D,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
editor: View<Editor>,
|
||||
pending_update_matches: Option<Task<Option<()>>>,
|
||||
pending_update_matches: Option<Task<()>>,
|
||||
confirm_on_update: Option<bool>,
|
||||
}
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
@ -21,7 +21,7 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
fn selected_index(&self) -> usize;
|
||||
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 confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
||||
@ -37,21 +37,28 @@ pub trait PickerDelegate: Sized + 'static {
|
||||
|
||||
impl<D: PickerDelegate> Picker<D> {
|
||||
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();
|
||||
Self {
|
||||
let mut this = Self {
|
||||
delegate,
|
||||
editor,
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
pending_update_matches: None,
|
||||
editor,
|
||||
}
|
||||
confirm_on_update: None,
|
||||
};
|
||||
this.update_matches("".to_string(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn focus(&self, cx: &mut WindowContext) {
|
||||
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();
|
||||
if count > 0 {
|
||||
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>) {
|
||||
self.delegate.dismissed(cx);
|
||||
}
|
||||
|
||||
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>) {
|
||||
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(
|
||||
@ -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>) {
|
||||
let update = self.delegate.update_matches(query, cx);
|
||||
self.matches_updated(cx);
|
||||
@ -123,7 +159,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.matches_updated(cx);
|
||||
})
|
||||
.ok()
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
@ -131,18 +167,19 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
let index = self.delegate.selected_index();
|
||||
self.scroll_handle.scroll_to_item(index);
|
||||
self.pending_update_matches = None;
|
||||
if let Some(secondary) = self.confirm_on_update.take() {
|
||||
self.delegate.confirm(secondary, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
div()
|
||||
.context("picker")
|
||||
.id("picker-container")
|
||||
.focusable()
|
||||
.key_context("picker")
|
||||
.size_full()
|
||||
.elevation_2(cx)
|
||||
.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(Divider::horizontal())
|
||||
.child(
|
||||
v_stack()
|
||||
.p_1()
|
||||
.grow()
|
||||
.child(
|
||||
uniform_list("candidates", self.delegate.match_count(), {
|
||||
move |this: &mut Self, visible_range, cx| {
|
||||
let selected_ix = this.delegate.selected_index();
|
||||
visible_range
|
||||
.map(|ix| 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(
|
||||
uniform_list("candidates", self.delegate.match_count(), {
|
||||
move |this: &mut Self, visible_range, cx| {
|
||||
let selected_ix = this.delegate.selected_index();
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
move |this: &mut Self, event, cx| {
|
||||
this.handle_click(
|
||||
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)),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -497,7 +497,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
"/root",
|
||||
json!({
|
||||
@ -573,7 +573,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
"/root",
|
||||
json!({
|
||||
@ -638,7 +638,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
"/root",
|
||||
json!({
|
||||
@ -731,7 +731,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
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(
|
||||
"/root",
|
||||
json!({
|
||||
@ -812,7 +812,7 @@ mod tests {
|
||||
async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
|
@ -863,7 +863,7 @@ impl Project {
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Model<Project> {
|
||||
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 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));
|
||||
|
@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
@ -189,7 +189,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
json!({
|
||||
@ -547,7 +547,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/the-root",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
@ -914,7 +914,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -1046,7 +1046,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "" })).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;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "x" })).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;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "" })).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;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
|
||||
.await;
|
||||
|
||||
@ -1401,7 +1401,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": text })).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 fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "a.rs": text })).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) {
|
||||
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" }))
|
||||
.await;
|
||||
|
||||
@ -1813,7 +1813,7 @@ async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -1959,7 +1959,7 @@ async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAp
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -2067,7 +2067,7 @@ async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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 fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -2299,7 +2299,7 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -2396,7 +2396,7 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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 fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({})).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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -2927,7 +2927,7 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3074,7 +3074,7 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let initial_contents = "aaa\nbbbbb\nc\n";
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
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) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3216,7 +3216,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/the-dir",
|
||||
json!({
|
||||
@ -3479,7 +3479,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3596,7 +3596,7 @@ async fn test_rename(cx: &mut gpui::TestAppContext) {
|
||||
async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3655,7 +3655,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let search_query = "file";
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3767,7 +3767,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let search_query = "file";
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
@ -3878,7 +3878,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||
|
||||
let search_query = "file";
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
|
41
crates/project_panel2/Cargo.toml
Normal file
41
crates/project_panel2/Cargo.toml
Normal 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
|
96
crates/project_panel2/src/file_associations.rs
Normal file
96
crates/project_panel2/src/file_associations.rs
Normal 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()))
|
||||
}
|
||||
}
|
2880
crates/project_panel2/src/project_panel.rs
Normal file
2880
crates/project_panel2/src/project_panel.rs
Normal file
File diff suppressed because it is too large
Load Diff
45
crates/project_panel2/src/project_panel_settings.rs
Normal file
45
crates/project_panel2/src/project_panel_settings.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -577,18 +577,18 @@ mod tests {
|
||||
let client2 = Peer::new(0);
|
||||
|
||||
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) =
|
||||
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) =
|
||||
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) =
|
||||
Connection::in_memory(cx.executor().clone());
|
||||
Connection::in_memory(cx.executor());
|
||||
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) =
|
||||
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_task2).detach();
|
||||
@ -763,16 +763,16 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
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 client = Peer::new(0);
|
||||
|
||||
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) =
|
||||
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) =
|
||||
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_task2).detach();
|
||||
|
@ -10,16 +10,15 @@ use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
action, actions, blue, div, red, rems, white, Action, AnyElement, AnyView, AppContext,
|
||||
Component, Div, Entity, EventEmitter, Hsla, ParentElement as _, Render, Styled, Subscription,
|
||||
Svg, Task, View, ViewContext, VisualContext as _, WindowContext,
|
||||
action, actions, div, red, Action, AppContext, Component, Div, EventEmitter,
|
||||
ParentComponent as _, Render, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::search::SearchQuery;
|
||||
use serde::Deserialize;
|
||||
use std::{any::Any, sync::Arc};
|
||||
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 workspace::{
|
||||
item::ItemHandle,
|
||||
|
@ -1,17 +1,10 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use gpui::{
|
||||
div, Action, AnyElement, Component, CursorStyle, Element, MouseButton, MouseDownEvent,
|
||||
ParentElement as _, StatelessInteractive, Styled, Svg, View, ViewContext,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{v_stack, Button, ButtonVariant, Label};
|
||||
use gpui::{div, Action, Component, ViewContext};
|
||||
use ui::{Button, ButtonVariant};
|
||||
use workspace::searchable::Direction;
|
||||
|
||||
use crate::{
|
||||
mode::{SearchMode, Side},
|
||||
SelectNextMatch, SelectPrevMatch,
|
||||
};
|
||||
use crate::mode::{SearchMode, Side};
|
||||
|
||||
pub(super) fn render_nav_button<V: 'static>(
|
||||
icon: &'static str,
|
||||
|
@ -33,7 +33,7 @@ lazy_static.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
async-trait.workspace = true
|
||||
tiktoken-rs = "0.5.0"
|
||||
tiktoken-rs.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
|
@ -9,7 +9,7 @@ use schemars::{
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use util::{asset_str, ResultExt};
|
||||
use util::asset_str;
|
||||
|
||||
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
|
||||
#[serde(transparent)]
|
||||
@ -86,7 +86,9 @@ impl KeymapFile {
|
||||
"invalid binding value for keystroke {keystroke}, context {context:?}"
|
||||
)
|
||||
})
|
||||
.log_err()
|
||||
// todo!()
|
||||
.ok()
|
||||
// .log_err()
|
||||
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::story::Story;
|
||||
use gpui::{px, Div, Render};
|
||||
use gpui::{prelude::*, px, Div, Render};
|
||||
use theme2::{default_color_scales, ColorScaleStep};
|
||||
use ui::prelude::*;
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
use gpui::{
|
||||
actions, div, Div, FocusEnabled, Focusable, KeyBinding, ParentElement, Render,
|
||||
StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext, WindowContext,
|
||||
actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
|
||||
WindowContext,
|
||||
};
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
actions!(ActionA, ActionB, ActionC);
|
||||
|
||||
pub struct FocusStory {}
|
||||
pub struct FocusStory {
|
||||
child_1_focus: FocusHandle,
|
||||
child_2_focus: FocusHandle,
|
||||
}
|
||||
|
||||
impl FocusStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
@ -16,12 +19,15 @@ impl FocusStory {
|
||||
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 {
|
||||
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 {
|
||||
let theme = cx.theme();
|
||||
@ -31,18 +37,16 @@ impl Render for FocusStory {
|
||||
let color_4 = theme.status().conflict;
|
||||
let color_5 = theme.status().ignored;
|
||||
let color_6 = theme.status().renamed;
|
||||
let child_1 = cx.focus_handle();
|
||||
let child_2 = cx.focus_handle();
|
||||
|
||||
div()
|
||||
.id("parent")
|
||||
.focusable()
|
||||
.context("parent")
|
||||
.key_context("parent")
|
||||
.on_action(|_, action: &ActionA, cx| {
|
||||
println!("Action A dispatched on parent during");
|
||||
println!("Action A dispatched on parent");
|
||||
})
|
||||
.on_action(|_, action: &ActionB, cx| {
|
||||
println!("Action B dispatched on parent during");
|
||||
println!("Action B dispatched on parent");
|
||||
})
|
||||
.on_focus(|_, _, _| println!("Parent focused"))
|
||||
.on_blur(|_, _, _| println!("Parent blurred"))
|
||||
@ -56,8 +60,8 @@ impl Render for FocusStory {
|
||||
.focus_in(|style| style.bg(color_3))
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&child_1)
|
||||
.context("child-1")
|
||||
.track_focus(&self.child_1_focus)
|
||||
.key_context("child-1")
|
||||
.on_action(|_, action: &ActionB, cx| {
|
||||
println!("Action B dispatched on child 1 during");
|
||||
})
|
||||
@ -76,10 +80,10 @@ impl Render for FocusStory {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.track_focus(&child_2)
|
||||
.context("child-2")
|
||||
.track_focus(&self.child_2_focus)
|
||||
.key_context("child-2")
|
||||
.on_action(|_, action: &ActionC, cx| {
|
||||
println!("Action C dispatched on child 2 during");
|
||||
println!("Action C dispatched on child 2");
|
||||
})
|
||||
.w_full()
|
||||
.h_6()
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 ui::prelude::*;
|
||||
|
||||
@ -12,7 +12,7 @@ impl 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 {
|
||||
let component_stories = ComponentStory::iter()
|
||||
|
@ -1,11 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task,
|
||||
View, VisualContext, WindowContext,
|
||||
};
|
||||
use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme2::ActiveTheme;
|
||||
|
||||
pub struct PickerStory {
|
||||
@ -44,6 +40,10 @@ impl PickerDelegate for Delegate {
|
||||
self.candidates.len()
|
||||
}
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Test".into()
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user