Merge branch 'main' into search2

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

67
Cargo.lock generated
View file

@ -2012,7 +2012,7 @@ dependencies = [
"serde_derive",
"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",

View file

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

View file

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

View file

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

View file

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

View file

@ -29,7 +29,7 @@ postage.workspace = true
rand.workspace = true
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"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -208,11 +208,11 @@ impl TestServer {
})
});
let fs = FakeFs::new(cx.executor().clone());
let fs = FakeFs::new(cx.executor());
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
let 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(),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ mod editor_tests;
pub mod test;
use ::git::diff::DiffHunk;
use aho_corasick::AhoCorasick;
use anyhow::{Context as _, Result};
use anyhow::{anyhow, Context as _, Result};
use blink_manager::BlinkManager;
use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
use clock::ReplicaId;
@ -39,12 +39,12 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
action, actions, div, point, px, relative, rems, size, uniform_list, AnyElement, AppContext,
AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
DispatchContext, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, Model, MouseButton, ParentElement, Pixels, Render,
StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View,
ViewContext, VisualContext, WeakView, WindowContext,
action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@ -69,7 +69,7 @@ pub use multi_buffer::{
};
use ordered_float::OrderedFloat;
use parking_lot::{Mutex, RwLock};
use project::{FormatTrigger, Location, Project, ProjectTransaction};
use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
use rand::prelude::*;
use rpc::proto::*;
use scroll::{
@ -97,10 +97,12 @@ use text::{OffsetUtf16, Rope};
use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
};
use ui::{IconButton, StyledExt};
use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace,
item::{ItemEvent, ItemHandle},
searchable::SearchEvent,
ItemNavHistory, SplitDirection, ViewId, Workspace,
};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@ -646,7 +648,7 @@ pub struct Editor {
collapse_matches: bool,
autoindent_mode: Option<AutoindentMode>,
workspace: Option<(WeakView<Workspace>, i64)>,
keymap_context_layers: BTreeMap<TypeId, DispatchContext>,
keymap_context_layers: BTreeMap<TypeId, KeyContext>,
input_enabled: bool,
read_only: bool,
leader_peer_id: Option<PeerId>,
@ -1878,10 +1880,8 @@ impl Editor {
);
let focus_handle = cx.focus_handle();
cx.on_focus_in(&focus_handle, Self::handle_focus_in)
.detach();
cx.on_focus_out(&focus_handle, Self::handle_focus_out)
.detach();
cx.on_focus(&focus_handle, Self::handle_focus).detach();
cx.on_blur(&focus_handle, Self::handle_blur).detach();
let mut this = Self {
handle: cx.view().downgrade(),
@ -1981,9 +1981,9 @@ impl Editor {
this
}
fn dispatch_context(&self, cx: &AppContext) -> DispatchContext {
let mut dispatch_context = DispatchContext::default();
dispatch_context.insert("Editor");
fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
let mut dispatch_context = KeyContext::default();
dispatch_context.add("Editor");
let mode = match self.mode {
EditorMode::SingleLine => "single_line",
EditorMode::AutoHeight { .. } => "auto_height",
@ -1991,17 +1991,17 @@ impl Editor {
};
dispatch_context.set("mode", mode);
if self.pending_rename.is_some() {
dispatch_context.insert("renaming");
dispatch_context.add("renaming");
}
if self.context_menu_visible() {
match self.context_menu.read().as_ref() {
Some(ContextMenu::Completions(_)) => {
dispatch_context.insert("menu");
dispatch_context.insert("showing_completions")
dispatch_context.add("menu");
dispatch_context.add("showing_completions")
}
Some(ContextMenu::CodeActions(_)) => {
dispatch_context.insert("menu");
dispatch_context.insert("showing_code_actions")
dispatch_context.add("menu");
dispatch_context.add("showing_code_actions")
}
None => {}
}
@ -2023,24 +2023,24 @@ impl Editor {
dispatch_context
}
// pub fn new_file(
// workspace: &mut Workspace,
// _: &workspace::NewFile,
// cx: &mut ViewContext<Workspace>,
// ) {
// let project = workspace.project().clone();
// if project.read(cx).is_remote() {
// cx.propagate();
// } else if let Some(buffer) = project
// .update(cx, |project, cx| project.create_buffer("", None, cx))
// .log_err()
// {
// workspace.add_item(
// Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
// cx,
// );
// }
// }
pub fn new_file(
workspace: &mut Workspace,
_: &workspace::NewFile,
cx: &mut ViewContext<Workspace>,
) {
let project = workspace.project().clone();
if project.read(cx).is_remote() {
cx.propagate();
} else if let Some(buffer) = project
.update(cx, |project, cx| project.create_buffer("", None, cx))
.log_err()
{
workspace.add_item(
Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
cx,
);
}
}
// pub fn new_file_in_direction(
// workspace: &mut Workspace,
@ -2124,17 +2124,17 @@ impl Editor {
// )
// }
// pub fn mode(&self) -> EditorMode {
// self.mode
// }
pub fn mode(&self) -> EditorMode {
self.mode
}
// pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
// self.collaboration_hub.as_deref()
// }
pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
self.collaboration_hub.as_deref()
}
// pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
// self.collaboration_hub = Some(hub);
// }
pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
self.collaboration_hub = Some(hub);
}
pub fn set_placeholder_text(
&mut self,
@ -7588,53 +7588,47 @@ impl Editor {
})
}
// pub fn find_all_references(
// workspace: &mut Workspace,
// _: &FindAllReferences,
// cx: &mut ViewContext<Workspace>,
// ) -> Option<Task<Result<()>>> {
// let active_item = workspace.active_item(cx)?;
// let editor_handle = active_item.act_as::<Self>(cx)?;
pub fn find_all_references(
&mut self,
_: &FindAllReferences,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let buffer = self.buffer.read(cx);
let head = self.selections.newest::<usize>(cx).head();
let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
let replica_id = self.replica_id(cx);
// let editor = editor_handle.read(cx);
// let buffer = editor.buffer.read(cx);
// let head = editor.selections.newest::<usize>(cx).head();
// let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
// let replica_id = editor.replica_id(cx);
let workspace = self.workspace()?;
let project = workspace.read(cx).project().clone();
let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
Some(cx.spawn(|_, mut cx| async move {
let locations = references.await?;
if locations.is_empty() {
return Ok(());
}
// let project = workspace.project().clone();
// let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
// Some(cx.spawn_labeled(
// "Finding All References...",
// |workspace, mut cx| async move {
// let locations = references.await?;
// if locations.is_empty() {
// return Ok(());
// }
workspace.update(&mut cx, |workspace, cx| {
let title = locations
.first()
.as_ref()
.map(|location| {
let buffer = location.buffer.read(cx);
format!(
"References to `{}`",
buffer
.text_for_range(location.range.clone())
.collect::<String>()
)
})
.unwrap();
Self::open_locations_in_multibuffer(
workspace, locations, replica_id, title, false, cx,
);
})?;
// workspace.update(&mut cx, |workspace, cx| {
// let title = locations
// .first()
// .as_ref()
// .map(|location| {
// let buffer = location.buffer.read(cx);
// format!(
// "References to `{}`",
// buffer
// .text_for_range(location.range.clone())
// .collect::<String>()
// )
// })
// .unwrap();
// Self::open_locations_in_multibuffer(
// workspace, locations, replica_id, title, false, cx,
// );
// })?;
// Ok(())
// },
// ))
// }
Ok(())
}))
}
/// Opens a multibuffer with the given project locations in it
pub fn open_locations_in_multibuffer(
@ -7685,7 +7679,7 @@ impl Editor {
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
ranges_to_highlight,
|theme| todo!("theme.editor.highlighted_line_background"),
|theme| theme.editor_highlighted_line_background,
cx,
);
});
@ -7696,183 +7690,210 @@ impl Editor {
}
}
// pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
// use language::ToOffset as _;
pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
use language::ToOffset as _;
// let project = self.project.clone()?;
// let selection = self.selections.newest_anchor().clone();
// let (cursor_buffer, cursor_buffer_position) = self
// .buffer
// .read(cx)
// .text_anchor_for_position(selection.head(), cx)?;
// let (tail_buffer, _) = self
// .buffer
// .read(cx)
// .text_anchor_for_position(selection.tail(), cx)?;
// if tail_buffer != cursor_buffer {
// return None;
// }
let project = self.project.clone()?;
let selection = self.selections.newest_anchor().clone();
let (cursor_buffer, cursor_buffer_position) = self
.buffer
.read(cx)
.text_anchor_for_position(selection.head(), cx)?;
let (tail_buffer, _) = self
.buffer
.read(cx)
.text_anchor_for_position(selection.tail(), cx)?;
if tail_buffer != cursor_buffer {
return None;
}
// let snapshot = cursor_buffer.read(cx).snapshot();
// let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
// let prepare_rename = project.update(cx, |project, cx| {
// project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
// });
let snapshot = cursor_buffer.read(cx).snapshot();
let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
let prepare_rename = project.update(cx, |project, cx| {
project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
});
// Some(cx.spawn(|this, mut cx| async move {
// let rename_range = if let Some(range) = prepare_rename.await? {
// Some(range)
// } else {
// this.update(&mut cx, |this, cx| {
// let buffer = this.buffer.read(cx).snapshot(cx);
// let mut buffer_highlights = this
// .document_highlights_for_position(selection.head(), &buffer)
// .filter(|highlight| {
// highlight.start.excerpt_id == selection.head().excerpt_id
// && highlight.end.excerpt_id == selection.head().excerpt_id
// });
// buffer_highlights
// .next()
// .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
// })?
// };
// if let Some(rename_range) = rename_range {
// let rename_buffer_range = rename_range.to_offset(&snapshot);
// let cursor_offset_in_rename_range =
// cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
Some(cx.spawn(|this, mut cx| async move {
let rename_range = if let Some(range) = prepare_rename.await? {
Some(range)
} else {
this.update(&mut cx, |this, cx| {
let buffer = this.buffer.read(cx).snapshot(cx);
let mut buffer_highlights = this
.document_highlights_for_position(selection.head(), &buffer)
.filter(|highlight| {
highlight.start.excerpt_id == selection.head().excerpt_id
&& highlight.end.excerpt_id == selection.head().excerpt_id
});
buffer_highlights
.next()
.map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
})?
};
if let Some(rename_range) = rename_range {
let rename_buffer_range = rename_range.to_offset(&snapshot);
let cursor_offset_in_rename_range =
cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
// this.update(&mut cx, |this, cx| {
// this.take_rename(false, cx);
// let buffer = this.buffer.read(cx).read(cx);
// let cursor_offset = selection.head().to_offset(&buffer);
// let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
// let rename_end = rename_start + rename_buffer_range.len();
// let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
// let mut old_highlight_id = None;
// let old_name: Arc<str> = buffer
// .chunks(rename_start..rename_end, true)
// .map(|chunk| {
// if old_highlight_id.is_none() {
// old_highlight_id = chunk.syntax_highlight_id;
// }
// chunk.text
// })
// .collect::<String>()
// .into();
this.update(&mut cx, |this, cx| {
this.take_rename(false, cx);
let buffer = this.buffer.read(cx).read(cx);
let cursor_offset = selection.head().to_offset(&buffer);
let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
let rename_end = rename_start + rename_buffer_range.len();
let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
let mut old_highlight_id = None;
let old_name: Arc<str> = buffer
.chunks(rename_start..rename_end, true)
.map(|chunk| {
if old_highlight_id.is_none() {
old_highlight_id = chunk.syntax_highlight_id;
}
chunk.text
})
.collect::<String>()
.into();
// drop(buffer);
drop(buffer);
// // Position the selection in the rename editor so that it matches the current selection.
// this.show_local_selections = false;
// let rename_editor = cx.build_view(|cx| {
// let mut editor = Editor::single_line(cx);
// if let Some(old_highlight_id) = old_highlight_id {
// editor.override_text_style =
// Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
// }
// editor.buffer.update(cx, |buffer, cx| {
// buffer.edit([(0..0, old_name.clone())], None, cx)
// });
// editor.select_all(&SelectAll, cx);
// editor
// });
// Position the selection in the rename editor so that it matches the current selection.
this.show_local_selections = false;
let rename_editor = cx.build_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, old_name.clone())], None, cx)
});
editor.select_all(&SelectAll, cx);
editor
});
// let ranges = this
// .clear_background_highlights::<DocumentHighlightWrite>(cx)
// .into_iter()
// .flat_map(|(_, ranges)| ranges.into_iter())
// .chain(
// this.clear_background_highlights::<DocumentHighlightRead>(cx)
// .into_iter()
// .flat_map(|(_, ranges)| ranges.into_iter()),
// )
// .collect();
let ranges = this
.clear_background_highlights::<DocumentHighlightWrite>(cx)
.into_iter()
.flat_map(|(_, ranges)| ranges.into_iter())
.chain(
this.clear_background_highlights::<DocumentHighlightRead>(cx)
.into_iter()
.flat_map(|(_, ranges)| ranges.into_iter()),
)
.collect();
// this.highlight_text::<Rename>(
// ranges,
// HighlightStyle {
// fade_out: Some(style.rename_fade),
// ..Default::default()
// },
// cx,
// );
// cx.focus(&rename_editor);
// let block_id = this.insert_blocks(
// [BlockProperties {
// style: BlockStyle::Flex,
// position: range.start.clone(),
// height: 1,
// render: Arc::new({
// let editor = rename_editor.clone();
// move |cx: &mut BlockContext| {
// ChildView::new(&editor, cx)
// .contained()
// .with_padding_left(cx.anchor_x)
// .into_any()
// }
// }),
// disposition: BlockDisposition::Below,
// }],
// Some(Autoscroll::fit()),
// cx,
// )[0];
// this.pending_rename = Some(RenameState {
// range,
// old_name,
// editor: rename_editor,
// block_id,
// });
// })?;
// }
this.highlight_text::<Rename>(
ranges,
HighlightStyle {
fade_out: Some(0.6),
..Default::default()
},
cx,
);
let rename_focus_handle = rename_editor.focus_handle(cx);
cx.focus(&rename_focus_handle);
let block_id = this.insert_blocks(
[BlockProperties {
style: BlockStyle::Flex,
position: range.start.clone(),
height: 1,
render: Arc::new({
let rename_editor = rename_editor.clone();
move |cx: &mut BlockContext| {
let mut text_style = cx.editor_style.text.clone();
if let Some(highlight_style) = old_highlight_id
.and_then(|h| h.style(&cx.editor_style.syntax))
{
text_style = text_style.highlight(highlight_style);
}
div()
.pl(cx.anchor_x)
.child(render_view(
&rename_editor,
EditorElement::new(
&rename_editor,
EditorStyle {
background: cx.theme().system().transparent,
local_player: cx.editor_style.local_player,
text: text_style,
scrollbar_width: cx
.editor_style
.scrollbar_width,
syntax: cx.editor_style.syntax.clone(),
diagnostic_style: cx
.editor_style
.diagnostic_style
.clone(),
},
),
))
.render()
}
}),
disposition: BlockDisposition::Below,
}],
Some(Autoscroll::fit()),
cx,
)[0];
this.pending_rename = Some(RenameState {
range,
old_name,
editor: rename_editor,
block_id,
});
})?;
}
// Ok(())
// }))
// }
Ok(())
}))
}
// pub fn confirm_rename(
// workspace: &mut Workspace,
// _: &ConfirmRename,
// cx: &mut ViewContext<Workspace>,
// ) -> Option<Task<Result<()>>> {
// let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
pub fn confirm_rename(
&mut self,
_: &ConfirmRename,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
let rename = self.take_rename(false, cx)?;
let workspace = self.workspace()?;
let (start_buffer, start) = self
.buffer
.read(cx)
.text_anchor_for_position(rename.range.start.clone(), cx)?;
let (end_buffer, end) = self
.buffer
.read(cx)
.text_anchor_for_position(rename.range.end.clone(), cx)?;
if start_buffer != end_buffer {
return None;
}
// let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
// let rename = editor.take_rename(false, cx)?;
// let buffer = editor.buffer.read(cx);
// let (start_buffer, start) =
// buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
// let (end_buffer, end) =
// buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
// if start_buffer == end_buffer {
// let new_name = rename.editor.read(cx).text(cx);
// Some((start_buffer, start..end, rename.old_name, new_name))
// } else {
// None
// }
// })?;
let buffer = start_buffer;
let range = start..end;
let old_name = rename.old_name;
let new_name = rename.editor.read(cx).text(cx);
// let rename = workspace.project().clone().update(cx, |project, cx| {
// project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
// });
let rename = workspace
.read(cx)
.project()
.clone()
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
});
let workspace = workspace.downgrade();
// let editor = editor.downgrade();
// Some(cx.spawn(|workspace, mut cx| async move {
// let project_transaction = rename.await?;
// Self::open_project_transaction(
// &editor,
// workspace,
// project_transaction,
// format!("Rename: {} → {}", old_name, new_name),
// cx.clone(),
// )
// .await?;
Some(cx.spawn(|editor, mut cx| async move {
let project_transaction = rename.await?;
Self::open_project_transaction(
&editor,
workspace,
project_transaction,
format!("Rename: {}{}", old_name, new_name),
cx.clone(),
)
.await?;
// editor.update(&mut cx, |editor, cx| {
// editor.refresh_document_highlights(cx);
// })?;
// Ok(())
// }))
// }
editor.update(&mut cx, |editor, cx| {
editor.refresh_document_highlights(cx);
})?;
Ok(())
}))
}
fn take_rename(
&mut self,
@ -7880,6 +7901,10 @@ impl Editor {
cx: &mut ViewContext<Self>,
) -> Option<RenameState> {
let rename = self.pending_rename.take()?;
if rename.editor.focus_handle(cx).is_focused(cx) {
cx.focus(&self.focus_handle);
}
self.remove_blocks(
[rename.block_id].into_iter().collect(),
Some(Autoscroll::fit()),
@ -8869,46 +8894,50 @@ impl Editor {
// });
// }
// fn jump(
// workspace: &mut Workspace,
// path: ProjectPath,
// position: Point,
// anchor: language::Anchor,
// cx: &mut ViewContext<Workspace>,
// ) {
// let editor = workspace.open_path(path, None, true, cx);
// cx.spawn(|_, mut cx| async move {
// let editor = editor
// .await?
// .downcast::<Editor>()
// .ok_or_else(|| anyhow!("opened item was not an editor"))?
// .downgrade();
// editor.update(&mut cx, |editor, cx| {
// let buffer = editor
// .buffer()
// .read(cx)
// .as_singleton()
// .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
// let buffer = buffer.read(cx);
// let cursor = if buffer.can_resolve(&anchor) {
// language::ToPoint::to_point(&anchor, buffer)
// } else {
// buffer.clip_point(position, Bias::Left)
// };
fn jump(
&mut self,
path: ProjectPath,
position: Point,
anchor: language::Anchor,
cx: &mut ViewContext<Self>,
) {
let workspace = self.workspace();
cx.spawn(|_, mut cx| async move {
let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?;
let editor = workspace.update(&mut cx, |workspace, cx| {
workspace.open_path(path, None, true, cx)
})?;
let editor = editor
.await?
.downcast::<Editor>()
.ok_or_else(|| anyhow!("opened item was not an editor"))?
.downgrade();
editor.update(&mut cx, |editor, cx| {
let buffer = editor
.buffer()
.read(cx)
.as_singleton()
.ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
let buffer = buffer.read(cx);
let cursor = if buffer.can_resolve(&anchor) {
language::ToPoint::to_point(&anchor, buffer)
} else {
buffer.clip_point(position, Bias::Left)
};
// let nav_history = editor.nav_history.take();
// editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
// s.select_ranges([cursor..cursor]);
// });
// editor.nav_history = nav_history;
let nav_history = editor.nav_history.take();
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
s.select_ranges([cursor..cursor]);
});
editor.nav_history = nav_history;
// anyhow::Ok(())
// })??;
anyhow::Ok(())
})??;
// anyhow::Ok(())
// })
// .detach_and_log_err(cx);
// }
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn marked_text_ranges(&self, cx: &AppContext) -> Option<Vec<Range<OffsetUtf16>>> {
let snapshot = self.buffer.read(cx).read(cx);
@ -9170,17 +9199,17 @@ impl Editor {
cx.focus(&self.focus_handle)
}
fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
if self.focus_handle.is_focused(cx) {
// todo!()
// let focused_event = EditorFocused(cx.handle());
// cx.emit_global(focused_event);
cx.emit(Event::Focused);
pub fn is_focused(&self, cx: &WindowContext) -> bool {
self.focus_handle.is_focused(cx)
}
fn handle_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Focused);
if let Some(rename) = self.pending_rename.as_ref() {
let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone();
cx.focus(&rename_editor_focus_handle);
} else if self.focus_handle.is_focused(cx) {
} else {
self.blink_manager.update(cx, BlinkManager::enable);
self.buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(cx);
@ -9196,7 +9225,7 @@ impl Editor {
}
}
fn handle_focus_out(&mut self, cx: &mut ViewContext<Self>) {
fn handle_blur(&mut self, cx: &mut ViewContext<Self>) {
// todo!()
// let blurred_event = EditorBlurred(cx.handle());
// cx.emit_global(blurred_event);
@ -9381,9 +9410,9 @@ impl Render for Editor {
EditorMode::SingleLine => {
TextStyle {
color: cx.theme().colors().text,
font_family: "Zed Sans".into(), // todo!()
font_features: FontFeatures::default(),
font_size: rems(1.0).into(),
font_family: settings.ui_font.family.clone(), // todo!()
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
@ -9411,14 +9440,17 @@ impl Render for Editor {
EditorMode::Full => cx.theme().colors().editor_background,
};
EditorElement::new(EditorStyle {
EditorElement::new(
cx.view(),
EditorStyle {
background,
local_player: cx.theme().players().local(),
text: text_style,
scrollbar_width: px(12.),
syntax: cx.theme().syntax().clone(),
diagnostic_style: cx.theme().diagnostic_style(),
})
},
)
}
}
@ -9973,43 +10005,22 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
}
let message = diagnostic.message;
Arc::new(move |cx: &mut BlockContext| {
todo!()
// let message = message.clone();
// let settings = ThemeSettings::get_global(cx);
// let tooltip_style = settings.theme.tooltip.clone();
// let theme = &settings.theme.editor;
// let style = diagnostic_style(diagnostic.severity, is_valid, theme);
// let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
// let anchor_x = cx.anchor_x;
// enum BlockContextToolip {}
// MouseEventHandler::new::<BlockContext, _>(cx.block_id, cx, |_, _| {
// Flex::column()
// .with_children(highlighted_lines.iter().map(|(line, highlights)| {
// Label::new(
// line.clone(),
// style.message.clone().with_font_size(font_size),
// )
// .with_highlights(highlights.clone())
// .contained()
// .with_margin_left(anchor_x)
// }))
// .aligned()
// .left()
// .into_any()
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, move |_, _, cx| {
// cx.write_to_clipboard(ClipboardItem::new(message.clone()));
// })
// // We really need to rethink this ID system...
// .with_tooltip::<BlockContextToolip>(
// cx.block_id,
// "Copy diagnostic message",
// None,
// tooltip_style,
// cx,
// )
// .into_any()
let message = message.clone();
v_stack()
.id(cx.block_id)
.size_full()
.bg(gpui::red())
.children(highlighted_lines.iter().map(|(line, highlights)| {
div()
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
.ml(cx.anchor_x)
}))
.cursor_pointer()
.on_click(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(message.clone()));
})
.tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message")))
.render()
})
}
@ -10056,76 +10067,76 @@ pub fn diagnostic_style(
}
}
// pub fn combine_syntax_and_fuzzy_match_highlights(
// text: &str,
// default_style: HighlightStyle,
// syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
// match_indices: &[usize],
// ) -> Vec<(Range<usize>, HighlightStyle)> {
// let mut result = Vec::new();
// let mut match_indices = match_indices.iter().copied().peekable();
pub fn combine_syntax_and_fuzzy_match_highlights(
text: &str,
default_style: HighlightStyle,
syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
match_indices: &[usize],
) -> Vec<(Range<usize>, HighlightStyle)> {
let mut result = Vec::new();
let mut match_indices = match_indices.iter().copied().peekable();
// for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
// {
// syntax_highlight.weight = None;
for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
{
syntax_highlight.font_weight = None;
// // Add highlights for any fuzzy match characters before the next
// // syntax highlight range.
// while let Some(&match_index) = match_indices.peek() {
// if match_index >= range.start {
// break;
// }
// match_indices.next();
// let end_index = char_ix_after(match_index, text);
// let mut match_style = default_style;
// match_style.weight = Some(FontWeight::BOLD);
// result.push((match_index..end_index, match_style));
// }
// Add highlights for any fuzzy match characters before the next
// syntax highlight range.
while let Some(&match_index) = match_indices.peek() {
if match_index >= range.start {
break;
}
match_indices.next();
let end_index = char_ix_after(match_index, text);
let mut match_style = default_style;
match_style.font_weight = Some(FontWeight::BOLD);
result.push((match_index..end_index, match_style));
}
// if range.start == usize::MAX {
// break;
// }
if range.start == usize::MAX {
break;
}
// // Add highlights for any fuzzy match characters within the
// // syntax highlight range.
// let mut offset = range.start;
// while let Some(&match_index) = match_indices.peek() {
// if match_index >= range.end {
// break;
// }
// Add highlights for any fuzzy match characters within the
// syntax highlight range.
let mut offset = range.start;
while let Some(&match_index) = match_indices.peek() {
if match_index >= range.end {
break;
}
// match_indices.next();
// if match_index > offset {
// result.push((offset..match_index, syntax_highlight));
// }
match_indices.next();
if match_index > offset {
result.push((offset..match_index, syntax_highlight));
}
// let mut end_index = char_ix_after(match_index, text);
// while let Some(&next_match_index) = match_indices.peek() {
// if next_match_index == end_index && next_match_index < range.end {
// end_index = char_ix_after(next_match_index, text);
// match_indices.next();
// } else {
// break;
// }
// }
let mut end_index = char_ix_after(match_index, text);
while let Some(&next_match_index) = match_indices.peek() {
if next_match_index == end_index && next_match_index < range.end {
end_index = char_ix_after(next_match_index, text);
match_indices.next();
} else {
break;
}
}
// let mut match_style = syntax_highlight;
// match_style.weight = Some(FontWeight::BOLD);
// result.push((match_index..end_index, match_style));
// offset = end_index;
// }
let mut match_style = syntax_highlight;
match_style.font_weight = Some(FontWeight::BOLD);
result.push((match_index..end_index, match_style));
offset = end_index;
}
// if offset < range.end {
// result.push((offset..range.end, syntax_highlight));
// }
// }
if offset < range.end {
result.push((offset..range.end, syntax_highlight));
}
}
// fn char_ix_after(ix: usize, text: &str) -> usize {
// ix + text[ix..].chars().next().unwrap().len_utf8()
// }
fn char_ix_after(ix: usize, text: &str) -> usize {
ix + text[ix..].chars().next().unwrap().len_utf8()
}
// result
// }
result
}
// pub fn styled_runs_for_code_label<'a>(
// label: &'a CodeLabel,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1220,8 +1220,6 @@ pub mod tests {
use super::*;
// 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| {
let worktree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
})
})
.unwrap();
});
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| {
let worktree_id = project.update(cx, |project, cx| {
project.worktrees().next().unwrap().read(cx).id()
})
})
.unwrap();
});
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| {

View file

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

View file

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

View file

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

View file

@ -1,81 +1,74 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use gpui::{Context, Model, Pixels, ViewContext};
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();
}
}
// 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 font = cx.text_style().font();
let font_size: Pixels = 14.into();
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)
}
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);
}
// 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!()
// use crate::{
// display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
// DisplayPoint, Editor, EditorMode, MultiBuffer,
// };
Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
}
// use gpui::{Model, ViewContext};
// 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();
// }
// }
// // 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 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();
// (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 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)
// }
// 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)
}

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,11 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use 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)),
),
)
}

View file

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

View file

@ -0,0 +1 @@

View file

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

View file

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

View file

@ -1,6 +1,6 @@
use crate::SharedString;
use 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())),
)
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,70 +35,52 @@ 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);
});
let style = self.base.compute_style(bounds, element_state, cx);
self.interactivity.paint(
bounds,
bounds.size,
element_state,
cx,
|style, _scroll_offset, cx| {
let corner_radii = style.corner_radii;
if let Some(uri) = self.uri.clone() {
@ -131,56 +106,19 @@ where
.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
}
}

View file

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

View file

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

View file

@ -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,72 @@ impl<V: 'static> Element<V> for UniformList<V> {
let item_size = element_state.item_size;
let rem_size = cx.rem_size();
self.interactivity
.layout(&mut element_state.interactive, cx, |style, cx| {
cx.request_measured_layout(
self.computed_style(),
style,
rem_size,
move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| {
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,
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,
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());
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),
);
let item_size = element_state.item_size;
let content_size = Size {
width: padded_bounds.size.width,
height: item_size.height * 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();
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());
@ -173,7 +217,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
style.paint(bounds, cx);
let content_size;
if self.item_count > 0 {
let item_height = self
.measure_item(view_state, Some(padded_bounds.size.width), cx)
@ -182,7 +225,7 @@ impl<V: 'static> Element<V> for UniformList<V> {
scroll_handle.0.lock().replace(ScrollHandleState {
item_height,
list_height: padded_bounds.size.height,
scroll_offset: element_state.interactive.track_scroll_offset(),
scroll_offset: shared_scroll_offset,
});
}
let visible_item_count = if item_height > px(0.) {
@ -190,11 +233,9 @@ impl<V: 'static> Element<V> for UniformList<V> {
} 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 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,
@ -202,16 +243,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
);
let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
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 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),
@ -219,25 +254,11 @@ impl<V: 'static> Element<V> for UniformList<V> {
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,
);
});
})
},
);
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
}
}

View file

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

View file

@ -6,13 +6,14 @@ mod color;
mod element;
mod 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,
{
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) {

View file

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

View file

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

View file

@ -1,11 +1,21 @@
use crate::{Action, DispatchContext, DispatchContextPredicate, KeyMatch, Keystroke};
use crate::{Action, KeyBindingContextPredicate, KeyContext, KeyMatch, Keystroke};
use anyhow::Result;
use 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())

View file

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

View file

@ -1,4 +1,4 @@
use crate::{DispatchContextPredicate, KeyBinding, Keystroke};
use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
use collections::HashSet;
use 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,
}

View file

@ -1,15 +1,15 @@
use crate::{Action, DispatchContext, Keymap, KeymapVersion, Keystroke};
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
use parking_lot::Mutex;
use 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,

View file

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

View file

@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
fn 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,

View file

@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem {
},
)?;
Ok(candidates[ix])
let font_id = candidates[ix];
lock.font_selections.insert(font.clone(), font_id);
Ok(font_id)
}
}
@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem {
fn rasterize_glyph(
&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;

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
use crate::{
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.

View file

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

View file

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

View file

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

View file

@ -1,16 +1,26 @@
#[cfg(any(test, feature = "test-support"))]
use std::time::Duration;
#[cfg(any(test, feature = "test-support"))]
use futures::Future;
#[cfg(any(test, feature = "test-support"))]
use smol::future::FutureExt;
pub use util::*;
// pub 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);

View file

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

View file

@ -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) => {
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! {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,17 +98,41 @@ 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>) {
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>) {
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(
&mut self,
@ -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,7 +196,8 @@ impl<D: PickerDelegate> Render for Picker<D> {
.child(div().px_1().py_0p5().child(self.editor.clone())),
)
.child(Divider::horizontal())
.child(
.when(self.delegate.match_count() > 0, |el| {
el.child(
v_stack()
.p_1()
.grow()
@ -168,7 +206,24 @@ impl<D: PickerDelegate> Render for Picker<D> {
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))
.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()
}
})
@ -177,5 +232,15 @@ impl<D: PickerDelegate> Render for Picker<D> {
.max_h_72()
.overflow_hidden(),
)
})
.when(self.delegate.match_count() == 0, |el| {
el.child(
v_stack().p_1().grow().child(
div()
.px_1()
.child(Label::new("No matches").color(TextColor::Muted)),
),
)
})
}
}

View file

@ -497,7 +497,7 @@ mod tests {
#[gpui::test]
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!({

View file

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

View file

@ -89,7 +89,7 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
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!({

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -577,18 +577,18 @@ mod tests {
let client2 = Peer::new(0);
let (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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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