Detailed changes
@@ -1095,6 +1095,23 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "breadcrumbs2"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "editor2",
+ "gpui2",
+ "itertools 0.10.5",
+ "language2",
+ "project2",
+ "search2",
+ "settings2",
+ "theme2",
+ "ui2",
+ "workspace2",
+]
+
[[package]]
name = "bromberg_sl2"
version = "0.6.0"
@@ -1205,7 +1222,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
- "async-trait",
"audio2",
"client2",
"collections",
@@ -1225,9 +1241,7 @@ dependencies = [
"serde_json",
"settings2",
"smallvec",
- "ui2",
"util",
- "workspace2",
]
[[package]]
@@ -1688,7 +1702,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.29.0"
+version = "0.29.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2098,7 +2112,7 @@ dependencies = [
"lsp2",
"node_runtime",
"parking_lot 0.11.2",
- "rpc",
+ "rpc2",
"serde",
"serde_derive",
"settings2",
@@ -2126,6 +2140,25 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "copilot_button2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "copilot2",
+ "editor2",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "settings2",
+ "smol",
+ "theme2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -4805,6 +4838,24 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "language_selector2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "picker2",
+ "project2",
+ "settings2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "language_tools"
version = "0.1.0"
@@ -6141,6 +6192,26 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "outline2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "ordered-float 2.10.0",
+ "picker2",
+ "postage",
+ "settings2",
+ "smol",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "overload"
version = "0.1.1"
@@ -8192,6 +8263,57 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "semantic_index2"
+version = "0.1.0"
+dependencies = [
+ "ai2",
+ "anyhow",
+ "async-trait",
+ "client2",
+ "collections",
+ "ctor",
+ "env_logger 0.9.3",
+ "futures 0.3.28",
+ "globset",
+ "gpui2",
+ "language2",
+ "lazy_static",
+ "log",
+ "ndarray",
+ "node_runtime",
+ "ordered-float 2.10.0",
+ "parking_lot 0.11.2",
+ "postage",
+ "pretty_assertions",
+ "project2",
+ "rand 0.8.5",
+ "rpc2",
+ "rusqlite",
+ "rust-embed",
+ "schemars",
+ "serde",
+ "serde_json",
+ "settings2",
+ "sha1",
+ "smol",
+ "tempdir",
+ "tiktoken-rs",
+ "tree-sitter",
+ "tree-sitter-cpp",
+ "tree-sitter-elixir",
+ "tree-sitter-json 0.20.0",
+ "tree-sitter-lua",
+ "tree-sitter-php",
+ "tree-sitter-ruby",
+ "tree-sitter-rust",
+ "tree-sitter-toml",
+ "tree-sitter-typescript",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "semver"
version = "1.0.18"
@@ -11491,7 +11613,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
- "async-trait",
"bincode",
"call2",
"client2",
@@ -11757,6 +11878,7 @@ dependencies = [
"audio2",
"auto_update2",
"backtrace",
+ "breadcrumbs2",
"call2",
"channel2",
"chrono",
@@ -11766,6 +11888,7 @@ dependencies = [
"collections",
"command_palette2",
"copilot2",
+ "copilot_button2",
"ctor",
"db2",
"diagnostics2",
@@ -11786,6 +11909,7 @@ dependencies = [
"isahc",
"journal2",
"language2",
+ "language_selector2",
"lazy_static",
"libc",
"log",
@@ -11793,6 +11917,7 @@ dependencies = [
"menu2",
"node_runtime",
"num_cpus",
+ "outline2",
"parking_lot 0.11.2",
"postage",
"project2",
@@ -9,6 +9,7 @@ members = [
"crates/auto_update",
"crates/auto_update2",
"crates/breadcrumbs",
+ "crates/breadcrumbs2",
"crates/call",
"crates/call2",
"crates/channel",
@@ -60,6 +61,7 @@ members = [
"crates/language",
"crates/language2",
"crates/language_selector",
+ "crates/language_selector2",
"crates/language_tools",
"crates/live_kit_client",
"crates/live_kit_server",
@@ -74,6 +76,7 @@ members = [
"crates/notifications",
"crates/notifications2",
"crates/outline",
+ "crates/outline2",
"crates/picker",
"crates/picker2",
"crates/plugin",
@@ -92,6 +95,8 @@ members = [
"crates/rpc2",
"crates/search",
"crates/search2",
+ "crates/semantic_index",
+ "crates/semantic_index2",
"crates/settings",
"crates/settings2",
"crates/snippet",
@@ -111,7 +116,6 @@ members = [
"crates/theme_selector2",
"crates/ui2",
"crates/util",
- "crates/semantic_index",
"crates/story",
"crates/vim",
"crates/vcs_menu",
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
@@ -530,12 +530,17 @@
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
- "space": "project_panel::Open",
"backspace": "project_panel::Delete",
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
+ {
+ "context": "ProjectPanel && not_editing",
+ "bindings": {
+ "space": "project_panel::Open"
+ }
+ },
{
"context": "CollabPanel && not_editing",
"bindings": {
@@ -7,7 +7,7 @@ pub enum ProviderCredential {
NotNeeded,
}
-pub trait CredentialProvider {
+pub trait CredentialProvider: Send + Sync {
fn has_credentials(&self) -> bool;
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
model: OpenAILanguageModel,
credential: Arc<RwLock<ProviderCredential>>,
pub client: Arc<dyn HttpClient>,
- pub executor: Arc<BackgroundExecutor>,
+ pub executor: BackgroundExecutor,
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
}
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
}
impl OpenAIEmbeddingProvider {
- pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
+ pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
@@ -1218,6 +1218,31 @@ impl View for AssistantPanel {
let style = &theme.assistant;
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
Flex::column()
+ .with_child(
+ Text::new(
+ "To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
+ style.api_key_prompt.text.clone(),
+ ),
+ )
+ .with_child(
+ Text::new(
+ " - Having a subscription for another service like GitHub Copilot won't work.",
+ style.api_key_prompt.text.clone(),
+ ),
+ )
+ .with_child(
+ Text::new(
+ " - You can create a api key at: platform.openai.com/api-keys",
+ style.api_key_prompt.text.clone(),
+ ),
+ )
+ .with_child(
+ Text::new(
+ " ",
+ style.api_key_prompt.text.clone(),
+ )
+ .aligned(),
+ )
.with_child(
Text::new(
"Paste your OpenAI API key and press Enter to use the assistant",
@@ -1231,6 +1256,20 @@ impl View for AssistantPanel {
.with_style(style.api_key_editor.container)
.aligned(),
)
+ .with_child(
+ Text::new(
+ " ",
+ style.api_key_prompt.text.clone(),
+ )
+ .aligned(),
+ )
+ .with_child(
+ Text::new(
+ "Click on the Z button in the status bar to close this panel.",
+ style.api_key_prompt.text.clone(),
+ )
+ .aligned(),
+ )
.contained()
.with_style(style.api_key_prompt.container)
.aligned()
@@ -102,7 +102,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
})
.detach();
- if let Some(version) = *ZED_APP_VERSION {
+ if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
let auto_updater = cx.build_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url);
@@ -0,0 +1,28 @@
+[package]
+name = "breadcrumbs2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/breadcrumbs.rs"
+doctest = false
+
+[dependencies]
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+project = { package = "project2", path = "../project2" }
+search = { package = "search2", path = "../search2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+# outline = { path = "../outline" }
+itertools = "0.10"
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
@@ -0,0 +1,204 @@
+use gpui::{
+ Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
+ ViewContext, WeakView,
+};
+use itertools::Itertools;
+use theme::ActiveTheme;
+use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label};
+use workspace::{
+ item::{ItemEvent, ItemHandle},
+ ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+pub enum Event {
+ UpdateLocation,
+}
+
+pub struct Breadcrumbs {
+ pane_focused: bool,
+ active_item: Option<Box<dyn ItemHandle>>,
+ subscription: Option<Subscription>,
+ _workspace: WeakView<Workspace>,
+}
+
+impl Breadcrumbs {
+ pub fn new(workspace: &Workspace) -> Self {
+ Self {
+ pane_focused: false,
+ active_item: Default::default(),
+ subscription: Default::default(),
+ _workspace: workspace.weak_handle(),
+ }
+ }
+}
+
+impl EventEmitter<Event> for Breadcrumbs {}
+impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
+
+impl Render for Breadcrumbs {
+ type Element = Component<ButtonLike>;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let button = ButtonLike::new("breadcrumbs")
+ .style(ButtonStyle::Transparent)
+ .disabled(true);
+
+ let active_item = match &self.active_item {
+ Some(active_item) => active_item,
+ None => return button.into_element(),
+ };
+ let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
+ let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) {
+ Some(breadcrumbs) => breadcrumbs,
+ None => return button.into_element(),
+ }
+ .into_iter()
+ .map(|breadcrumb| {
+ StyledText::new(breadcrumb.text)
+ .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default())
+ .into_any()
+ });
+
+ let button = button.children(Itertools::intersperse_with(breadcrumbs, || {
+ Label::new(" › ").into_any_element()
+ }));
+
+ if not_editor || !self.pane_focused {
+ return button.into_element();
+ }
+
+ // let this = cx.view().downgrade();
+ button
+ .style(ButtonStyle::Filled)
+ .disabled(false)
+ .on_click(move |_, _cx| {
+ todo!("outline::toggle");
+ // this.update(cx, |this, cx| {
+ // if let Some(workspace) = this.workspace.upgrade() {
+ // workspace.update(cx, |_workspace, _cx| {
+ // outline::toggle(workspace, &Default::default(), cx)
+ // })
+ // }
+ // })
+ // .ok();
+ })
+ .into_element()
+ }
+}
+
+// impl View for Breadcrumbs {
+// fn ui_name() -> &'static str {
+// "Breadcrumbs"
+// }
+
+// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+// let active_item = match &self.active_item {
+// Some(active_item) => active_item,
+// None => return Empty::new().into_any(),
+// };
+// let not_editor = active_item.downcast::<editor::Editor>().is_none();
+
+// let theme = theme::current(cx).clone();
+// let style = &theme.workspace.toolbar.breadcrumbs;
+
+// let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
+// Some(breadcrumbs) => breadcrumbs,
+// None => return Empty::new().into_any(),
+// }
+// .into_iter()
+// .map(|breadcrumb| {
+// Text::new(
+// breadcrumb.text,
+// theme.workspace.toolbar.breadcrumbs.default.text.clone(),
+// )
+// .with_highlights(breadcrumb.highlights.unwrap_or_default())
+// .into_any()
+// });
+
+// let crumbs = Flex::row()
+// .with_children(Itertools::intersperse_with(breadcrumbs, || {
+// Label::new(" › ", style.default.text.clone()).into_any()
+// }))
+// .constrained()
+// .with_height(theme.workspace.toolbar.breadcrumb_height)
+// .contained();
+
+// if not_editor || !self.pane_focused {
+// return crumbs
+// .with_style(style.default.container)
+// .aligned()
+// .left()
+// .into_any();
+// }
+
+// MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
+// let style = style.style_for(state);
+// crumbs.with_style(style.container)
+// })
+// .on_click(MouseButton::Left, |_, this, cx| {
+// if let Some(workspace) = this.workspace.upgrade(cx) {
+// workspace.update(cx, |workspace, cx| {
+// outline::toggle(workspace, &Default::default(), cx)
+// })
+// }
+// })
+// .with_tooltip::<Breadcrumbs>(
+// 0,
+// "Show symbol outline".to_owned(),
+// Some(Box::new(outline::Toggle)),
+// theme.tooltip.clone(),
+// cx,
+// )
+// .aligned()
+// .left()
+// .into_any()
+// }
+// }
+
+impl ToolbarItemView for Breadcrumbs {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ cx.notify();
+ self.active_item = None;
+ if let Some(item) = active_pane_item {
+ let this = cx.view().downgrade();
+ self.subscription = Some(item.subscribe_to_item_events(
+ cx,
+ Box::new(move |event, cx| {
+ if let ItemEvent::UpdateBreadcrumbs = event {
+ this.update(cx, |_, cx| {
+ cx.emit(Event::UpdateLocation);
+ cx.notify();
+ })
+ .ok();
+ }
+ }),
+ ));
+ self.active_item = Some(item.boxed_clone());
+ item.breadcrumb_location(cx)
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+
+ // fn location_for_event(
+ // &self,
+ // _: &Event,
+ // current_location: ToolbarItemLocation,
+ // cx: &AppContext,
+ // ) -> ToolbarItemLocation {
+ // if let Some(active_item) = self.active_item.as_ref() {
+ // active_item.breadcrumb_location(cx)
+ // } else {
+ // current_location
+ // }
+ // }
+
+ fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
+ self.pane_focused = pane_focused;
+ }
+}
@@ -31,9 +31,7 @@ media = { path = "../media" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
util = { path = "../util" }
-ui = {package = "ui2", path = "../ui2"}
-workspace = {package = "workspace2", path = "../workspace2"}
-async-trait.workspace = true
+
anyhow.workspace = true
async-broadcast = "0.4"
futures.workspace = true
@@ -1,32 +1,25 @@
pub mod call_settings;
pub mod participant;
pub mod room;
-mod shared_screen;
use anyhow::{anyhow, Result};
-use async_trait::async_trait;
use audio::Audio;
use call_settings::CallSettings;
-use client::{
- proto::{self, PeerId},
- Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
-};
+use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
- AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
- Subscription, Task, View, ViewContext, VisualContext, WeakModel, WindowHandle,
+ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
+ WeakModel,
};
-pub use participant::ParticipantLocation;
use postage::watch;
use project::Project;
use room::Event;
-pub use room::Room;
use settings::Settings;
-use shared_screen::SharedScreen;
use std::sync::Arc;
-use util::ResultExt;
-use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
+
+pub use participant::ParticipantLocation;
+pub use room::Room;
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
@@ -334,55 +327,12 @@ impl ActiveCall {
pub fn join_channel(
&mut self,
channel_id: u64,
- requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
- return cx.spawn(|_, _| async move {
- todo!();
- // let future = room.update(&mut cx, |room, cx| {
- // room.most_active_project(cx).map(|(host, project)| {
- // room.join_project(project, host, app_state.clone(), cx)
- // })
- // })
-
- // if let Some(future) = future {
- // future.await?;
- // }
-
- // Ok(Some(room))
- });
- }
-
- let should_prompt = room.update(cx, |room, _| {
- room.channel_id().is_some()
- && room.is_sharing_project()
- && room.remote_participants().len() > 0
- });
- if should_prompt && requesting_window.is_some() {
- return cx.spawn(|this, mut cx| async move {
- let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Leaving this call will unshare your current project.\nDo you want to switch channels?",
- &["Yes, Join Channel", "Cancel"],
- )
- })?;
- if answer.await? == 1 {
- return Ok(None);
- }
-
- room.update(&mut cx, |room, cx| room.clear_state(cx))?;
-
- this.update(&mut cx, |this, cx| {
- this.join_channel(channel_id, requesting_window, cx)
- })?
- .await
- });
- }
-
- if room.read(cx).channel_id().is_some() {
+ return Task::ready(Ok(Some(room)));
+ } else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
@@ -555,197 +505,6 @@ pub fn report_call_event_for_channel(
)
}
-pub struct Call {
- active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
-}
-
-impl Call {
- pub fn new(cx: &mut ViewContext<'_, Workspace>) -> Box<dyn CallHandler> {
- let mut active_call = None;
- if cx.has_global::<Model<ActiveCall>>() {
- let call = cx.global::<Model<ActiveCall>>().clone();
- let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
- active_call = Some((call, subscriptions));
- }
- Box::new(Self { active_call })
- }
- fn on_active_call_event(
- workspace: &mut Workspace,
- _: Model<ActiveCall>,
- event: &room::Event,
- cx: &mut ViewContext<Workspace>,
- ) {
- match event {
- room::Event::ParticipantLocationChanged { participant_id }
- | room::Event::RemoteVideoTracksChanged { participant_id } => {
- workspace.leader_updated(*participant_id, cx);
- }
- _ => {}
- }
- }
-}
-
-#[async_trait(?Send)]
-impl CallHandler for Call {
- fn peer_state(
- &mut self,
- leader_id: PeerId,
- project: &Model<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(bool, bool)> {
- let (call, _) = self.active_call.as_ref()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(leader_id)?;
-
- let leader_in_this_app;
- let leader_in_this_project;
- match participant.location {
- ParticipantLocation::SharedProject { project_id } => {
- leader_in_this_app = true;
- leader_in_this_project = Some(project_id) == project.read(cx).remote_id();
- }
- ParticipantLocation::UnsharedProject => {
- leader_in_this_app = true;
- leader_in_this_project = false;
- }
- ParticipantLocation::External => {
- leader_in_this_app = false;
- leader_in_this_project = false;
- }
- };
-
- Some((leader_in_this_project, leader_in_this_app))
- }
-
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- let (call, _) = self.active_call.as_ref()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(peer_id)?;
- let track = participant.video_tracks.values().next()?.clone();
- let user = participant.user.clone();
- for item in pane.read(cx).items_of_type::<SharedScreen>() {
- if item.read(cx).peer_id == peer_id {
- return Some(Box::new(item));
- }
- }
-
- Some(Box::new(cx.build_view(|cx| {
- SharedScreen::new(&track, peer_id, user.clone(), cx)
- })))
- }
- fn room_id(&self, cx: &AppContext) -> Option<u64> {
- Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
- }
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
- let Some((call, _)) = self.active_call.as_ref() else {
- return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
- };
-
- call.update(cx, |this, cx| this.hang_up(cx))
- }
- fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
- ActiveCall::global(cx).read(cx).location().cloned()
- }
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>> {
- ActiveCall::global(cx).update(cx, |this, cx| {
- this.invite(called_user_id, initial_project, cx)
- })
- }
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
- self.active_call
- .as_ref()
- .map(|call| {
- call.0.read(cx).room().map(|room| {
- room.read(cx)
- .remote_participants()
- .iter()
- .map(|participant| {
- (participant.1.user.clone(), participant.1.peer_id.clone())
- })
- .collect()
- })
- })
- .flatten()
- }
- fn is_muted(&self, cx: &AppContext) -> Option<bool> {
- self.active_call
- .as_ref()
- .map(|call| {
- call.0
- .read(cx)
- .room()
- .map(|room| room.read(cx).is_muted(cx))
- })
- .flatten()
- }
- fn toggle_mute(&self, cx: &mut AppContext) {
- self.active_call.as_ref().map(|call| {
- call.0.update(cx, |this, cx| {
- this.room().map(|room| {
- let room = room.clone();
- cx.spawn(|_, mut cx| async move {
- room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
- .await
- })
- .detach_and_log_err(cx);
- })
- })
- });
- }
- fn toggle_screen_share(&self, cx: &mut AppContext) {
- self.active_call.as_ref().map(|call| {
- call.0.update(cx, |this, cx| {
- this.room().map(|room| {
- room.update(cx, |this, cx| {
- if this.is_screen_sharing() {
- this.unshare_screen(cx).log_err();
- } else {
- let t = this.share_screen(cx);
- cx.spawn(move |_, _| async move {
- t.await.log_err();
- })
- .detach();
- }
- })
- })
- })
- });
- }
- fn toggle_deafen(&self, cx: &mut AppContext) {
- self.active_call.as_ref().map(|call| {
- call.0.update(cx, |this, cx| {
- this.room().map(|room| {
- room.update(cx, |this, cx| {
- this.toggle_deafen(cx).log_err();
- })
- })
- })
- });
- }
- fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
- self.active_call
- .as_ref()
- .map(|call| {
- call.0
- .read(cx)
- .room()
- .map(|room| room.read(cx).is_deafened())
- })
- .flatten()
- .flatten()
- }
-}
-
#[cfg(test)]
mod test {
use gpui::TestAppContext;
@@ -4,7 +4,7 @@ use client::{proto, User};
use collections::HashMap;
use gpui::WeakModel;
pub use live_kit_client::Frame;
-pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
+pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
@@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
}
}
-#[derive(Copy, Clone)]
+#[derive(Debug, Copy, Clone)]
pub struct TelemetrySettings {
pub diagnostics: bool,
pub metrics: bool,
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.29.0"
+version = "0.29.1"
publish = false
[[bin]]
@@ -1220,6 +1220,13 @@ impl Database {
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
.await?;
+ if new_parent
+ .ancestors_including_self()
+ .any(|id| id == channel.id)
+ {
+ Err(anyhow!("cannot move a channel into one of its descendants"))?;
+ }
+
new_parent_path = new_parent.path();
new_parent_channel = Some(new_parent);
} else {
@@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
(livestreaming_id, &[projects_id]),
],
);
+
+ // Can't move a channel into its ancestor
+ db.move_channel(projects_id, Some(livestreaming_id), user_id)
+ .await
+ .unwrap_err();
+ let result = db.get_channels_for_user(user_id).await.unwrap();
+ assert_channel_tree(
+ result.channels,
+ &[
+ (zed_id, &[]),
+ (projects_id, &[]),
+ (livestreaming_id, &[projects_id]),
+ ],
+ );
}
test_both_dbs!(
@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
use live_kit_client::MacOSDisplay;
+use project::project_settings::ProjectSettings;
use rpc::proto::PeerId;
use serde_json::json;
+use settings::SettingsStore;
use std::{borrow::Cow, sync::Arc};
use workspace::{
dock::{test::TestPanel, DockPosition},
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
});
}
+#[gpui::test]
+async fn test_following_into_excluded_file(
+ deterministic: Arc<Deterministic>,
+ mut cx_a: &mut TestAppContext,
+ mut cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ for cx in [&mut cx_a, &mut cx_b] {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+ });
+ });
+ });
+ }
+ 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);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ ".git": {
+ "COMMIT_EDITMSG": "write your commit message here",
+ },
+ "1.txt": "one\none\none",
+ "2.txt": "two\ntwo\ntwo",
+ "3.txt": "three\nthree\nthree",
+ }),
+ )
+ .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 window_a = client_a.build_workspace(&project_a, cx_a);
+ let workspace_a = window_a.root(cx_a);
+ let peer_id_a = client_a.peer_id().unwrap();
+ let window_b = client_b.build_workspace(&project_b, cx_b);
+ let workspace_b = window_b.root(cx_b);
+
+ // Client A opens editors for a regular file and an excluded file.
+ let editor_for_regular = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let editor_for_excluded_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client A updates their selections in those editors
+ editor_for_regular.update(cx_a, |editor, cx| {
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+ });
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ editor.select_all(&Default::default(), cx);
+ editor.handle_input("new commit message", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+ });
+
+ // When client B starts following client A, currently visible file is replicated
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.follow(peer_id_a, cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+ Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+ );
+ assert_eq!(
+ editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![18..17]
+ );
+
+ // Changes from B to the excluded file are replicated in A's editor
+ editor_for_excluded_b.update(cx_b, |editor, cx| {
+ editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+ });
+ deterministic.run_until_parked();
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ "new commit messag\nCo-Authored-By: B <b@b.b>"
+ );
+ });
+}
+
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
let entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "c.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "c.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
.update(cx_b, |project, cx| {
project.rename_entry(entry.id, Path::new("d.txt"), cx)
})
- .unwrap()
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
let dir_entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/e.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
})
.await
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
.unwrap();
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .copy_entry(entry.id, Path::new("f.txt"), cx)
- .unwrap()
+ project.copy_entry(entry.id, Path::new("f.txt"), cx)
})
.await
.unwrap();
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
- .unwrap()
.await?;
}
@@ -1220,6 +1220,13 @@ impl Database {
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
.await?;
+ if new_parent
+ .ancestors_including_self()
+ .any(|id| id == channel.id)
+ {
+ Err(anyhow!("cannot move a channel into one of its descendants"))?;
+ }
+
new_parent_path = new_parent.path();
new_parent_channel = Some(new_parent);
} else {
@@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
.await
.unwrap();
- // Dag is: zed - projects - livestreaming
-
// Move to same parent should be a no-op
assert!(db
.move_channel(projects_id, Some(zed_id), user_id)
@@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
(livestreaming_id, &[projects_id]),
],
);
+
+ // Can't move a channel into its ancestor
+ db.move_channel(projects_id, Some(livestreaming_id), user_id)
+ .await
+ .unwrap_err();
+ let result = db.get_channels_for_user(user_id).await.unwrap();
+ assert_channel_tree(
+ result.channels,
+ &[
+ (zed_id, &[]),
+ (projects_id, &[]),
+ (livestreaming_id, &[projects_id]),
+ ],
+ );
}
test_both_dbs!(
@@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member(
let active_call_b = cx_b.read(ActiveCall::global);
assert!(active_call_b
- .update(cx_b, |active_call, cx| active_call
- .join_channel(sub_id, None, cx))
+ .update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
.await
.is_ok());
}
@@ -395,9 +394,7 @@ async fn test_channel_room(
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
- .update(cx_a, |active_call, cx| {
- active_call.join_channel(zed_id, None, cx)
- })
+ .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
@@ -445,9 +442,7 @@ async fn test_channel_room(
});
active_call_b
- .update(cx_b, |active_call, cx| {
- active_call.join_channel(zed_id, None, cx)
- })
+ .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
@@ -564,16 +559,12 @@ async fn test_channel_room(
});
active_call_a
- .update(cx_a, |active_call, cx| {
- active_call.join_channel(zed_id, None, cx)
- })
+ .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
active_call_b
- .update(cx_b, |active_call, cx| {
- active_call.join_channel(zed_id, None, cx)
- })
+ .update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
@@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
let active_call_a = cx_a.read(ActiveCall::global);
active_call_a
- .update(cx_a, |active_call, cx| {
- active_call.join_channel(zed_id, None, cx)
- })
+ .update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
.await
.unwrap();
@@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
active_call_a
.update(cx_a, |active_call, cx| {
- active_call.join_channel(rust_id, None, cx)
+ active_call.join_channel(rust_id, cx)
})
.await
.unwrap();
@@ -804,7 +793,7 @@ async fn test_call_from_channel(
let active_call_b = cx_b.read(ActiveCall::global);
active_call_a
- .update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
+ .update(cx_a, |call, cx| call.join_channel(channel_id, cx))
.await
.unwrap();
@@ -1297,7 +1286,7 @@ async fn test_guest_access(
// Non-members should not be allowed to join
assert!(active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+ .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.await
.is_err());
@@ -1319,7 +1308,7 @@ async fn test_guest_access(
// Client B joins channel A as a guest
active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
+ .update(cx_b, |call, cx| call.join_channel(channel_a, cx))
.await
.unwrap();
@@ -1352,7 +1341,7 @@ async fn test_guest_access(
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
+ .update(cx_b, |call, cx| call.join_channel(channel_b, cx))
.await
.unwrap();
@@ -1383,7 +1372,7 @@ async fn test_invite_access(
// should not be allowed to join
assert!(active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+ .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.is_err());
@@ -1401,7 +1390,7 @@ async fn test_invite_access(
.unwrap();
active_call_b
- .update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
+ .update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
.await
.unwrap();
@@ -4,10 +4,12 @@
// use call::ActiveCall;
// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
// use editor::{Editor, ExcerptRange, MultiBuffer};
-// use gpui::{BackgroundExecutor, TestAppContext, View};
+// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
// use live_kit_client::MacOSDisplay;
+// use project::project_settings::ProjectSettings;
// use rpc::proto::PeerId;
// use serde_json::json;
+// use settings::SettingsStore;
// use std::borrow::Cow;
// use workspace::{
// dock::{test::TestPanel, DockPosition},
@@ -24,7 +26,7 @@
// cx_c: &mut TestAppContext,
// cx_d: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).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;
@@ -71,12 +73,22 @@
// .unwrap();
// let window_a = client_a.build_workspace(&project_a, cx_a);
-// let workspace_a = window_a.root(cx_a);
+// let workspace_a = window_a.root(cx_a).unwrap();
// let window_b = client_b.build_workspace(&project_b, cx_b);
-// let workspace_b = window_b.root(cx_b);
+// let workspace_b = window_b.root(cx_b).unwrap();
+
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
+// let cx_c = &mut cx_c;
+// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
+// let cx_d = &mut cx_d;
// // Client A opens some editors.
-// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
// let editor_a1 = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -132,8 +144,8 @@
// .await
// .unwrap();
-// cx_c.foreground().run_until_parked();
-// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+// cx_c.executor().run_until_parked();
+// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -145,19 +157,19 @@
// Some((worktree_id, "2.txt").into())
// );
// assert_eq!(
-// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
// vec![2..1]
// );
// assert_eq!(
-// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
// vec![3..2]
// );
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// let active_call_c = cx_c.read(ActiveCall::global);
// let project_c = client_c.build_remote_project(project_id, cx_c).await;
// let window_c = client_c.build_workspace(&project_c, cx_c);
-// let workspace_c = window_c.root(cx_c);
+// let workspace_c = window_c.root(cx_c).unwrap();
// active_call_c
// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
// .await
@@ -172,10 +184,13 @@
// .await
// .unwrap();
-// cx_d.foreground().run_until_parked();
+// cx_d.executor().run_until_parked();
// let active_call_d = cx_d.read(ActiveCall::global);
// let project_d = client_d.build_remote_project(project_id, cx_d).await;
-// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
+// let workspace_d = client_d
+// .build_workspace(&project_d, cx_d)
+// .root(cx_d)
+// .unwrap();
// active_call_d
// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
// .await
@@ -183,7 +198,7 @@
// drop(project_d);
// // All clients see that clients B and C are following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -198,7 +213,7 @@
// });
// // All clients see that clients B is following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -216,7 +231,7 @@
// .unwrap();
// // All clients see that clients B and C are following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -240,7 +255,7 @@
// .unwrap();
// // All clients see that D is following C
-// cx_d.foreground().run_until_parked();
+// cx_d.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -257,7 +272,7 @@
// cx_c.drop_last(workspace_c);
// // Clients A and B see that client B is following A, and client C is not present in the followers.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -271,12 +286,15 @@
// workspace.activate_item(&editor_a1, cx)
// });
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// // When client A opens a multibuffer, client B does so as well.
-// let multibuffer_a = cx_a.add_model(|cx| {
+// let multibuffer_a = cx_a.build_model(|cx| {
// let buffer_a1 = project_a.update(cx, |project, cx| {
// project
// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
@@ -308,12 +326,12 @@
// });
// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
// let editor =
-// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
// workspace.add_item(Box::new(editor.clone()), cx);
// editor
// });
// executor.run_until_parked();
-// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -321,8 +339,8 @@
// .unwrap()
// });
// assert_eq!(
-// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
-// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
+// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
// );
// // When client A navigates back and forth, client B does so as well.
@@ -333,8 +351,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// workspace_a
@@ -344,8 +365,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b2.item_id()
+// );
// });
// workspace_a
@@ -355,8 +379,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// // Changes to client A's editor are reflected on client B.
@@ -364,20 +391,20 @@
// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
// });
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| {
+// editor_b1.update(cx_b, |editor, cx| {
// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
// });
// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
// editor_a1.update(cx_a, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-// editor.set_scroll_position(vec2f(0., 100.), cx);
+// editor.set_scroll_position(point(0., 100.), cx);
// });
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| {
+// editor_b1.update(cx_b, |editor, cx| {
// assert_eq!(editor.selections.ranges(cx), &[3..3]);
// });
@@ -390,11 +417,11 @@
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, cx| workspace
+// workspace_b.update(cx_b, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// editor_b1.id()
+// .item_id()),
+// editor_b1.item_id()
// );
// // Client A starts following client B.
@@ -405,15 +432,15 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
// Some(peer_id_b)
// );
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// editor_a1.id()
+// .item_id()),
+// editor_a1.item_id()
// );
// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
@@ -432,7 +459,7 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
// workspace
// .active_item(cx)
// .expect("no active item")
@@ -446,8 +473,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+// workspace_a.update(cx_a, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_a1.item_id()
+// )
// });
// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
@@ -455,26 +485,26 @@
// workspace.activate_item(&multibuffer_editor_b, cx)
// });
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
+// workspace_a.update(cx_a, |workspace, cx| {
// assert_eq!(
-// workspace.active_item(cx).unwrap().id(),
-// multibuffer_editor_a.id()
+// workspace.active_item(cx).unwrap().item_id(),
+// multibuffer_editor_a.item_id()
// )
// });
// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
+// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
// workspace_b.update(cx_b, |workspace, cx| {
// workspace.add_panel(panel, cx);
// workspace.toggle_panel_focus::<TestPanel>(cx);
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// shared_screen.id()
+// .item_id()),
+// shared_screen.item_id()
// );
// // Toggling the focus back to the pane causes client A to return to the multibuffer.
@@ -482,16 +512,16 @@
// workspace.toggle_panel_focus::<TestPanel>(cx);
// });
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
+// workspace_a.update(cx_a, |workspace, cx| {
// assert_eq!(
-// workspace.active_item(cx).unwrap().id(),
-// multibuffer_editor_a.id()
+// workspace.active_item(cx).unwrap().item_id(),
+// multibuffer_editor_a.item_id()
// )
// });
// // Client B activates an item that doesn't implement following,
// // so the previously-opened screen-sharing item gets activated.
-// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
+// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
// workspace_b.update(cx_b, |workspace, cx| {
// workspace.active_pane().update(cx, |pane, cx| {
// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@@ -499,18 +529,18 @@
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// shared_screen.id()
+// .item_id()),
+// shared_screen.item_id()
// );
// // Following interrupts when client B disconnects.
// client_b.disconnect(&cx_b.to_async());
// executor.advance_clock(RECONNECT_TIMEOUT);
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
// None
// );
// }
@@ -521,7 +551,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -560,13 +590,19 @@
// .await
// .unwrap();
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
+// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
+// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-// let client_b_id = project_a.read_with(cx_a, |project, _| {
+// let client_b_id = project_a.update(cx_a, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
@@ -584,7 +620,7 @@
// .await
// .unwrap();
-// let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
// pane.update(cx, |pane, cx| {
// pane.items()
// .map(|item| {
@@ -642,7 +678,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -685,7 +721,10 @@
// .unwrap();
// // Client A opens a file.
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
// workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -696,7 +735,10 @@
// .unwrap();
// // Client B opens a different file.
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
@@ -1167,7 +1209,7 @@
// cx_b: &mut TestAppContext,
// ) {
// // 2 clients connect to a server.
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -1207,8 +1249,17 @@
// .await
// .unwrap();
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
// // Client A opens some editors.
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
// let _editor_a1 = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -1219,9 +1270,12 @@
// .unwrap();
// // Client B starts following client A.
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-// let leader_id = project_b.read_with(cx_b, |project, _| {
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
+// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+// let leader_id = project_b.update(cx_b, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
// workspace_b
@@ -1231,10 +1285,10 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
-// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -1245,7 +1299,7 @@
// // When client B moves, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1256,14 +1310,14 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// // When client B edits, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1274,16 +1328,16 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// // When client B scrolls, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| {
-// editor.set_scroll_position(vec2f(0., 3.), cx)
+// editor.set_scroll_position(point(0., 3.), cx)
// });
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1294,7 +1348,7 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
@@ -1303,13 +1357,13 @@
// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
// });
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
@@ -1321,7 +1375,7 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
// }
@@ -1332,7 +1386,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -1345,20 +1399,26 @@
// client_a.fs().insert_tree("/a", json!({})).await;
// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .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;
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// executor.run_until_parked();
-// let client_a_id = project_b.read_with(cx_b, |project, _| {
+// let client_a_id = project_b.update(cx_b, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
-// let client_b_id = project_a.read_with(cx_a, |project, _| {
+// let client_b_id = project_a.update(cx_a, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
@@ -1370,13 +1430,13 @@
// });
// futures::try_join!(a_follow_b, b_follow_a).unwrap();
-// workspace_a.read_with(cx_a, |workspace, _| {
+// workspace_a.update(cx_a, |workspace, _| {
// assert_eq!(
// workspace.leader_for_pane(workspace.active_pane()),
// Some(client_b_id)
// );
// });
-// workspace_b.read_with(cx_b, |workspace, _| {
+// workspace_b.update(cx_b, |workspace, _| {
// assert_eq!(
// workspace.leader_for_pane(workspace.active_pane()),
// Some(client_a_id)
@@ -1398,7 +1458,7 @@
// // b opens a different file in project 2, a follows b
// // b opens a different file in project 1, a cannot follow b
// // b shares the project, a joins the project and follows b
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// cx_a.update(editor::init);
@@ -1435,8 +1495,14 @@
// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
-// 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);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
@@ -1455,6 +1521,12 @@
// .await
// .unwrap();
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
// workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
@@ -1476,11 +1548,12 @@
// let workspace_b_project_a = cx_b
// .windows()
// .iter()
-// .max_by_key(|window| window.id())
+// .max_by_key(|window| window.item_id())
// .unwrap()
// .downcast::<Workspace>()
// .unwrap()
-// .root(cx_b);
+// .root(cx_b)
+// .unwrap();
// // assert that b is following a in project a in w.rs
// workspace_b_project_a.update(cx_b, |workspace, cx| {
@@ -1534,7 +1607,7 @@
// workspace.leader_for_pane(workspace.active_pane())
// );
// let item = workspace.active_pane().read(cx).active_item().unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
// });
// // b moves to y.rs in b's project, a is still following but can't yet see
@@ -1578,11 +1651,12 @@
// let workspace_a_project_b = cx_a
// .windows()
// .iter()
-// .max_by_key(|window| window.id())
+// .max_by_key(|window| window.item_id())
// .unwrap()
// .downcast::<Workspace>()
// .unwrap()
-// .root(cx_a);
+// .root(cx_a)
+// .unwrap();
// workspace_a_project_b.update(cx_a, |workspace, cx| {
// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
@@ -1596,12 +1670,151 @@
// });
// }
+// #[gpui::test]
+// async fn test_following_into_excluded_file(
+// executor: BackgroundExecutor,
+// mut cx_a: &mut TestAppContext,
+// mut cx_b: &mut TestAppContext,
+// ) {
+// let mut server = TestServer::start(executor.clone()).await;
+// let client_a = server.create_client(cx_a, "user_a").await;
+// let client_b = server.create_client(cx_b, "user_b").await;
+// for cx in [&mut cx_a, &mut cx_b] {
+// cx.update(|cx| {
+// cx.update_global::<SettingsStore, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+// });
+// });
+// });
+// }
+// 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);
+
+// client_a
+// .fs()
+// .insert_tree(
+// "/a",
+// json!({
+// ".git": {
+// "COMMIT_EDITMSG": "write your commit message here",
+// },
+// "1.txt": "one\none\none",
+// "2.txt": "two\ntwo\ntwo",
+// "3.txt": "three\nthree\nthree",
+// }),
+// )
+// .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 window_a = client_a.build_workspace(&project_a, cx_a);
+// let workspace_a = window_a.root(cx_a).unwrap();
+// let peer_id_a = client_a.peer_id().unwrap();
+// let window_b = client_b.build_workspace(&project_b, cx_b);
+// let workspace_b = window_b.root(cx_b).unwrap();
+
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
+// // Client A opens editors for a regular file and an excluded file.
+// let editor_for_regular = workspace_a
+// .update(cx_a, |workspace, cx| {
+// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+// })
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// let editor_for_excluded_a = workspace_a
+// .update(cx_a, |workspace, cx| {
+// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+// })
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+
+// // Client A updates their selections in those editors
+// editor_for_regular.update(cx_a, |editor, cx| {
+// editor.handle_input("a", cx);
+// editor.handle_input("b", cx);
+// editor.handle_input("c", cx);
+// editor.select_left(&Default::default(), cx);
+// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+// });
+// editor_for_excluded_a.update(cx_a, |editor, cx| {
+// editor.select_all(&Default::default(), cx);
+// editor.handle_input("new commit message", cx);
+// editor.select_left(&Default::default(), cx);
+// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+// });
+
+// // When client B starts following client A, currently visible file is replicated
+// workspace_b
+// .update(cx_b, |workspace, cx| {
+// workspace.follow(peer_id_a, cx).unwrap()
+// })
+// .await
+// .unwrap();
+
+// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert_eq!(
+// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+// );
+// assert_eq!(
+// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// vec![18..17]
+// );
+
+// // Changes from B to the excluded file are replicated in A's editor
+// editor_for_excluded_b.update(cx_b, |editor, cx| {
+// editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+// });
+// executor.run_until_parked();
+// editor_for_excluded_a.update(cx_a, |editor, cx| {
+// assert_eq!(
+// editor.text(cx),
+// "new commit messag\nCo-Authored-By: B <b@b.b>"
+// );
+// });
+// }
+
// fn visible_push_notifications(
// cx: &mut TestAppContext,
-// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+// ) -> Vec<gpui::View<ProjectSharedNotification>> {
// let mut ret = Vec::new();
// for window in cx.windows() {
-// window.read_with(cx, |window| {
+// window.update(cx, |window| {
// if let Some(handle) = window
// .root_view()
// .clone()
@@ -1645,8 +1858,8 @@
// })
// }
-// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
-// workspace.read_with(cx, |workspace, cx| {
+// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
+// workspace.update(cx, |workspace, cx| {
// let active_pane = workspace.active_pane();
// workspace
// .panes()
@@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
// Simultaneously join channel 1 and then channel 2
active_call_a
- .update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
+ .update(cx_a, |call, cx| call.join_channel(channel_1, cx))
.detach();
- let join_channel_2 =
- active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
+ let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
join_channel_2.await.unwrap();
@@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
call.invite(client_c.user_id().unwrap(), None, cx)
});
- let join_channel =
- active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+ let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
b_invite.await.unwrap();
c_invite.await.unwrap();
@@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
.unwrap();
// Simultaneously join channel 1 and call user B and user C from client A.
- let join_channel =
- active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
+ let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
let b_invite = active_call_a.update(cx_a, |call, cx| {
call.invite(client_b.user_id().unwrap(), None, cx)
@@ -2784,11 +2781,10 @@ async fn test_fs_operations(
let entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "c.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "c.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2815,8 +2811,8 @@ async fn test_fs_operations(
.update(cx_b, |project, cx| {
project.rename_entry(entry.id, Path::new("d.txt"), cx)
})
- .unwrap()
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2841,11 +2837,10 @@ async fn test_fs_operations(
let dir_entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2870,27 +2865,24 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/e.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2931,11 +2923,10 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .copy_entry(entry.id, Path::new("f.txt"), cx)
- .unwrap()
+ project.copy_entry(entry.id, Path::new("f.txt"), cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
- .unwrap()
.await?;
}
@@ -221,7 +221,6 @@ impl TestServer {
fs: fs.clone(),
build_window_options: |_, _, _| Default::default(),
node_runtime: FakeNodeRuntime::new(),
- call_factory: |_| Box::new(workspace::TestCallHandler),
});
cx.update(|cx| {
@@ -1,5 +1,5 @@
#![allow(unused)]
-// mod channel_modal;
+mod channel_modal;
mod contact_finder;
// use crate::{
@@ -18,7 +18,7 @@ mod contact_finder;
// };
use contact_finder::ContactFinder;
use menu::{Cancel, Confirm, SelectNext, SelectPrev};
-use rpc::proto;
+use rpc::proto::{self, PeerId};
use theme::{ActiveTheme, ThemeSettings};
// use context_menu::{ContextMenu, ContextMenuItem};
// use db::kvp::KEY_VALUE_STORE;
@@ -169,11 +169,12 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
- AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
- Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
- ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
- Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+ actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
+ AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
+ FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
+ MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
+ ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext,
+ VisualContext, WeakView,
};
use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize};
@@ -192,6 +193,8 @@ use workspace::{
use crate::{face_pile::FacePile, CollaborationPanelSettings};
+use self::channel_modal::ChannelModal;
+
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
@@ -345,21 +348,21 @@ enum Section {
#[derive(Clone, Debug)]
enum ListEntry {
Header(Section),
- // CallParticipant {
- // user: Arc<User>,
- // peer_id: Option<PeerId>,
- // is_pending: bool,
- // },
- // ParticipantProject {
- // project_id: u64,
- // worktree_root_names: Vec<String>,
- // host_user_id: u64,
- // is_last: bool,
- // },
- // ParticipantScreen {
- // peer_id: Option<PeerId>,
- // is_last: bool,
- // },
+ CallParticipant {
+ user: Arc<User>,
+ peer_id: Option<PeerId>,
+ is_pending: bool,
+ },
+ ParticipantProject {
+ project_id: u64,
+ worktree_root_names: Vec<String>,
+ host_user_id: u64,
+ is_last: bool,
+ },
+ ParticipantScreen {
+ peer_id: Option<PeerId>,
+ is_last: bool,
+ },
IncomingRequest(Arc<User>),
OutgoingRequest(Arc<User>),
// ChannelInvite(Arc<Channel>),
@@ -368,12 +371,12 @@ enum ListEntry {
depth: usize,
has_children: bool,
},
- // ChannelNotes {
- // channel_id: ChannelId,
- // },
- // ChannelChat {
- // channel_id: ChannelId,
- // },
+ ChannelNotes {
+ channel_id: ChannelId,
+ },
+ ChannelChat {
+ channel_id: ChannelId,
+ },
ChannelEditor {
depth: usize,
},
@@ -706,136 +709,136 @@ impl CollabPanel {
let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
let old_entries = mem::take(&mut self.entries);
- let scroll_to_top = false;
-
- // if let Some(room) = ActiveCall::global(cx).read(cx).room() {
- // self.entries.push(ListEntry::Header(Section::ActiveCall));
- // if !old_entries
- // .iter()
- // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
- // {
- // scroll_to_top = true;
- // }
+ let mut scroll_to_top = false;
- // if !self.collapsed_sections.contains(&Section::ActiveCall) {
- // let room = room.read(cx);
+ if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+ self.entries.push(ListEntry::Header(Section::ActiveCall));
+ if !old_entries
+ .iter()
+ .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+ {
+ scroll_to_top = true;
+ }
- // if let Some(channel_id) = room.channel_id() {
- // self.entries.push(ListEntry::ChannelNotes { channel_id });
- // self.entries.push(ListEntry::ChannelChat { channel_id })
- // }
+ if !self.collapsed_sections.contains(&Section::ActiveCall) {
+ let room = room.read(cx);
- // // Populate the active user.
- // if let Some(user) = user_store.current_user() {
- // self.match_candidates.clear();
- // self.match_candidates.push(StringMatchCandidate {
- // id: 0,
- // string: user.github_login.clone(),
- // char_bag: user.github_login.chars().collect(),
- // });
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // if !matches.is_empty() {
- // let user_id = user.id;
- // self.entries.push(ListEntry::CallParticipant {
- // user,
- // peer_id: None,
- // is_pending: false,
- // });
- // let mut projects = room.local_participant().projects.iter().peekable();
- // while let Some(project) = projects.next() {
- // self.entries.push(ListEntry::ParticipantProject {
- // project_id: project.id,
- // worktree_root_names: project.worktree_root_names.clone(),
- // host_user_id: user_id,
- // is_last: projects.peek().is_none() && !room.is_screen_sharing(),
- // });
- // }
- // if room.is_screen_sharing() {
- // self.entries.push(ListEntry::ParticipantScreen {
- // peer_id: None,
- // is_last: true,
- // });
- // }
- // }
- // }
+ if let Some(channel_id) = room.channel_id() {
+ self.entries.push(ListEntry::ChannelNotes { channel_id });
+ self.entries.push(ListEntry::ChannelChat { channel_id })
+ }
- // // Populate remote participants.
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(room.remote_participants().iter().map(|(_, participant)| {
- // StringMatchCandidate {
- // id: participant.user.id as usize,
- // string: participant.user.github_login.clone(),
- // char_bag: participant.user.github_login.chars().collect(),
- // }
- // }));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // for mat in matches {
- // let user_id = mat.candidate_id as u64;
- // let participant = &room.remote_participants()[&user_id];
- // self.entries.push(ListEntry::CallParticipant {
- // user: participant.user.clone(),
- // peer_id: Some(participant.peer_id),
- // is_pending: false,
- // });
- // let mut projects = participant.projects.iter().peekable();
- // while let Some(project) = projects.next() {
- // self.entries.push(ListEntry::ParticipantProject {
- // project_id: project.id,
- // worktree_root_names: project.worktree_root_names.clone(),
- // host_user_id: participant.user.id,
- // is_last: projects.peek().is_none()
- // && participant.video_tracks.is_empty(),
- // });
- // }
- // if !participant.video_tracks.is_empty() {
- // self.entries.push(ListEntry::ParticipantScreen {
- // peer_id: Some(participant.peer_id),
- // is_last: true,
- // });
- // }
- // }
+ // Populate the active user.
+ if let Some(user) = user_store.current_user() {
+ self.match_candidates.clear();
+ self.match_candidates.push(StringMatchCandidate {
+ id: 0,
+ string: user.github_login.clone(),
+ char_bag: user.github_login.chars().collect(),
+ });
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ if !matches.is_empty() {
+ let user_id = user.id;
+ self.entries.push(ListEntry::CallParticipant {
+ user,
+ peer_id: None,
+ is_pending: false,
+ });
+ let mut projects = room.local_participant().projects.iter().peekable();
+ while let Some(project) = projects.next() {
+ self.entries.push(ListEntry::ParticipantProject {
+ project_id: project.id,
+ worktree_root_names: project.worktree_root_names.clone(),
+ host_user_id: user_id,
+ is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+ });
+ }
+ if room.is_screen_sharing() {
+ self.entries.push(ListEntry::ParticipantScreen {
+ peer_id: None,
+ is_last: true,
+ });
+ }
+ }
+ }
- // // Populate pending participants.
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(room.pending_participants().iter().enumerate().map(
- // |(id, participant)| StringMatchCandidate {
- // id,
- // string: participant.github_login.clone(),
- // char_bag: participant.github_login.chars().collect(),
- // },
- // ));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // self.entries
- // .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
- // user: room.pending_participants()[mat.candidate_id].clone(),
- // peer_id: None,
- // is_pending: true,
- // }));
- // }
- // }
+ // Populate remote participants.
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(room.remote_participants().iter().map(|(_, participant)| {
+ StringMatchCandidate {
+ id: participant.user.id as usize,
+ string: participant.user.github_login.clone(),
+ char_bag: participant.user.github_login.chars().collect(),
+ }
+ }));
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ for mat in matches {
+ let user_id = mat.candidate_id as u64;
+ let participant = &room.remote_participants()[&user_id];
+ self.entries.push(ListEntry::CallParticipant {
+ user: participant.user.clone(),
+ peer_id: Some(participant.peer_id),
+ is_pending: false,
+ });
+ let mut projects = participant.projects.iter().peekable();
+ while let Some(project) = projects.next() {
+ self.entries.push(ListEntry::ParticipantProject {
+ project_id: project.id,
+ worktree_root_names: project.worktree_root_names.clone(),
+ host_user_id: participant.user.id,
+ is_last: projects.peek().is_none()
+ && participant.video_tracks.is_empty(),
+ });
+ }
+ if !participant.video_tracks.is_empty() {
+ self.entries.push(ListEntry::ParticipantScreen {
+ peer_id: Some(participant.peer_id),
+ is_last: true,
+ });
+ }
+ }
+
+ // Populate pending participants.
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(room.pending_participants().iter().enumerate().map(
+ |(id, participant)| StringMatchCandidate {
+ id,
+ string: participant.github_login.clone(),
+ char_bag: participant.github_login.chars().collect(),
+ },
+ ));
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ self.entries
+ .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+ user: room.pending_participants()[mat.candidate_id].clone(),
+ peer_id: None,
+ is_pending: true,
+ }));
+ }
+ }
let mut request_entries = Vec::new();
@@ -1133,290 +1136,234 @@ impl CollabPanel {
cx.notify();
}
- // fn render_call_participant(
- // user: &User,
- // peer_id: Option<PeerId>,
- // user_store: ModelHandle<UserStore>,
- // is_pending: bool,
- // is_selected: bool,
- // theme: &theme::Theme,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // enum CallParticipant {}
- // enum CallParticipantTooltip {}
- // enum LeaveCallButton {}
- // enum LeaveCallTooltip {}
-
- // let collab_theme = &theme.collab_panel;
-
- // let is_current_user =
- // user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
-
- // let content = MouseEventHandler::new::<CallParticipant, _>(
- // user.id as usize,
- // cx,
- // |mouse_state, cx| {
- // let style = if is_current_user {
- // *collab_theme
- // .contact_row
- // .in_state(is_selected)
- // .style_for(&mut Default::default())
- // } else {
- // *collab_theme
- // .contact_row
- // .in_state(is_selected)
- // .style_for(mouse_state)
- // };
-
- // Flex::row()
- // .with_children(user.avatar.clone().map(|avatar| {
- // Image::from_data(avatar)
- // .with_style(collab_theme.contact_avatar)
- // .aligned()
- // .left()
- // }))
- // .with_child(
- // Label::new(
- // user.github_login.clone(),
- // collab_theme.contact_username.text.clone(),
- // )
- // .contained()
- // .with_style(collab_theme.contact_username.container)
- // .aligned()
- // .left()
- // .flex(1., true),
- // )
- // .with_children(if is_pending {
- // Some(
- // Label::new("Calling", collab_theme.calling_indicator.text.clone())
- // .contained()
- // .with_style(collab_theme.calling_indicator.container)
- // .aligned()
- // .into_any(),
- // )
- // } else if is_current_user {
- // Some(
- // MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
- // render_icon_button(
- // theme
- // .collab_panel
- // .leave_call_button
- // .style_for(is_selected, state),
- // "icons/exit.svg",
- // )
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, |_, _, cx| {
- // Self::leave_call(cx);
- // })
- // .with_tooltip::<LeaveCallTooltip>(
- // 0,
- // "Leave call",
- // None,
- // theme.tooltip.clone(),
- // cx,
- // )
- // .into_any(),
- // )
- // } else {
- // None
- // })
- // .constrained()
- // .with_height(collab_theme.row_height)
- // .contained()
- // .with_style(style)
- // },
- // );
-
- // if is_current_user || is_pending || peer_id.is_none() {
- // return content.into_any();
- // }
-
- // let tooltip = format!("Follow {}", user.github_login);
-
- // content
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // if let Some(workspace) = this.workspace.upgrade(cx) {
- // workspace
- // .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx))
- // .map(|task| task.detach_and_log_err(cx));
- // }
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .with_tooltip::<CallParticipantTooltip>(
- // user.id as usize,
- // tooltip,
- // Some(Box::new(FollowNextCollaborator)),
- // theme.tooltip.clone(),
- // cx,
- // )
- // .into_any()
- // }
+ fn render_call_participant(
+ &self,
+ user: Arc<User>,
+ peer_id: Option<PeerId>,
+ is_pending: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> impl IntoElement {
+ let is_current_user =
+ self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
+ let tooltip = format!("Follow {}", user.github_login);
- // fn render_participant_project(
- // project_id: u64,
- // worktree_root_names: &[String],
- // host_user_id: u64,
- // is_current: bool,
- // is_last: bool,
- // is_selected: bool,
- // theme: &theme::Theme,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // enum JoinProject {}
- // enum JoinProjectTooltip {}
-
- // let collab_theme = &theme.collab_panel;
- // let host_avatar_width = collab_theme
- // .contact_avatar
- // .width
- // .or(collab_theme.contact_avatar.height)
- // .unwrap_or(0.);
- // let tree_branch = collab_theme.tree_branch;
- // let project_name = if worktree_root_names.is_empty() {
- // "untitled".to_string()
- // } else {
- // worktree_root_names.join(", ")
- // };
-
- // let content =
- // MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
- // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- // let row = if is_current {
- // collab_theme
- // .project_row
- // .in_state(true)
- // .style_for(&mut Default::default())
- // } else {
- // collab_theme
- // .project_row
- // .in_state(is_selected)
- // .style_for(mouse_state)
- // };
-
- // Flex::row()
- // .with_child(render_tree_branch(
- // tree_branch,
- // &row.name.text,
- // is_last,
- // vec2f(host_avatar_width, collab_theme.row_height),
- // cx.font_cache(),
- // ))
- // .with_child(
- // Svg::new("icons/file_icons/folder.svg")
- // .with_color(collab_theme.channel_hash.color)
- // .constrained()
- // .with_width(collab_theme.channel_hash.width)
- // .aligned()
- // .left(),
- // )
- // .with_child(
- // Label::new(project_name.clone(), row.name.text.clone())
- // .aligned()
- // .left()
- // .contained()
- // .with_style(row.name.container)
- // .flex(1., false),
- // )
- // .constrained()
- // .with_height(collab_theme.row_height)
- // .contained()
- // .with_style(row.container)
- // });
-
- // if is_current {
- // return content.into_any();
- // }
-
- // content
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // if let Some(workspace) = this.workspace.upgrade(cx) {
- // let app_state = workspace.read(cx).app_state().clone();
- // workspace::join_remote_project(project_id, host_user_id, app_state, cx)
- // .detach_and_log_err(cx);
- // }
- // })
- // .with_tooltip::<JoinProjectTooltip>(
- // project_id as usize,
- // format!("Open {}", project_name),
- // None,
- // theme.tooltip.clone(),
- // cx,
- // )
- // .into_any()
- // }
+ ListItem::new(SharedString::from(user.github_login.clone()))
+ .left_child(Avatar::data(user.avatar.clone().unwrap()))
+ .child(
+ h_stack()
+ .w_full()
+ .justify_between()
+ .child(Label::new(user.github_login.clone()))
+ .child(if is_pending {
+ Label::new("Calling").color(Color::Muted).into_any_element()
+ } else if is_current_user {
+ IconButton::new("leave-call", Icon::ArrowRight)
+ .on_click(cx.listener(move |this, _, cx| {
+ Self::leave_call(cx);
+ }))
+ .tooltip(|cx| Tooltip::text("Leave Call", cx))
+ .into_any_element()
+ } else {
+ div().into_any_element()
+ }),
+ )
+ .when_some(peer_id, |this, peer_id| {
+ this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
+ .on_click(cx.listener(move |this, _, cx| {
+ this.workspace
+ .update(cx, |workspace, cx| workspace.follow(peer_id, cx));
+ }))
+ })
+ }
- // fn render_participant_screen(
- // peer_id: Option<PeerId>,
- // is_last: bool,
- // is_selected: bool,
- // theme: &theme::CollabPanel,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // enum OpenSharedScreen {}
-
- // let host_avatar_width = theme
- // .contact_avatar
- // .width
- // .or(theme.contact_avatar.height)
- // .unwrap_or(0.);
- // let tree_branch = theme.tree_branch;
-
- // let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
- // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
- // cx,
- // |mouse_state, cx| {
- // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
- // let row = theme
- // .project_row
- // .in_state(is_selected)
- // .style_for(mouse_state);
-
- // Flex::row()
- // .with_child(render_tree_branch(
- // tree_branch,
- // &row.name.text,
- // is_last,
- // vec2f(host_avatar_width, theme.row_height),
- // cx.font_cache(),
- // ))
- // .with_child(
- // Svg::new("icons/desktop.svg")
- // .with_color(theme.channel_hash.color)
- // .constrained()
- // .with_width(theme.channel_hash.width)
- // .aligned()
- // .left(),
- // )
- // .with_child(
- // Label::new("Screen", row.name.text.clone())
- // .aligned()
- // .left()
- // .contained()
- // .with_style(row.name.container)
- // .flex(1., false),
- // )
- // .constrained()
- // .with_height(theme.row_height)
- // .contained()
- // .with_style(row.container)
- // },
- // );
- // if peer_id.is_none() {
- // return handler.into_any();
- // }
- // handler
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // if let Some(workspace) = this.workspace.upgrade(cx) {
- // workspace.update(cx, |workspace, cx| {
- // workspace.open_shared_screen(peer_id.unwrap(), cx)
- // });
- // }
- // })
- // .into_any()
- // }
+ fn render_participant_project(
+ &self,
+ project_id: u64,
+ worktree_root_names: &[String],
+ host_user_id: u64,
+ // is_current: bool,
+ is_last: bool,
+ // is_selected: bool,
+ // theme: &theme::Theme,
+ cx: &mut ViewContext<Self>,
+ ) -> impl IntoElement {
+ let project_name: SharedString = if worktree_root_names.is_empty() {
+ "untitled".to_string()
+ } else {
+ worktree_root_names.join(", ")
+ }
+ .into();
+
+ let theme = cx.theme();
+
+ ListItem::new(project_id as usize)
+ .on_click(cx.listener(move |this, _, cx| {
+ this.workspace.update(cx, |workspace, cx| {
+ let app_state = workspace.app_state().clone();
+ workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+ .detach_and_log_err(cx);
+ });
+ }))
+ .left_child(IconButton::new(0, Icon::Folder))
+ .child(
+ h_stack()
+ .w_full()
+ .justify_between()
+ .child(render_tree_branch(is_last, cx))
+ .child(Label::new(project_name.clone())),
+ )
+ .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
+
+ // enum JoinProject {}
+ // enum JoinProjectTooltip {}
+
+ // let collab_theme = &theme.collab_panel;
+ // let host_avatar_width = collab_theme
+ // .contact_avatar
+ // .width
+ // .or(collab_theme.contact_avatar.height)
+ // .unwrap_or(0.);
+ // let tree_branch = collab_theme.tree_branch;
+
+ // let content =
+ // MouseEventHandler::new::<JoinProject, _>(project_id as usize, cx, |mouse_state, cx| {
+ // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+ // let row = if is_current {
+ // collab_theme
+ // .project_row
+ // .in_state(true)
+ // .style_for(&mut Default::default())
+ // } else {
+ // collab_theme
+ // .project_row
+ // .in_state(is_selected)
+ // .style_for(mouse_state)
+ // };
+
+ // Flex::row()
+ // .with_child(render_tree_branch(
+ // tree_branch,
+ // &row.name.text,
+ // is_last,
+ // vec2f(host_avatar_width, collab_theme.row_height),
+ // cx.font_cache(),
+ // ))
+ // .with_child(
+ // Svg::new("icons/file_icons/folder.svg")
+ // .with_color(collab_theme.channel_hash.color)
+ // .constrained()
+ // .with_width(collab_theme.channel_hash.width)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_child(
+ // Label::new(project_name.clone(), row.name.text.clone())
+ // .aligned()
+ // .left()
+ // .contained()
+ // .with_style(row.name.container)
+ // .flex(1., false),
+ // )
+ // .constrained()
+ // .with_height(collab_theme.row_height)
+ // .contained()
+ // .with_style(row.container)
+ // });
+
+ // if is_current {
+ // return content.into_any();
+ // }
+
+ // content
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(workspace) = this.workspace.upgrade(cx) {
+ // let app_state = workspace.read(cx).app_state().clone();
+ // workspace::join_remote_project(project_id, host_user_id, app_state, cx)
+ // .detach_and_log_err(cx);
+ // }
+ // })
+ // .with_tooltip::<JoinProjectTooltip>(
+ // project_id as usize,
+ // format!("Open {}", project_name),
+ // None,
+ // theme.tooltip.clone(),
+ // cx,
+ // )
+ // .into_any()
+ }
+
+ fn render_participant_screen(
+ &self,
+ peer_id: Option<PeerId>,
+ is_last: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> impl IntoElement {
+ // enum OpenSharedScreen {}
+
+ // let host_avatar_width = theme
+ // .contact_avatar
+ // .width
+ // .or(theme.contact_avatar.height)
+ // .unwrap_or(0.);
+ // let tree_branch = theme.tree_branch;
+
+ // let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
+ // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
+ // cx,
+ // |mouse_state, cx| {
+ // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
+ // let row = theme
+ // .project_row
+ // .in_state(is_selected)
+ // .style_for(mouse_state);
+
+ // Flex::row()
+ // .with_child(render_tree_branch(
+ // tree_branch,
+ // &row.name.text,
+ // is_last,
+ // vec2f(host_avatar_width, theme.row_height),
+ // cx.font_cache(),
+ // ))
+ // .with_child(
+ // Svg::new("icons/desktop.svg")
+ // .with_color(theme.channel_hash.color)
+ // .constrained()
+ // .with_width(theme.channel_hash.width)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_child(
+ // Label::new("Screen", row.name.text.clone())
+ // .aligned()
+ // .left()
+ // .contained()
+ // .with_style(row.name.container)
+ // .flex(1., false),
+ // )
+ // .constrained()
+ // .with_height(theme.row_height)
+ // .contained()
+ // .with_style(row.container)
+ // },
+ // );
+ // if peer_id.is_none() {
+ // return handler.into_any();
+ // }
+ // handler
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(workspace) = this.workspace.upgrade(cx) {
+ // workspace.update(cx, |workspace, cx| {
+ // workspace.open_shared_screen(peer_id.unwrap(), cx)
+ // });
+ // }
+ // })
+ // .into_any()
+
+ div()
+ }
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
if let Some(_) = self.channel_editing_state.take() {
@@ -1463,117 +1410,114 @@ impl CollabPanel {
// .into_any()
// }
- // fn render_channel_notes(
- // &self,
- // channel_id: ChannelId,
- // theme: &theme::CollabPanel,
- // is_selected: bool,
- // ix: usize,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // enum ChannelNotes {}
- // let host_avatar_width = theme
- // .contact_avatar
- // .width
- // .or(theme.contact_avatar.height)
- // .unwrap_or(0.);
-
- // MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
- // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
- // let row = theme.project_row.in_state(is_selected).style_for(state);
-
- // Flex::<Self>::row()
- // .with_child(render_tree_branch(
- // tree_branch,
- // &row.name.text,
- // false,
- // vec2f(host_avatar_width, theme.row_height),
- // cx.font_cache(),
- // ))
- // .with_child(
- // Svg::new("icons/file.svg")
- // .with_color(theme.channel_hash.color)
- // .constrained()
- // .with_width(theme.channel_hash.width)
- // .aligned()
- // .left(),
- // )
- // .with_child(
- // Label::new("notes", theme.channel_name.text.clone())
- // .contained()
- // .with_style(theme.channel_name.container)
- // .aligned()
- // .left()
- // .flex(1., true),
- // )
- // .constrained()
- // .with_height(theme.row_height)
- // .contained()
- // .with_style(*theme.channel_row.style_for(is_selected, state))
- // .with_padding_left(theme.channel_row.default_style().padding.left)
- // })
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .into_any()
- // }
+ fn render_channel_notes(
+ &self,
+ channel_id: ChannelId,
+ cx: &mut ViewContext<Self>,
+ ) -> impl IntoElement {
+ // enum ChannelNotes {}
+ // let host_avatar_width = theme
+ // .contact_avatar
+ // .width
+ // .or(theme.contact_avatar.height)
+ // .unwrap_or(0.);
+
+ // MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
+ // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+ // let row = theme.project_row.in_state(is_selected).style_for(state);
+
+ // Flex::<Self>::row()
+ // .with_child(render_tree_branch(
+ // tree_branch,
+ // &row.name.text,
+ // false,
+ // vec2f(host_avatar_width, theme.row_height),
+ // cx.font_cache(),
+ // ))
+ // .with_child(
+ // Svg::new("icons/file.svg")
+ // .with_color(theme.channel_hash.color)
+ // .constrained()
+ // .with_width(theme.channel_hash.width)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_child(
+ // Label::new("notes", theme.channel_name.text.clone())
+ // .contained()
+ // .with_style(theme.channel_name.container)
+ // .aligned()
+ // .left()
+ // .flex(1., true),
+ // )
+ // .constrained()
+ // .with_height(theme.row_height)
+ // .contained()
+ // .with_style(*theme.channel_row.style_for(is_selected, state))
+ // .with_padding_left(theme.channel_row.default_style().padding.left)
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .into_any()
- // fn render_channel_chat(
- // &self,
- // channel_id: ChannelId,
- // theme: &theme::CollabPanel,
- // is_selected: bool,
- // ix: usize,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // enum ChannelChat {}
- // let host_avatar_width = theme
- // .contact_avatar
- // .width
- // .or(theme.contact_avatar.height)
- // .unwrap_or(0.);
-
- // MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
- // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
- // let row = theme.project_row.in_state(is_selected).style_for(state);
-
- // Flex::<Self>::row()
- // .with_child(render_tree_branch(
- // tree_branch,
- // &row.name.text,
- // true,
- // vec2f(host_avatar_width, theme.row_height),
- // cx.font_cache(),
- // ))
- // .with_child(
- // Svg::new("icons/conversations.svg")
- // .with_color(theme.channel_hash.color)
- // .constrained()
- // .with_width(theme.channel_hash.width)
- // .aligned()
- // .left(),
- // )
- // .with_child(
- // Label::new("chat", theme.channel_name.text.clone())
- // .contained()
- // .with_style(theme.channel_name.container)
- // .aligned()
- // .left()
- // .flex(1., true),
- // )
- // .constrained()
- // .with_height(theme.row_height)
- // .contained()
- // .with_style(*theme.channel_row.style_for(is_selected, state))
- // .with_padding_left(theme.channel_row.default_style().padding.left)
- // })
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .into_any()
- // }
+ div()
+ }
+
+ fn render_channel_chat(
+ &self,
+ channel_id: ChannelId,
+ cx: &mut ViewContext<Self>,
+ ) -> impl IntoElement {
+ // enum ChannelChat {}
+ // let host_avatar_width = theme
+ // .contact_avatar
+ // .width
+ // .or(theme.contact_avatar.height)
+ // .unwrap_or(0.);
+
+ // MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
+ // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
+ // let row = theme.project_row.in_state(is_selected).style_for(state);
+
+ // Flex::<Self>::row()
+ // .with_child(render_tree_branch(
+ // tree_branch,
+ // &row.name.text,
+ // true,
+ // vec2f(host_avatar_width, theme.row_height),
+ // cx.font_cache(),
+ // ))
+ // .with_child(
+ // Svg::new("icons/conversations.svg")
+ // .with_color(theme.channel_hash.color)
+ // .constrained()
+ // .with_width(theme.channel_hash.width)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_child(
+ // Label::new("chat", theme.channel_name.text.clone())
+ // .contained()
+ // .with_style(theme.channel_name.container)
+ // .aligned()
+ // .left()
+ // .flex(1., true),
+ // )
+ // .constrained()
+ // .with_height(theme.row_height)
+ // .contained()
+ // .with_style(*theme.channel_row.style_for(is_selected, state))
+ // .with_padding_left(theme.channel_row.default_style().padding.left)
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .into_any()
+ div()
+ }
// fn render_channel_invite(
// channel: Arc<Channel>,
@@ -3,58 +3,54 @@ use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
-use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions,
- elements::*,
- platform::{CursorStyle, MouseButton},
- AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
- ViewHandle,
+ actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
+ FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
+ WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use std::sync::Arc;
+use ui::v_stack;
use util::TryFutureExt;
-use workspace::Modal;
actions!(
- channel_modal,
- [
- SelectNextControl,
- ToggleMode,
- ToggleMemberAdmin,
- RemoveMember
- ]
+ SelectNextControl,
+ ToggleMode,
+ ToggleMemberAdmin,
+ RemoveMember
);
-pub fn init(cx: &mut AppContext) {
- Picker::<ChannelModalDelegate>::init(cx);
- cx.add_action(ChannelModal::toggle_mode);
- cx.add_action(ChannelModal::toggle_member_admin);
- cx.add_action(ChannelModal::remove_member);
- cx.add_action(ChannelModal::dismiss);
-}
+// pub fn init(cx: &mut AppContext) {
+// Picker::<ChannelModalDelegate>::init(cx);
+// cx.add_action(ChannelModal::toggle_mode);
+// cx.add_action(ChannelModal::toggle_member_admin);
+// cx.add_action(ChannelModal::remove_member);
+// cx.add_action(ChannelModal::dismiss);
+// }
pub struct ChannelModal {
- picker: ViewHandle<Picker<ChannelModalDelegate>>,
- channel_store: ModelHandle<ChannelStore>,
+ picker: View<Picker<ChannelModalDelegate>>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
- let picker = cx.add_view(|cx| {
+ let channel_modal = cx.view().downgrade();
+ let picker = cx.build_view(|cx| {
Picker::new(
ChannelModalDelegate {
+ channel_modal,
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
@@ -64,20 +60,17 @@ impl ChannelModal {
match_candidates: Vec::new(),
members,
mode,
- context_menu: cx.add_view(|cx| {
- let mut menu = ContextMenu::new(cx.view_id(), cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
+ // context_menu: cx.add_view(|cx| {
+ // let mut menu = ContextMenu::new(cx.view_id(), cx);
+ // menu.set_position_mode(OverlayPositionMode::Local);
+ // menu
+ // }),
},
cx,
)
- .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
- cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
-
- let has_focus = picker.read(cx).has_focus();
+ let has_focus = picker.focus_handle(cx).contains_focused(cx);
Self {
picker,
@@ -88,7 +81,7 @@ impl ChannelModal {
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
- let mode = match self.picker.read(cx).delegate().mode {
+ let mode = match self.picker.read(cx).delegate.mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
@@ -103,20 +96,20 @@ impl ChannelModal {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
- })
+ })?
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
- .update(cx, |picker, _| picker.delegate_mut().members = members);
+ .update(cx, |picker, _| picker.delegate.members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
@@ -131,203 +124,194 @@ impl ChannelModal {
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
- picker.delegate_mut().toggle_selected_member_admin(cx);
+ picker.delegate.toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
- picker.delegate_mut().remove_selected_member(cx);
+ picker.delegate.remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(PickerEvent::Dismiss);
+ cx.emit(DismissEvent);
}
}
-impl Entity for ChannelModal {
- type Event = PickerEvent;
-}
-
-impl View for ChannelModal {
- fn ui_name() -> &'static str {
- "ChannelModal"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &theme::current(cx).collab_panel.tabbed_modal;
-
- let mode = self.picker.read(cx).delegate().mode;
- let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
- return Empty::new().into_any();
- };
-
- enum InviteMembers {}
- enum ManageMembers {}
-
- fn render_mode_button<T: 'static>(
- mode: Mode,
- text: &'static str,
- current_mode: Mode,
- theme: &theme::TabbedModal,
- cx: &mut ViewContext<ChannelModal>,
- ) -> AnyElement<ChannelModal> {
- let active = mode == current_mode;
- MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
- let contained_text = theme.tab_button.style_for(active, state);
- Label::new(text, contained_text.text.clone())
- .contained()
- .with_style(contained_text.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if !active {
- this.set_mode(mode, cx);
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }
-
- fn render_visibility(
- channel_id: ChannelId,
- visibility: ChannelVisibility,
- theme: &theme::TabbedModal,
- cx: &mut ViewContext<ChannelModal>,
- ) -> AnyElement<ChannelModal> {
- enum TogglePublic {}
-
- if visibility == ChannelVisibility::Members {
- return Flex::row()
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
- let style = theme.visibility_toggle.style_for(state);
- Label::new(format!("{}", "Public access: OFF"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.set_channel_visibility(
- channel_id,
- ChannelVisibility::Public,
- cx,
- )
- })
- .detach_and_log_err(cx);
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .into_any();
- }
-
- Flex::row()
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
- let style = theme.visibility_toggle.style_for(state);
- Label::new(format!("{}", "Public access: ON"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.set_channel_visibility(
- channel_id,
- ChannelVisibility::Members,
- cx,
- )
- })
- .detach_and_log_err(cx);
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .with_spacing(14.0)
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
- let style = theme.channel_link.style_for(state);
- Label::new(format!("{}", "copy link"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(channel) =
- this.channel_store.read(cx).channel_for_id(channel_id)
- {
- let item = ClipboardItem::new(channel.link());
- cx.write_to_clipboard(item);
- }
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .into_any()
- }
-
- Flex::column()
- .with_child(
- Flex::column()
- .with_child(
- Label::new(format!("#{}", channel.name), theme.title.text.clone())
- .contained()
- .with_style(theme.title.container.clone()),
- )
- .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
- .with_child(Flex::row().with_children([
- render_mode_button::<InviteMembers>(
- Mode::InviteMembers,
- "Invite members",
- mode,
- theme,
- cx,
- ),
- render_mode_button::<ManageMembers>(
- Mode::ManageMembers,
- "Manage members",
- mode,
- theme,
- cx,
- ),
- ]))
- .expanded()
- .contained()
- .with_style(theme.header),
- )
- .with_child(
- ChildView::new(&self.picker, cx)
- .contained()
- .with_style(theme.body),
- )
- .constrained()
- .with_max_height(theme.max_height)
- .with_max_width(theme.max_width)
- .contained()
- .with_style(theme.modal)
- .into_any()
- }
-
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if cx.is_self_focused() {
- cx.focus(&self.picker)
- }
- }
+impl EventEmitter<DismissEvent> for ChannelModal {}
- fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
+impl FocusableView for ChannelModal {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
}
}
-impl Modal for ChannelModal {
- fn has_focus(&self) -> bool {
- self.has_focus
+impl Render for ChannelModal {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ // let theme = &theme::current(cx).collab_panel.tabbed_modal;
+
+ // let mode = self.picker.read(cx).delegate().mode;
+ // let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
+ // return Empty::new().into_any();
+ // };
+
+ // enum InviteMembers {}
+ // enum ManageMembers {}
+
+ // fn render_mode_button<T: 'static>(
+ // mode: Mode,
+ // text: &'static str,
+ // current_mode: Mode,
+ // theme: &theme::TabbedModal,
+ // cx: &mut ViewContext<ChannelModal>,
+ // ) -> AnyElement<ChannelModal> {
+ // let active = mode == current_mode;
+ // MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
+ // let contained_text = theme.tab_button.style_for(active, state);
+ // Label::new(text, contained_text.text.clone())
+ // .contained()
+ // .with_style(contained_text.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if !active {
+ // this.set_mode(mode, cx);
+ // }
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .into_any()
+ // }
+
+ // fn render_visibility(
+ // channel_id: ChannelId,
+ // visibility: ChannelVisibility,
+ // theme: &theme::TabbedModal,
+ // cx: &mut ViewContext<ChannelModal>,
+ // ) -> AnyElement<ChannelModal> {
+ // enum TogglePublic {}
+
+ // if visibility == ChannelVisibility::Members {
+ // return Flex::row()
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+ // let style = theme.visibility_toggle.style_for(state);
+ // Label::new(format!("{}", "Public access: OFF"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.channel_store
+ // .update(cx, |channel_store, cx| {
+ // channel_store.set_channel_visibility(
+ // channel_id,
+ // ChannelVisibility::Public,
+ // cx,
+ // )
+ // })
+ // .detach_and_log_err(cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .into_any();
+ // }
+
+ // Flex::row()
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+ // let style = theme.visibility_toggle.style_for(state);
+ // Label::new(format!("{}", "Public access: ON"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.channel_store
+ // .update(cx, |channel_store, cx| {
+ // channel_store.set_channel_visibility(
+ // channel_id,
+ // ChannelVisibility::Members,
+ // cx,
+ // )
+ // })
+ // .detach_and_log_err(cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .with_spacing(14.0)
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
+ // let style = theme.channel_link.style_for(state);
+ // Label::new(format!("{}", "copy link"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(channel) =
+ // this.channel_store.read(cx).channel_for_id(channel_id)
+ // {
+ // let item = ClipboardItem::new(channel.link());
+ // cx.write_to_clipboard(item);
+ // }
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .into_any()
+ // }
+
+ // Flex::column()
+ // .with_child(
+ // Flex::column()
+ // .with_child(
+ // Label::new(format!("#{}", channel.name), theme.title.text.clone())
+ // .contained()
+ // .with_style(theme.title.container.clone()),
+ // )
+ // .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
+ // .with_child(Flex::row().with_children([
+ // render_mode_button::<InviteMembers>(
+ // Mode::InviteMembers,
+ // "Invite members",
+ // mode,
+ // theme,
+ // cx,
+ // ),
+ // render_mode_button::<ManageMembers>(
+ // Mode::ManageMembers,
+ // "Manage members",
+ // mode,
+ // theme,
+ // cx,
+ // ),
+ // ]))
+ // .expanded()
+ // .contained()
+ // .with_style(theme.header),
+ // )
+ // .with_child(
+ // ChildView::new(&self.picker, cx)
+ // .contained()
+ // .with_style(theme.body),
+ // )
+ // .constrained()
+ // .with_max_height(theme.max_height)
+ // .with_max_width(theme.max_width)
+ // .contained()
+ // .with_style(theme.modal)
+ // .into_any()
}
- fn dismiss_on_event(event: &Self::Event) -> bool {
- match event {
- PickerEvent::Dismiss => true,
- }
- }
+ // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ // self.has_focus = true;
+ // if cx.is_self_focused() {
+ // cx.focus(&self.picker)
+ // }
+ // }
+
+ // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+ // self.has_focus = false;
+ // }
}
#[derive(Copy, Clone, PartialEq)]
@@ -337,19 +321,22 @@ pub enum Mode {
}
pub struct ChannelModalDelegate {
+ channel_modal: WeakView<ChannelModal>,
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
- context_menu: ViewHandle<ContextMenu>,
+ // context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
+ type ListItem = Div;
+
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
@@ -382,19 +369,19 @@ impl PickerDelegate for ChannelModalDelegate {
}
}));
- let matches = cx.background().block(match_strings(
+ let matches = cx.background_executor().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
- cx.background().clone(),
+ cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
@@ -412,8 +399,7 @@ impl PickerDelegate for ChannelModalDelegate {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
- delegate.matching_users = users;
+ picker.delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
@@ -445,138 +431,142 @@ impl PickerDelegate for ChannelModalDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ self.channel_modal
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> AnyElement<Picker<Self>> {
- let full_theme = &theme::current(cx);
- let theme = &full_theme.collab_panel.channel_modal;
- let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
- let (user, role) = self.user_at_index(ix).unwrap();
- let request_status = self.member_status(user.id, cx);
-
- let style = tabbed_modal
- .picker
- .item
- .in_state(selected)
- .style_for(mouse_state);
-
- let in_manage = matches!(self.mode, Mode::ManageMembers);
-
- let mut result = Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(user.github_login.clone(), style.label.clone())
- .contained()
- .with_style(theme.contact_username)
- .aligned()
- .left(),
- )
- .with_children({
- (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
- || {
- Label::new("Invited", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left()
- },
- )
- })
- .with_children(if in_manage && role == Some(ChannelRole::Admin) {
- Some(
- Label::new("Admin", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left(),
- )
- } else if in_manage && role == Some(ChannelRole::Guest) {
- Some(
- Label::new("Guest", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left(),
- )
- } else {
- None
- })
- .with_children({
- let svg = match self.mode {
- Mode::ManageMembers => Some(
- Svg::new("icons/ellipsis.svg")
- .with_color(theme.member_icon.color)
- .constrained()
- .with_width(theme.member_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.member_icon.button_width)
- .with_height(theme.member_icon.button_width)
- .contained()
- .with_style(theme.member_icon.container),
- ),
- Mode::InviteMembers => match request_status {
- Some(proto::channel_member::Kind::Member) => Some(
- Svg::new("icons/check.svg")
- .with_color(theme.member_icon.color)
- .constrained()
- .with_width(theme.member_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.member_icon.button_width)
- .with_height(theme.member_icon.button_width)
- .contained()
- .with_style(theme.member_icon.container),
- ),
- Some(proto::channel_member::Kind::Invitee) => Some(
- Svg::new("icons/check.svg")
- .with_color(theme.invitee_icon.color)
- .constrained()
- .with_width(theme.invitee_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.invitee_icon.button_width)
- .with_height(theme.invitee_icon.button_width)
- .contained()
- .with_style(theme.invitee_icon.container),
- ),
- Some(proto::channel_member::Kind::AncestorMember) | None => None,
- },
- };
-
- svg.map(|svg| svg.aligned().flex_float().into_any())
- })
- .contained()
- .with_style(style.container)
- .constrained()
- .with_height(tabbed_modal.row_height)
- .into_any();
-
- if selected {
- result = Stack::new()
- .with_child(result)
- .with_child(
- ChildView::new(&self.context_menu, cx)
- .aligned()
- .top()
- .right(),
- )
- .into_any();
- }
-
- result
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ None
+ // let full_theme = &theme::current(cx);
+ // let theme = &full_theme.collab_panel.channel_modal;
+ // let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+ // let (user, role) = self.user_at_index(ix).unwrap();
+ // let request_status = self.member_status(user.id, cx);
+
+ // let style = tabbed_modal
+ // .picker
+ // .item
+ // .in_state(selected)
+ // .style_for(mouse_state);
+
+ // let in_manage = matches!(self.mode, Mode::ManageMembers);
+
+ // let mut result = Flex::row()
+ // .with_children(user.avatar.clone().map(|avatar| {
+ // Image::from_data(avatar)
+ // .with_style(theme.contact_avatar)
+ // .aligned()
+ // .left()
+ // }))
+ // .with_child(
+ // Label::new(user.github_login.clone(), style.label.clone())
+ // .contained()
+ // .with_style(theme.contact_username)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_children({
+ // (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
+ // || {
+ // Label::new("Invited", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left()
+ // },
+ // )
+ // })
+ // .with_children(if in_manage && role == Some(ChannelRole::Admin) {
+ // Some(
+ // Label::new("Admin", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left(),
+ // )
+ // } else if in_manage && role == Some(ChannelRole::Guest) {
+ // Some(
+ // Label::new("Guest", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left(),
+ // )
+ // } else {
+ // None
+ // })
+ // .with_children({
+ // let svg = match self.mode {
+ // Mode::ManageMembers => Some(
+ // Svg::new("icons/ellipsis.svg")
+ // .with_color(theme.member_icon.color)
+ // .constrained()
+ // .with_width(theme.member_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.member_icon.button_width)
+ // .with_height(theme.member_icon.button_width)
+ // .contained()
+ // .with_style(theme.member_icon.container),
+ // ),
+ // Mode::InviteMembers => match request_status {
+ // Some(proto::channel_member::Kind::Member) => Some(
+ // Svg::new("icons/check.svg")
+ // .with_color(theme.member_icon.color)
+ // .constrained()
+ // .with_width(theme.member_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.member_icon.button_width)
+ // .with_height(theme.member_icon.button_width)
+ // .contained()
+ // .with_style(theme.member_icon.container),
+ // ),
+ // Some(proto::channel_member::Kind::Invitee) => Some(
+ // Svg::new("icons/check.svg")
+ // .with_color(theme.invitee_icon.color)
+ // .constrained()
+ // .with_width(theme.invitee_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.invitee_icon.button_width)
+ // .with_height(theme.invitee_icon.button_width)
+ // .contained()
+ // .with_style(theme.invitee_icon.container),
+ // ),
+ // Some(proto::channel_member::Kind::AncestorMember) | None => None,
+ // },
+ // };
+
+ // svg.map(|svg| svg.aligned().flex_float().into_any())
+ // })
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_height(tabbed_modal.row_height)
+ // .into_any();
+
+ // if selected {
+ // result = Stack::new()
+ // .with_child(result)
+ // .with_child(
+ // ChildView::new(&self.context_menu, cx)
+ // .aligned()
+ // .top()
+ // .right(),
+ // )
+ // .into_any();
+ // }
+
+ // result
}
}
@@ -623,7 +613,7 @@ impl ChannelModalDelegate {
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
- let this = picker.delegate_mut();
+ let this = &mut picker.delegate;
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.role = new_role;
}
@@ -644,7 +634,7 @@ impl ChannelModalDelegate {
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
- let this = picker.delegate_mut();
+ let this = &mut picker.delegate;
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
@@ -683,7 +673,7 @@ impl ChannelModalDelegate {
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
- let members = &mut this.delegate_mut().members;
+ let members = &mut this.delegate.members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
@@ -695,23 +685,23 @@ impl ChannelModalDelegate {
}
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
- self.context_menu.update(cx, |context_menu, cx| {
- context_menu.show(
- Default::default(),
- AnchorCorner::TopRight,
- vec![
- ContextMenuItem::action("Remove", RemoveMember),
- ContextMenuItem::action(
- if role == ChannelRole::Admin {
- "Make non-admin"
- } else {
- "Make admin"
- },
- ToggleMemberAdmin,
- ),
- ],
- cx,
- )
- })
+ // self.context_menu.update(cx, |context_menu, cx| {
+ // context_menu.show(
+ // Default::default(),
+ // AnchorCorner::TopRight,
+ // vec![
+ // ContextMenuItem::action("Remove", RemoveMember),
+ // ContextMenuItem::action(
+ // if role == ChannelRole::Admin {
+ // "Make non-admin"
+ // } else {
+ // "Make admin"
+ // },
+ // ToggleMemberAdmin,
+ // ),
+ // ],
+ // cx,
+ // )
+ // })
}
}
@@ -31,20 +31,31 @@ use std::sync::Arc;
use call::ActiveCall;
use client::{Client, UserStore};
use gpui::{
- div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
- ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
- ViewContext, VisualContext, WeakView, WindowBounds,
+ actions, div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model,
+ MouseButton, ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled,
+ Subscription, ViewContext, VisualContext, WeakView, WindowBounds,
};
-use project::Project;
+use project::{Project, RepositoryEntry};
use theme::ActiveTheme;
-use ui::{h_stack, prelude::*, Avatar, Button, ButtonStyle, IconButton, KeyBinding, Tooltip};
+use ui::{
+ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
+ IconButton, IconElement, KeyBinding, Tooltip,
+};
use util::ResultExt;
use workspace::{notifications::NotifyResultExt, Workspace};
use crate::face_pile::FacePile;
-// const MAX_PROJECT_NAME_LENGTH: usize = 40;
-// const MAX_BRANCH_NAME_LENGTH: usize = 40;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
+
+actions!(
+ ShareProject,
+ UnshareProject,
+ ToggleUserMenu,
+ ToggleProjectMenu,
+ SwitchBranch
+);
// actions!(
// collab,
@@ -88,36 +99,23 @@ impl Render for CollabTitlebarItem {
type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let is_in_room = self
- .workspace
- .update(cx, |this, cx| this.call_state().is_in_room(cx))
- .unwrap_or_default();
+ let room = ActiveCall::global(cx).read(cx).room();
+ let is_in_room = room.is_some();
let is_shared = is_in_room && self.project.read(cx).is_shared();
let current_user = self.user_store.read(cx).current_user();
let client = self.client.clone();
- let users = self
- .workspace
- .update(cx, |this, cx| this.call_state().remote_participants(cx))
- .log_err()
- .flatten();
- let mic_icon = if self
- .workspace
- .update(cx, |this, cx| this.call_state().is_muted(cx))
- .log_err()
- .flatten()
- .unwrap_or_default()
- {
- ui::Icon::MicMute
- } else {
- ui::Icon::Mic
- };
- let speakers_icon = if self
- .workspace
- .update(cx, |this, cx| this.call_state().is_deafened(cx))
- .log_err()
- .flatten()
- .unwrap_or_default()
- {
+ let remote_participants = room.map(|room| {
+ room.read(cx)
+ .remote_participants()
+ .values()
+ .map(|participant| (participant.user.clone(), participant.peer_id))
+ .collect::<Vec<_>>()
+ });
+ let is_muted = room.map_or(false, |room| room.read(cx).is_muted(cx));
+ let is_deafened = room
+ .and_then(|room| room.read(cx).is_deafened())
+ .unwrap_or(false);
+ let speakers_icon = if is_deafened {
ui::Icon::AudioOff
} else {
ui::Icon::AudioOn
@@ -146,59 +144,14 @@ impl Render for CollabTitlebarItem {
.child(
h_stack()
.gap_1()
- // TODO - Add player menu
- .child(
- div()
- .border()
- .border_color(gpui::red())
- .id("project_owner_indicator")
- .child(
- Button::new("player", "player")
- .style(ButtonStyle::Subtle)
- .color(Some(Color::Player(0))),
- )
- .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
- )
- // TODO - Add project menu
- .child(
- div()
- .border()
- .border_color(gpui::red())
- .id("titlebar_project_menu_button")
- .child(
- Button::new("project_name", "project_name")
- .style(ButtonStyle::Subtle),
- )
- .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
- )
- // TODO - Add git menu
- .child(
- div()
- .border()
- .border_color(gpui::red())
- .id("titlebar_git_menu_button")
- .child(
- Button::new("branch_name", "branch_name")
- .style(ButtonStyle::Subtle)
- .color(Some(Color::Muted)),
- )
- .tooltip(move |cx| {
- cx.build_view(|_| {
- Tooltip::new("Recent Branches")
- .key_binding(KeyBinding::new(gpui::KeyBinding::new(
- "cmd-b",
- // todo!() Replace with real action.
- gpui::NoAction,
- None,
- )))
- .meta("Only local branches shown")
- })
- .into()
- }),
- ),
+ .when(is_in_room, |this| {
+ this.children(self.render_project_owner(cx))
+ })
+ .child(self.render_project_name(cx))
+ .children(self.render_project_branch(cx)),
)
.when_some(
- users.zip(current_user.clone()),
+ remote_participants.zip(current_user.clone()),
|this, (remote_participants, current_user)| {
let mut pile = FacePile::default();
pile.extend(
@@ -209,25 +162,30 @@ impl Render for CollabTitlebarItem {
div().child(Avatar::data(avatar.clone())).into_any_element()
})
.into_iter()
- .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
- user.avatar.as_ref().map(|avatar| {
- div()
- .child(
- Avatar::data(avatar.clone()).into_element().into_any(),
- )
- .on_mouse_down(MouseButton::Left, {
- let workspace = workspace.clone();
- move |_, cx| {
- workspace
- .update(cx, |this, cx| {
- this.open_shared_screen(peer_id, cx);
- })
- .log_err();
- }
- })
- .into_any_element()
- })
- })),
+ .chain(remote_participants.into_iter().filter_map(
+ |(user, peer_id)| {
+ let avatar = user.avatar.as_ref()?;
+ Some(
+ div()
+ .child(
+ Avatar::data(avatar.clone())
+ .into_element()
+ .into_any(),
+ )
+ .on_mouse_down(MouseButton::Left, {
+ let workspace = workspace.clone();
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.open_shared_screen(peer_id, cx);
+ })
+ .log_err();
+ }
+ })
+ .into_any_element(),
+ )
+ },
+ )),
);
this.child(pile.render(cx))
},
@@ -236,62 +194,112 @@ impl Render for CollabTitlebarItem {
.when(is_in_room, |this| {
this.child(
h_stack()
+ .gap_1()
.child(
h_stack()
- .child(Button::new(
- "toggle_sharing",
- if is_shared { "Unshare" } else { "Share" },
- ))
- .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
- let workspace = workspace.clone();
- move |_, cx| {
- workspace
- .update(cx, |this, cx| {
- this.call_state().hang_up(cx).detach();
- })
- .log_err();
- }
- })),
+ .gap_1()
+ .child(
+ Button::new(
+ "toggle_sharing",
+ if is_shared { "Unshare" } else { "Share" },
+ )
+ .style(ButtonStyle::Subtle)
+ .on_click(cx.listener(
+ move |this, _, cx| {
+ if is_shared {
+ this.unshare_project(&Default::default(), cx);
+ } else {
+ this.share_project(&Default::default(), cx);
+ }
+ },
+ )),
+ )
+ .child(
+ IconButton::new("leave-call", ui::Icon::Exit)
+ .style(ButtonStyle::Subtle)
+ .on_click(move |_, cx| {
+ ActiveCall::global(cx)
+ .update(cx, |call, cx| call.hang_up(cx))
+ .detach_and_log_err(cx);
+ }),
+ ),
)
.child(
h_stack()
- .child(IconButton::new("mute-microphone", mic_icon).on_click({
- let workspace = workspace.clone();
- move |_, cx| {
- workspace
- .update(cx, |this, cx| {
- this.call_state().toggle_mute(cx);
- })
- .log_err();
- }
- }))
- .child(IconButton::new("mute-sound", speakers_icon).on_click({
- let workspace = workspace.clone();
- move |_, cx| {
- workspace
- .update(cx, |this, cx| {
- this.call_state().toggle_deafen(cx);
- })
- .log_err();
- }
- }))
- .child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
- move |_, cx| {
- workspace
- .update(cx, |this, cx| {
- this.call_state().toggle_screen_share(cx);
- })
- .log_err();
- },
- ))
+ .gap_1()
+ .child(
+ IconButton::new(
+ "mute-microphone",
+ if is_muted {
+ ui::Icon::MicMute
+ } else {
+ ui::Icon::Mic
+ },
+ )
+ .style(ButtonStyle::Subtle)
+ .selected(is_muted)
+ .on_click(move |_, cx| {
+ crate::toggle_mute(&Default::default(), cx)
+ }),
+ )
+ .child(
+ IconButton::new("mute-sound", speakers_icon)
+ .style(ButtonStyle::Subtle)
+ .selected(is_deafened.clone())
+ .tooltip(move |cx| {
+ Tooltip::with_meta(
+ "Deafen Audio",
+ None,
+ "Mic will be muted",
+ cx,
+ )
+ })
+ .on_click(move |_, cx| {
+ crate::toggle_mute(&Default::default(), cx)
+ }),
+ )
+ .child(
+ IconButton::new("screen-share", ui::Icon::Screen)
+ .style(ButtonStyle::Subtle)
+ .on_click(move |_, cx| {
+ crate::toggle_screen_sharing(&Default::default(), cx)
+ }),
+ )
.pl_2(),
),
)
})
- .map(|this| {
+ .child(h_stack().px_1p5().map(|this| {
if let Some(user) = current_user {
this.when_some(user.avatar.clone(), |this, avatar| {
- this.child(ui::Avatar::data(avatar))
+ // TODO: Finish implementing user menu popover
+ //
+ this.child(
+ popover_menu("user-menu")
+ .menu(|cx| ContextMenu::build(cx, |menu, _| menu.header("ADADA")))
+ .trigger(
+ ButtonLike::new("user-menu")
+ .child(
+ h_stack().gap_0p5().child(Avatar::data(avatar)).child(
+ IconElement::new(Icon::ChevronDown)
+ .color(Color::Muted),
+ ),
+ )
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+ )
+ .anchor(gpui::AnchorCorner::TopRight),
+ )
+ // this.child(
+ // ButtonLike::new("user-menu")
+ // .child(
+ // h_stack().gap_0p5().child(Avatar::data(avatar)).child(
+ // IconElement::new(Icon::ChevronDown).color(Color::Muted),
+ // ),
+ // )
+ // .style(ButtonStyle::Subtle)
+ // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+ // )
})
} else {
this.child(Button::new("sign_in", "Sign in").on_click(move |_, cx| {
@@ -305,7 +313,7 @@ impl Render for CollabTitlebarItem {
.detach();
}))
}
- })
+ }))
}
}
@@ -424,6 +432,83 @@ impl CollabTitlebarItem {
}
}
+ // resolve if you are in a room -> render_project_owner
+ // render_project_owner -> resolve if you are in a room -> Option<foo>
+
+ pub fn render_project_owner(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+ let host = self.project.read(cx).host()?;
+ let host = self.user_store.read(cx).get_cached_user(host.user_id)?;
+ let participant_index = self
+ .user_store
+ .read(cx)
+ .participant_indices()
+ .get(&host.id)?;
+ Some(
+ div().border().border_color(gpui::red()).child(
+ Button::new("project_owner_trigger", host.github_login.clone())
+ .color(Color::Player(participant_index.0))
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
+ ),
+ )
+ }
+
+ pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
+ let name = {
+ let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+ let worktree = worktree.read(cx);
+ worktree.root_name()
+ });
+
+ names.next().unwrap_or("")
+ };
+
+ let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
+
+ div().border().border_color(gpui::red()).child(
+ Button::new("project_name_trigger", name)
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
+ )
+ }
+
+ pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
+ let entry = {
+ let mut names_and_branches =
+ self.project.read(cx).visible_worktrees(cx).map(|worktree| {
+ let worktree = worktree.read(cx);
+ worktree.root_git_entry()
+ });
+
+ names_and_branches.next().flatten()
+ };
+
+ let branch_name = entry
+ .as_ref()
+ .and_then(RepositoryEntry::branch)
+ .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH))?;
+
+ Some(
+ div().border().border_color(gpui::red()).child(
+ Button::new("project_branch_trigger", branch_name)
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| {
+ cx.build_view(|_| {
+ Tooltip::new("Recent Branches")
+ .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+ "cmd-b",
+ // todo!() Replace with real action.
+ gpui::NoAction,
+ None,
+ )))
+ .meta("Local branches only")
+ })
+ .into()
+ }),
+ ),
+ )
+ }
+
// fn collect_title_root_names(
// &self,
// theme: Arc<Theme>,
@@ -603,21 +688,21 @@ impl CollabTitlebarItem {
cx.notify();
}
- // fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
- // let active_call = ActiveCall::global(cx);
- // let project = self.project.clone();
- // active_call
- // .update(cx, |call, cx| call.share_project(project, cx))
- // .detach_and_log_err(cx);
- // }
+ fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
+ let active_call = ActiveCall::global(cx);
+ let project = self.project.clone();
+ active_call
+ .update(cx, |call, cx| call.share_project(project, cx))
+ .detach_and_log_err(cx);
+ }
- // fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
- // let active_call = ActiveCall::global(cx);
- // let project = self.project.clone();
- // active_call
- // .update(cx, |call, cx| call.unshare_project(project, cx))
- // .log_err();
- // }
+ fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
+ let active_call = ActiveCall::global(cx);
+ let project = self.project.clone();
+ active_call
+ .update(cx, |call, cx| call.unshare_project(project, cx))
+ .log_err();
+ }
// pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
// self.user_menu.update(cx, |user_menu, cx| {
@@ -9,22 +9,21 @@ mod panel_settings;
use std::{rc::Rc, sync::Arc};
+use call::{report_call_event_for_room, ActiveCall, Room};
pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem;
use gpui::{
- point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
- WindowOptions,
+ actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
+ WindowKind, WindowOptions,
};
pub use panel_settings::{
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
};
use settings::Settings;
+use util::ResultExt;
use workspace::AppState;
-// actions!(
-// collab,
-// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
-// );
+actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
@@ -42,61 +41,61 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
// cx.add_global_action(toggle_deafen);
}
-// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
-// let call = ActiveCall::global(cx).read(cx);
-// if let Some(room) = call.room().cloned() {
-// let client = call.client();
-// let toggle_screen_sharing = room.update(cx, |room, cx| {
-// if room.is_screen_sharing() {
-// report_call_event_for_room(
-// "disable screen share",
-// room.id(),
-// room.channel_id(),
-// &client,
-// cx,
-// );
-// Task::ready(room.unshare_screen(cx))
-// } else {
-// report_call_event_for_room(
-// "enable screen share",
-// room.id(),
-// room.channel_id(),
-// &client,
-// cx,
-// );
-// room.share_screen(cx)
-// }
-// });
-// toggle_screen_sharing.detach_and_log_err(cx);
-// }
-// }
+pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
+ let call = ActiveCall::global(cx).read(cx);
+ if let Some(room) = call.room().cloned() {
+ let client = call.client();
+ let toggle_screen_sharing = room.update(cx, |room, cx| {
+ if room.is_screen_sharing() {
+ report_call_event_for_room(
+ "disable screen share",
+ room.id(),
+ room.channel_id(),
+ &client,
+ cx,
+ );
+ Task::ready(room.unshare_screen(cx))
+ } else {
+ report_call_event_for_room(
+ "enable screen share",
+ room.id(),
+ room.channel_id(),
+ &client,
+ cx,
+ );
+ room.share_screen(cx)
+ }
+ });
+ toggle_screen_sharing.detach_and_log_err(cx);
+ }
+}
-// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
-// let call = ActiveCall::global(cx).read(cx);
-// if let Some(room) = call.room().cloned() {
-// let client = call.client();
-// room.update(cx, |room, cx| {
-// let operation = if room.is_muted(cx) {
-// "enable microphone"
-// } else {
-// "disable microphone"
-// };
-// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
+pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
+ let call = ActiveCall::global(cx).read(cx);
+ if let Some(room) = call.room().cloned() {
+ let client = call.client();
+ room.update(cx, |room, cx| {
+ let operation = if room.is_muted(cx) {
+ "enable microphone"
+ } else {
+ "disable microphone"
+ };
+ report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
-// room.toggle_mute(cx)
-// })
-// .map(|task| task.detach_and_log_err(cx))
-// .log_err();
-// }
-// }
+ room.toggle_mute(cx)
+ })
+ .map(|task| task.detach_and_log_err(cx))
+ .log_err();
+ }
+}
-// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
-// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
-// room.update(cx, Room::toggle_deafen)
-// .map(|task| task.detach_and_log_err(cx))
-// .log_err();
-// }
-// }
+pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
+ if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
+ room.update(cx, Room::toggle_deafen)
+ .map(|task| task.detach_and_log_err(cx))
+ .log_err();
+ }
+}
fn notification_window_options(
screen: Rc<dyn PlatformDisplay>,
@@ -311,7 +311,11 @@ impl PickerDelegate for CommandPaletteDelegate {
command.name.clone(),
r#match.positions.clone(),
))
- .children(KeyBinding::for_action(&*command.action, cx)),
+ .children(KeyBinding::for_action_in(
+ &*command.action,
+ &self.previous_focus_handle,
+ cx,
+ )),
),
)
}
@@ -45,6 +45,6 @@ fs = { path = "../fs", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
language = { package = "language2", path = "../language2", features = ["test-support"] }
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
-rpc = { path = "../rpc", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
@@ -1002,229 +1002,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
}
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use gpui::{executor::Deterministic, TestAppContext};
-
-// #[gpui::test(iterations = 10)]
-// async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
-// deterministic.forbid_parking();
-// let (copilot, mut lsp) = Copilot::fake(cx);
-
-// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
-// let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
-// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-// .await,
-// lsp::DidOpenTextDocumentParams {
-// text_document: lsp::TextDocumentItem::new(
-// buffer_1_uri.clone(),
-// "plaintext".into(),
-// 0,
-// "Hello".into()
-// ),
-// }
-// );
-
-// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
-// let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
-// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-// .await,
-// lsp::DidOpenTextDocumentParams {
-// text_document: lsp::TextDocumentItem::new(
-// buffer_2_uri.clone(),
-// "plaintext".into(),
-// 0,
-// "Goodbye".into()
-// ),
-// }
-// );
-
-// buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
-// .await,
-// lsp::DidChangeTextDocumentParams {
-// text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
-// content_changes: vec![lsp::TextDocumentContentChangeEvent {
-// range: Some(lsp::Range::new(
-// lsp::Position::new(0, 5),
-// lsp::Position::new(0, 5)
-// )),
-// range_length: None,
-// text: " world".into(),
-// }],
-// }
-// );
-
-// // Ensure updates to the file are reflected in the LSP.
-// buffer_1
-// .update(cx, |buffer, cx| {
-// buffer.file_updated(
-// Arc::new(File {
-// abs_path: "/root/child/buffer-1".into(),
-// path: Path::new("child/buffer-1").into(),
-// }),
-// cx,
-// )
-// })
-// .await;
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-// .await,
-// lsp::DidCloseTextDocumentParams {
-// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
-// }
-// );
-// let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-// .await,
-// lsp::DidOpenTextDocumentParams {
-// text_document: lsp::TextDocumentItem::new(
-// buffer_1_uri.clone(),
-// "plaintext".into(),
-// 1,
-// "Hello world".into()
-// ),
-// }
-// );
-
-// // Ensure all previously-registered buffers are closed when signing out.
-// lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
-// Ok(request::SignOutResult {})
-// });
-// copilot
-// .update(cx, |copilot, cx| copilot.sign_out(cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-// .await,
-// lsp::DidCloseTextDocumentParams {
-// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
-// }
-// );
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-// .await,
-// lsp::DidCloseTextDocumentParams {
-// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
-// }
-// );
-
-// // Ensure all previously-registered buffers are re-opened when signing in.
-// lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
-// Ok(request::SignInInitiateResult::AlreadySignedIn {
-// user: "user-1".into(),
-// })
-// });
-// copilot
-// .update(cx, |copilot, cx| copilot.sign_in(cx))
-// .await
-// .unwrap();
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-// .await,
-// lsp::DidOpenTextDocumentParams {
-// text_document: lsp::TextDocumentItem::new(
-// buffer_2_uri.clone(),
-// "plaintext".into(),
-// 0,
-// "Goodbye".into()
-// ),
-// }
-// );
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
-// .await,
-// lsp::DidOpenTextDocumentParams {
-// text_document: lsp::TextDocumentItem::new(
-// buffer_1_uri.clone(),
-// "plaintext".into(),
-// 0,
-// "Hello world".into()
-// ),
-// }
-// );
-
-// // Dropping a buffer causes it to be closed on the LSP side as well.
-// cx.update(|_| drop(buffer_2));
-// assert_eq!(
-// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
-// .await,
-// lsp::DidCloseTextDocumentParams {
-// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
-// }
-// );
-// }
-
-// struct File {
-// abs_path: PathBuf,
-// path: Arc<Path>,
-// }
-
-// impl language2::File for File {
-// fn as_local(&self) -> Option<&dyn language2::LocalFile> {
-// Some(self)
-// }
-
-// fn mtime(&self) -> std::time::SystemTime {
-// unimplemented!()
-// }
-
-// fn path(&self) -> &Arc<Path> {
-// &self.path
-// }
-
-// fn full_path(&self, _: &AppContext) -> PathBuf {
-// unimplemented!()
-// }
-
-// fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
-// unimplemented!()
-// }
-
-// fn is_deleted(&self) -> bool {
-// unimplemented!()
-// }
-
-// fn as_any(&self) -> &dyn std::any::Any {
-// unimplemented!()
-// }
-
-// fn to_proto(&self) -> rpc::proto::File {
-// unimplemented!()
-// }
-
-// fn worktree_id(&self) -> usize {
-// 0
-// }
-// }
-
-// impl language::LocalFile for File {
-// fn abs_path(&self, _: &AppContext) -> PathBuf {
-// self.abs_path.clone()
-// }
-
-// fn load(&self, _: &AppContext) -> Task<Result<String>> {
-// unimplemented!()
-// }
-
-// fn buffer_reloaded(
-// &self,
-// _: u64,
-// _: &clock::Global,
-// _: language::RopeFingerprint,
-// _: language::LineEnding,
-// _: std::time::SystemTime,
-// _: &mut AppContext,
-// ) {
-// unimplemented!()
-// }
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::TestAppContext;
+
+ #[gpui::test(iterations = 10)]
+ async fn test_buffer_management(cx: &mut TestAppContext) {
+ let (copilot, mut lsp) = Copilot::fake(cx);
+
+ let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
+ let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
+ .parse()
+ .unwrap();
+ copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Hello".into()
+ ),
+ }
+ );
+
+ let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
+ let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
+ .parse()
+ .unwrap();
+ copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_2_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Goodbye".into()
+ ),
+ }
+ );
+
+ buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
+ .await,
+ lsp::DidChangeTextDocumentParams {
+ text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
+ content_changes: vec![lsp::TextDocumentContentChangeEvent {
+ range: Some(lsp::Range::new(
+ lsp::Position::new(0, 5),
+ lsp::Position::new(0, 5)
+ )),
+ range_length: None,
+ text: " world".into(),
+ }],
+ }
+ );
+
+ // Ensure updates to the file are reflected in the LSP.
+ buffer_1.update(cx, |buffer, cx| {
+ buffer.file_updated(
+ Arc::new(File {
+ abs_path: "/root/child/buffer-1".into(),
+ path: Path::new("child/buffer-1").into(),
+ }),
+ cx,
+ )
+ });
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
+ }
+ );
+ let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 1,
+ "Hello world".into()
+ ),
+ }
+ );
+
+ // Ensure all previously-registered buffers are closed when signing out.
+ lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
+ Ok(request::SignOutResult {})
+ });
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_out(cx))
+ .await
+ .unwrap();
+ // todo!() po: these notifications now happen in reverse order?
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
+ }
+ );
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
+ }
+ );
+
+ // Ensure all previously-registered buffers are re-opened when signing in.
+ lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
+ Ok(request::SignInInitiateResult::AlreadySignedIn {
+ user: "user-1".into(),
+ })
+ });
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .await
+ .unwrap();
+
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_1_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Hello world".into()
+ ),
+ }
+ );
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
+ .await,
+ lsp::DidOpenTextDocumentParams {
+ text_document: lsp::TextDocumentItem::new(
+ buffer_2_uri.clone(),
+ "plaintext".into(),
+ 0,
+ "Goodbye".into()
+ ),
+ }
+ );
+ // Dropping a buffer causes it to be closed on the LSP side as well.
+ cx.update(|_| drop(buffer_2));
+ assert_eq!(
+ lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
+ .await,
+ lsp::DidCloseTextDocumentParams {
+ text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
+ }
+ );
+ }
+
+ struct File {
+ abs_path: PathBuf,
+ path: Arc<Path>,
+ }
+
+ impl language::File for File {
+ fn as_local(&self) -> Option<&dyn language::LocalFile> {
+ Some(self)
+ }
+
+ fn mtime(&self) -> std::time::SystemTime {
+ unimplemented!()
+ }
+
+ fn path(&self) -> &Arc<Path> {
+ &self.path
+ }
+
+ fn full_path(&self, _: &AppContext) -> PathBuf {
+ unimplemented!()
+ }
+
+ fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
+ unimplemented!()
+ }
+
+ fn is_deleted(&self) -> bool {
+ unimplemented!()
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ unimplemented!()
+ }
+
+ fn to_proto(&self) -> rpc::proto::File {
+ unimplemented!()
+ }
+
+ fn worktree_id(&self) -> usize {
+ 0
+ }
+ }
+
+ impl language::LocalFile for File {
+ fn abs_path(&self, _: &AppContext) -> PathBuf {
+ self.abs_path.clone()
+ }
+
+ fn load(&self, _: &AppContext) -> Task<Result<String>> {
+ unimplemented!()
+ }
+
+ fn buffer_reloaded(
+ &self,
+ _: u64,
+ _: &clock::Global,
+ _: language::RopeFingerprint,
+ _: language::LineEnding,
+ _: std::time::SystemTime,
+ _: &mut AppContext,
+ ) {
+ unimplemented!()
+ }
+ }
+}
@@ -0,0 +1,27 @@
+[package]
+name = "copilot_button2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot_button.rs"
+doctest = false
+
+[dependencies]
+copilot = { package = "copilot2", path = "../copilot2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+smol.workspace = true
+futures.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -0,0 +1,370 @@
+#![allow(unused)]
+use anyhow::Result;
+use copilot::{Copilot, SignOut, Status};
+use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
+use gpui::{
+ div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
+ ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
+};
+use language::{
+ language_settings::{self, all_language_settings, AllLanguageSettings},
+ File, Language,
+};
+use settings::{update_settings_file, Settings, SettingsStore};
+use std::{path::Path, sync::Arc};
+use util::{paths, ResultExt};
+use workspace::{
+ create_and_open_local_file,
+ item::ItemHandle,
+ ui::{
+ popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
+ },
+ StatusItemView, Toast, Workspace,
+};
+use zed_actions::OpenBrowser;
+
+const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
+const COPILOT_STARTING_TOAST_ID: usize = 1337;
+const COPILOT_ERROR_TOAST_ID: usize = 1338;
+
+pub struct CopilotButton {
+ editor_subscription: Option<(Subscription, usize)>,
+ editor_enabled: Option<bool>,
+ language: Option<Arc<Language>>,
+ file: Option<Arc<dyn File>>,
+ fs: Arc<dyn Fs>,
+}
+
+impl Render for CopilotButton {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let all_language_settings = all_language_settings(None, cx);
+ if !all_language_settings.copilot.feature_enabled {
+ return div();
+ }
+
+ let Some(copilot) = Copilot::global(cx) else {
+ return div();
+ };
+ let status = copilot.read(cx).status();
+
+ let enabled = self
+ .editor_enabled
+ .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
+
+ let icon = match status {
+ Status::Error(_) => Icon::CopilotError,
+ Status::Authorized => {
+ if enabled {
+ Icon::Copilot
+ } else {
+ Icon::CopilotDisabled
+ }
+ }
+ _ => Icon::CopilotInit,
+ };
+
+ if let Status::Error(e) = status {
+ return div().child(
+ IconButton::new("copilot-error", icon)
+ .on_click(cx.listener(move |this, _, cx| {
+ if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+ workspace.update(cx, |workspace, cx| {
+ workspace.show_toast(
+ Toast::new(
+ COPILOT_ERROR_TOAST_ID,
+ format!("Copilot can't be started: {}", e),
+ )
+ .on_click(
+ "Reinstall Copilot",
+ |cx| {
+ if let Some(copilot) = Copilot::global(cx) {
+ copilot
+ .update(cx, |copilot, cx| copilot.reinstall(cx))
+ .detach();
+ }
+ },
+ ),
+ cx,
+ );
+ });
+ }
+ }))
+ .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+ );
+ }
+ let this = cx.view().clone();
+
+ div().child(
+ popover_menu("copilot")
+ .menu(move |cx| match status {
+ Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
+ _ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
+ })
+ .anchor(AnchorCorner::BottomRight)
+ .trigger(
+ IconButton::new("copilot-icon", icon)
+ .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+ ),
+ )
+ }
+}
+
+impl CopilotButton {
+ pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
+ Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
+
+ cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
+ .detach();
+
+ Self {
+ editor_subscription: None,
+ editor_enabled: None,
+ language: None,
+ file: None,
+ fs,
+ }
+ }
+
+ pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+ let fs = self.fs.clone();
+ ContextMenu::build(cx, |menu, cx| {
+ menu.entry("Sign In", initiate_sign_in)
+ .entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
+ })
+ }
+
+ pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+ let fs = self.fs.clone();
+
+ return ContextMenu::build(cx, move |mut menu, cx| {
+ if let Some(language) = self.language.clone() {
+ let fs = fs.clone();
+ let language_enabled =
+ language_settings::language_settings(Some(&language), None, cx)
+ .show_copilot_suggestions;
+
+ menu = menu.entry(
+ format!(
+ "{} Suggestions for {}",
+ if language_enabled { "Hide" } else { "Show" },
+ language.name()
+ ),
+ move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
+ );
+ }
+
+ let settings = AllLanguageSettings::get_global(cx);
+
+ if let Some(file) = &self.file {
+ let path = file.path().clone();
+ let path_enabled = settings.copilot_enabled_for_path(&path);
+
+ menu = menu.entry(
+ format!(
+ "{} Suggestions for This Path",
+ if path_enabled { "Hide" } else { "Show" }
+ ),
+ move |cx| {
+ if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+ if let Ok(workspace) = workspace.root_view(cx) {
+ let workspace = workspace.downgrade();
+ cx.spawn(|cx| {
+ configure_disabled_globs(
+ workspace,
+ path_enabled.then_some(path.clone()),
+ cx,
+ )
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ },
+ );
+ }
+
+ let globally_enabled = settings.copilot_enabled(None, None);
+ menu.entry(
+ if globally_enabled {
+ "Hide Suggestions for All Files"
+ } else {
+ "Show Suggestions for All Files"
+ },
+ move |cx| toggle_copilot_globally(fs.clone(), cx),
+ )
+ .separator()
+ .link(
+ "Copilot Settings",
+ OpenBrowser {
+ url: COPILOT_SETTINGS_URL.to_string(),
+ }
+ .boxed_clone(),
+ )
+ .action("Sign Out", SignOut.boxed_clone())
+ });
+ }
+
+ pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ let editor = editor.read(cx);
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let suggestion_anchor = editor.selections.newest_anchor().start;
+ let language = snapshot.language_at(suggestion_anchor);
+ let file = snapshot.file_at(suggestion_anchor).cloned();
+
+ self.editor_enabled = Some(
+ all_language_settings(self.file.as_ref(), cx)
+ .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
+ );
+ self.language = language.cloned();
+ self.file = file;
+
+ cx.notify()
+ }
+}
+
+impl StatusItemView for CopilotButton {
+ fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+ if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
+ self.editor_subscription = Some((
+ cx.observe(&editor, Self::update_enabled),
+ editor.entity_id().as_u64() as usize,
+ ));
+ self.update_enabled(editor, cx);
+ } else {
+ self.language = None;
+ self.editor_subscription = None;
+ self.editor_enabled = None;
+ }
+ cx.notify();
+ }
+}
+
+async fn configure_disabled_globs(
+ workspace: WeakView<Workspace>,
+ path_to_disable: Option<Arc<Path>>,
+ mut cx: AsyncWindowContext,
+) -> Result<()> {
+ let settings_editor = workspace
+ .update(&mut cx, |_, cx| {
+ create_and_open_local_file(&paths::SETTINGS, cx, || {
+ settings::initial_user_settings_content().as_ref().into()
+ })
+ })?
+ .await?
+ .downcast::<Editor>()
+ .unwrap();
+
+ settings_editor.downgrade().update(&mut cx, |item, cx| {
+ let text = item.buffer().read(cx).snapshot(cx).text();
+
+ let settings = cx.global::<SettingsStore>();
+ let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
+ let copilot = file.copilot.get_or_insert_with(Default::default);
+ let globs = copilot.disabled_globs.get_or_insert_with(|| {
+ settings
+ .get::<AllLanguageSettings>(None)
+ .copilot
+ .disabled_globs
+ .iter()
+ .map(|glob| glob.glob().to_string())
+ .collect()
+ });
+
+ if let Some(path_to_disable) = &path_to_disable {
+ globs.push(path_to_disable.to_string_lossy().into_owned());
+ } else {
+ globs.clear();
+ }
+ });
+
+ if !edits.is_empty() {
+ item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+ selections.select_ranges(edits.iter().map(|e| e.0.clone()));
+ });
+
+ // When *enabling* a path, don't actually perform an edit, just select the range.
+ if path_to_disable.is_some() {
+ item.edit(edits.iter().cloned(), cx);
+ }
+ }
+ })?;
+
+ anyhow::Ok(())
+}
+
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+ });
+}
+
+fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions =
+ all_language_settings(None, cx).copilot_enabled(Some(&language), None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.languages
+ .entry(language.name())
+ .or_default()
+ .show_copilot_suggestions = Some(!show_copilot_suggestions);
+ });
+}
+
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.features.get_or_insert(Default::default()).copilot = Some(false);
+ });
+}
+
+fn initiate_sign_in(cx: &mut WindowContext) {
+ let Some(copilot) = Copilot::global(cx) else {
+ return;
+ };
+ let status = copilot.read(cx).status();
+
+ match status {
+ Status::Starting { task } => {
+ let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+ return;
+ };
+
+ let Ok(workspace) = workspace.update(cx, |workspace, cx| {
+ workspace.show_toast(
+ Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
+ cx,
+ );
+ workspace.weak_handle()
+ }) else {
+ return;
+ };
+
+ cx.spawn(|mut cx| async move {
+ task.await;
+ if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
+ workspace
+ .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
+ Status::Authorized => workspace.show_toast(
+ Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
+ cx,
+ ),
+ _ => {
+ workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .detach_and_log_err(cx);
+ }
+ })
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ _ => {
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .detach_and_log_err(cx);
+ }
+ }
+}
@@ -774,24 +774,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
Arc::new(move |_| {
h_stack()
.id("diagnostic header")
- .gap_3()
- .bg(gpui::red())
- .map(|stack| {
- let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
- IconElement::new(Icon::XCircle).color(Color::Error)
- } else {
- IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
- };
-
- stack.child(div().pl_8().child(icon))
- })
- .when_some(diagnostic.source.as_ref(), |stack, source| {
- stack.child(Label::new(format!("{source}:")).color(Color::Accent))
- })
- .child(HighlightedLabel::new(message.clone(), highlights.clone()))
- .when_some(diagnostic.code.as_ref(), |stack, code| {
- stack.child(Label::new(code.clone()))
- })
+ .py_2()
+ .pl_10()
+ .pr_5()
+ .w_full()
+ .justify_between()
+ .gap_2()
+ .child(
+ h_stack()
+ .gap_3()
+ .map(|stack| {
+ let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
+ IconElement::new(Icon::XCircle).color(Color::Error)
+ } else {
+ IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
+ };
+ stack.child(icon)
+ })
+ .child(
+ h_stack()
+ .gap_1()
+ .child(HighlightedLabel::new(message.clone(), highlights.clone()))
+ .when_some(diagnostic.code.as_ref(), |stack, code| {
+ stack.child(Label::new(format!("({code})")).color(Color::Muted))
+ }),
+ ),
+ )
+ .child(
+ h_stack()
+ .gap_1()
+ .when_some(diagnostic.source.as_ref(), |stack, source| {
+ stack.child(Label::new(format!("{source}")).color(Color::Muted))
+ }),
+ )
.into_any_element()
})
}
@@ -802,11 +817,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
label.into_any_element()
} else {
h_stack()
- .bg(gpui::red())
- .child(IconElement::new(Icon::XCircle))
- .child(Label::new(summary.error_count.to_string()))
- .child(IconElement::new(Icon::ExclamationTriangle))
- .child(Label::new(summary.warning_count.to_string()))
+ .gap_1()
+ .when(summary.error_count > 0, |then| {
+ then.child(
+ h_stack()
+ .gap_1()
+ .child(IconElement::new(Icon::XCircle).color(Color::Error))
+ .child(Label::new(summary.error_count.to_string())),
+ )
+ })
+ .when(summary.warning_count > 0, |then| {
+ then.child(
+ h_stack()
+ .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
+ .child(Label::new(summary.warning_count.to_string())),
+ )
+ })
.into_any_element()
}
}
@@ -990,905 +990,869 @@ pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterat
})
}
-// #[cfg(test)]
-// pub mod tests {
-// use super::*;
-// use crate::{
-// movement,
-// test::{editor_test_context::EditorTestContext, marked_display_snapshot},
-// };
-// use gpui::{AppContext, Hsla};
-// use language::{
-// language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
-// Buffer, Language, LanguageConfig, SelectionGoal,
-// };
-// use project::Project;
-// use rand::{prelude::*, Rng};
-// use settings::SettingsStore;
-// use smol::stream::StreamExt;
-// use std::{env, sync::Arc};
-// use theme::SyntaxTheme;
-// use util::test::{marked_text_ranges, sample_text};
-// use Bias::*;
-
-// #[gpui::test(iterations = 100)]
-// async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-// cx.foreground().set_block_on_ticks(0..=50);
-// cx.foreground().forbid_parking();
-// let operations = env::var("OPERATIONS")
-// .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-// .unwrap_or(10);
-
-// let font_cache = cx.font_cache().clone();
-// let mut tab_size = rng.gen_range(1..=4);
-// let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
-// let excerpt_header_height = rng.gen_range(1..=5);
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 14.0;
-// let max_wrap_width = 300.0;
-// let mut wrap_width = if rng.gen_bool(0.1) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=max_wrap_width))
-// };
-
-// log::info!("tab size: {}", tab_size);
-// log::info!("wrap width: {:?}", wrap_width);
-
-// cx.update(|cx| {
-// init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
-// });
-
-// let buffer = cx.update(|cx| {
-// if rng.gen() {
-// let len = rng.gen_range(0..10);
-// let text = util::RandomCharIter::new(&mut rng)
-// .take(len)
-// .collect::<String>();
-// MultiBuffer::build_simple(&text, cx)
-// } else {
-// MultiBuffer::build_random(&mut rng, cx)
-// }
-// });
-
-// let map = cx.add_model(|cx| {
-// DisplayMap::new(
-// buffer.clone(),
-// font_id,
-// font_size,
-// wrap_width,
-// buffer_start_excerpt_header_height,
-// excerpt_header_height,
-// cx,
-// )
-// });
-// let mut notifications = observe(&map, cx);
-// let mut fold_count = 0;
-// let mut blocks = Vec::new();
-
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-// log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-// log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
-// log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
-// log::info!("block text: {:?}", snapshot.block_snapshot.text());
-// log::info!("display text: {:?}", snapshot.text());
-
-// for _i in 0..operations {
-// match rng.gen_range(0..100) {
-// 0..=19 => {
-// wrap_width = if rng.gen_bool(0.2) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=max_wrap_width))
-// };
-// log::info!("setting wrap width to {:?}", wrap_width);
-// map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-// }
-// 20..=29 => {
-// let mut tab_sizes = vec![1, 2, 3, 4];
-// tab_sizes.remove((tab_size - 1) as usize);
-// tab_size = *tab_sizes.choose(&mut rng).unwrap();
-// log::info!("setting tab size to {:?}", tab_size);
-// cx.update(|cx| {
-// cx.update_global::<SettingsStore, _, _>(|store, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, |s| {
-// s.defaults.tab_size = NonZeroU32::new(tab_size);
-// });
-// });
-// });
-// }
-// 30..=44 => {
-// map.update(cx, |map, cx| {
-// if rng.gen() || blocks.is_empty() {
-// let buffer = map.snapshot(cx).buffer_snapshot;
-// let block_properties = (0..rng.gen_range(1..=1))
-// .map(|_| {
-// let position =
-// buffer.anchor_after(buffer.clip_offset(
-// rng.gen_range(0..=buffer.len()),
-// Bias::Left,
-// ));
-
-// let disposition = if rng.gen() {
-// BlockDisposition::Above
-// } else {
-// BlockDisposition::Below
-// };
-// let height = rng.gen_range(1..5);
-// log::info!(
-// "inserting block {:?} {:?} with height {}",
-// disposition,
-// position.to_point(&buffer),
-// height
-// );
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position,
-// height,
-// disposition,
-// render: Arc::new(|_| Empty::new().into_any()),
-// }
-// })
-// .collect::<Vec<_>>();
-// blocks.extend(map.insert_blocks(block_properties, cx));
-// } else {
-// blocks.shuffle(&mut rng);
-// let remove_count = rng.gen_range(1..=4.min(blocks.len()));
-// let block_ids_to_remove = (0..remove_count)
-// .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
-// .collect();
-// log::info!("removing block ids {:?}", block_ids_to_remove);
-// map.remove_blocks(block_ids_to_remove, cx);
-// }
-// });
-// }
-// 45..=79 => {
-// let mut ranges = Vec::new();
-// for _ in 0..rng.gen_range(1..=3) {
-// buffer.read_with(cx, |buffer, cx| {
-// let buffer = buffer.read(cx);
-// let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
-// let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
-// ranges.push(start..end);
-// });
-// }
-
-// if rng.gen() && fold_count > 0 {
-// log::info!("unfolding ranges: {:?}", ranges);
-// map.update(cx, |map, cx| {
-// map.unfold(ranges, true, cx);
-// });
-// } else {
-// log::info!("folding ranges: {:?}", ranges);
-// map.update(cx, |map, cx| {
-// map.fold(ranges, cx);
-// });
-// }
-// }
-// _ => {
-// buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
-// }
-// }
-
-// if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
-// notifications.next().await.unwrap();
-// }
-
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// fold_count = snapshot.fold_count();
-// log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
-// log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
-// log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
-// log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
-// log::info!("block text: {:?}", snapshot.block_snapshot.text());
-// log::info!("display text: {:?}", snapshot.text());
-
-// // Line boundaries
-// let buffer = &snapshot.buffer_snapshot;
-// for _ in 0..5 {
-// let row = rng.gen_range(0..=buffer.max_point().row);
-// let column = rng.gen_range(0..=buffer.line_len(row));
-// let point = buffer.clip_point(Point::new(row, column), Left);
-
-// let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
-// let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
-
-// assert!(prev_buffer_bound <= point);
-// assert!(next_buffer_bound >= point);
-// assert_eq!(prev_buffer_bound.column, 0);
-// assert_eq!(prev_display_bound.column(), 0);
-// if next_buffer_bound < buffer.max_point() {
-// assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
-// }
-
-// assert_eq!(
-// prev_display_bound,
-// prev_buffer_bound.to_display_point(&snapshot),
-// "row boundary before {:?}. reported buffer row boundary: {:?}",
-// point,
-// prev_buffer_bound
-// );
-// assert_eq!(
-// next_display_bound,
-// next_buffer_bound.to_display_point(&snapshot),
-// "display row boundary after {:?}. reported buffer row boundary: {:?}",
-// point,
-// next_buffer_bound
-// );
-// assert_eq!(
-// prev_buffer_bound,
-// prev_display_bound.to_point(&snapshot),
-// "row boundary before {:?}. reported display row boundary: {:?}",
-// point,
-// prev_display_bound
-// );
-// assert_eq!(
-// next_buffer_bound,
-// next_display_bound.to_point(&snapshot),
-// "row boundary after {:?}. reported display row boundary: {:?}",
-// point,
-// next_display_bound
-// );
-// }
-
-// // Movement
-// let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
-// let max_point = snapshot.clip_point(snapshot.max_point(), Right);
-// for _ in 0..5 {
-// let row = rng.gen_range(0..=snapshot.max_point().row());
-// let column = rng.gen_range(0..=snapshot.line_len(row));
-// let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
-
-// log::info!("Moving from point {:?}", point);
-
-// let moved_right = movement::right(&snapshot, point);
-// log::info!("Right {:?}", moved_right);
-// if point < max_point {
-// assert!(moved_right > point);
-// if point.column() == snapshot.line_len(point.row())
-// || snapshot.soft_wrap_indent(point.row()).is_some()
-// && point.column() == snapshot.line_len(point.row()) - 1
-// {
-// assert!(moved_right.row() > point.row());
-// }
-// } else {
-// assert_eq!(moved_right, point);
-// }
-
-// let moved_left = movement::left(&snapshot, point);
-// log::info!("Left {:?}", moved_left);
-// if point > min_point {
-// assert!(moved_left < point);
-// if point.column() == 0 {
-// assert!(moved_left.row() < point.row());
-// }
-// } else {
-// assert_eq!(moved_left, point);
-// }
-// }
-// }
-// }
-
-// #[gpui::test(retries = 5)]
-// async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
-// cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-// cx.update(|cx| {
-// init_test(cx, |_| {});
-// });
-
-// let mut cx = EditorTestContext::new(cx).await;
-// let editor = cx.editor.clone();
-// let window = cx.window.clone();
-
-// cx.update_window(window, |cx| {
-// let text_layout_details =
-// editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
-
-// let font_cache = cx.font_cache().clone();
-
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 12.0;
-// let wrap_width = Some(64.);
-
-// let text = "one two three four five\nsix seven eight";
-// let buffer = MultiBuffer::build_simple(text, cx);
-// let map = cx.add_model(|cx| {
-// DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
-// });
-
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// assert_eq!(
-// snapshot.text_chunks(0).collect::<String>(),
-// "one two \nthree four \nfive\nsix seven \neight"
-// );
-// assert_eq!(
-// snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
-// DisplayPoint::new(0, 7)
-// );
-// assert_eq!(
-// snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
-// DisplayPoint::new(1, 0)
-// );
-// assert_eq!(
-// movement::right(&snapshot, DisplayPoint::new(0, 7)),
-// DisplayPoint::new(1, 0)
-// );
-// assert_eq!(
-// movement::left(&snapshot, DisplayPoint::new(1, 0)),
-// DisplayPoint::new(0, 7)
-// );
-
-// let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
-// assert_eq!(
-// movement::up(
-// &snapshot,
-// DisplayPoint::new(1, 10),
-// SelectionGoal::None,
-// false,
-// &text_layout_details,
-// ),
-// (
-// DisplayPoint::new(0, 7),
-// SelectionGoal::HorizontalPosition(x)
-// )
-// );
-// assert_eq!(
-// movement::down(
-// &snapshot,
-// DisplayPoint::new(0, 7),
-// SelectionGoal::HorizontalPosition(x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(1, 10),
-// SelectionGoal::HorizontalPosition(x)
-// )
-// );
-// assert_eq!(
-// movement::down(
-// &snapshot,
-// DisplayPoint::new(1, 10),
-// SelectionGoal::HorizontalPosition(x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(2, 4),
-// SelectionGoal::HorizontalPosition(x)
-// )
-// );
-
-// let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
-// buffer.update(cx, |buffer, cx| {
-// buffer.edit([(ix..ix, "and ")], None, cx);
-// });
-
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// assert_eq!(
-// snapshot.text_chunks(1).collect::<String>(),
-// "three four \nfive\nsix and \nseven eight"
-// );
-
-// // Re-wrap on font size changes
-// map.update(cx, |map, cx| map.set_font_with_size(font_id, font_size + 3., cx));
-
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// assert_eq!(
-// snapshot.text_chunks(1).collect::<String>(),
-// "three \nfour five\nsix and \nseven \neight"
-// )
-// });
-// }
-
-// #[gpui::test]
-// fn test_text_chunks(cx: &mut gpui::AppContext) {
-// init_test(cx, |_| {});
-
-// let text = sample_text(6, 6, 'a');
-// let buffer = MultiBuffer::build_simple(&text, cx);
-// 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 map =
-// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
-
-// buffer.update(cx, |buffer, cx| {
-// buffer.edit(
-// vec![
-// (Point::new(1, 0)..Point::new(1, 0), "\t"),
-// (Point::new(1, 1)..Point::new(1, 1), "\t"),
-// (Point::new(2, 1)..Point::new(2, 1), "\t"),
-// ],
-// None,
-// cx,
-// )
-// });
-
-// assert_eq!(
-// map.update(cx, |map, cx| map.snapshot(cx))
-// .text_chunks(1)
-// .collect::<String>()
-// .lines()
-// .next(),
-// Some(" b bbbbb")
-// );
-// assert_eq!(
-// map.update(cx, |map, cx| map.snapshot(cx))
-// .text_chunks(2)
-// .collect::<String>()
-// .lines()
-// .next(),
-// Some("c ccccc")
-// );
-// }
-
-// #[gpui::test]
-// async fn test_chunks(cx: &mut gpui::TestAppContext) {
-// use unindent::Unindent as _;
-
-// let text = r#"
-// fn outer() {}
-
-// mod module {
-// fn inner() {}
-// }"#
-// .unindent();
-
-// let theme = SyntaxTheme::new(vec![
-// ("mod.body".to_string(), Hsla::red().into()),
-// ("fn.name".to_string(), Hsla::blue().into()),
-// ]);
-// let language = Arc::new(
-// Language::new(
-// LanguageConfig {
-// name: "Test".into(),
-// path_suffixes: vec![".test".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// )
-// .with_highlights_query(
-// r#"
-// (mod_item name: (identifier) body: _ @mod.body)
-// (function_item name: (identifier) @fn.name)
-// "#,
-// )
-// .unwrap(),
-// );
-// language.set_theme(&theme);
-
-// cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
-
-// let buffer = cx
-// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-// buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-
-// let font_cache = cx.font_cache();
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 14.0;
-
-// let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
-// vec![
-// ("fn ".to_string(), None),
-// ("outer".to_string(), Some(Hsla::blue())),
-// ("() {}\n\nmod module ".to_string(), None),
-// ("{\n fn ".to_string(), Some(Hsla::red())),
-// ("inner".to_string(), Some(Hsla::blue())),
-// ("() {}\n}".to_string(), Some(Hsla::red())),
-// ]
-// );
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
-// vec![
-// (" fn ".to_string(), Some(Hsla::red())),
-// ("inner".to_string(), Some(Hsla::blue())),
-// ("() {}\n}".to_string(), Some(Hsla::red())),
-// ]
-// );
-
-// map.update(cx, |map, cx| {
-// map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
-// });
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
-// vec![
-// ("fn ".to_string(), None),
-// ("out".to_string(), Some(Hsla::blue())),
-// ("⋯".to_string(), None),
-// (" fn ".to_string(), Some(Hsla::red())),
-// ("inner".to_string(), Some(Hsla::blue())),
-// ("() {}\n}".to_string(), Some(Hsla::red())),
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
-// use unindent::Unindent as _;
-
-// cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
-
-// let text = r#"
-// fn outer() {}
-
-// mod module {
-// fn inner() {}
-// }"#
-// .unindent();
-
-// let theme = SyntaxTheme::new(vec![
-// ("mod.body".to_string(), Hsla::red().into()),
-// ("fn.name".to_string(), Hsla::blue().into()),
-// ]);
-// let language = Arc::new(
-// Language::new(
-// LanguageConfig {
-// name: "Test".into(),
-// path_suffixes: vec![".test".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// )
-// .with_highlights_query(
-// r#"
-// (mod_item name: (identifier) body: _ @mod.body)
-// (function_item name: (identifier) @fn.name)
-// "#,
-// )
-// .unwrap(),
-// );
-// language.set_theme(&theme);
-
-// cx.update(|cx| init_test(cx, |_| {}));
-
-// let buffer = cx
-// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-// buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-
-// let font_cache = cx.font_cache();
-
-// let family_id = font_cache
-// .load_family(&["Courier"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 16.0;
-
-// let map =
-// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
-// [
-// ("fn \n".to_string(), None),
-// ("oute\nr".to_string(), Some(Hsla::blue())),
-// ("() \n{}\n\n".to_string(), None),
-// ]
-// );
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
-// [("{}\n\n".to_string(), None)]
-// );
-
-// map.update(cx, |map, cx| {
-// map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
-// });
-// assert_eq!(
-// cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
-// [
-// ("out".to_string(), Some(Hsla::blue())),
-// ("⋯\n".to_string(), None),
-// (" \nfn ".to_string(), Some(Hsla::red())),
-// ("i\n".to_string(), Some(Hsla::blue()))
-// ]
-// );
-// }
-
-// #[gpui::test]
-// async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
-// cx.update(|cx| init_test(cx, |_| {}));
-
-// let theme = SyntaxTheme::new(vec![
-// ("operator".to_string(), Hsla::red().into()),
-// ("string".to_string(), Hsla::green().into()),
-// ]);
-// let language = Arc::new(
-// Language::new(
-// LanguageConfig {
-// name: "Test".into(),
-// path_suffixes: vec![".test".to_string()],
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// )
-// .with_highlights_query(
-// r#"
-// ":" @operator
-// (string_literal) @string
-// "#,
-// )
-// .unwrap(),
-// );
-// language.set_theme(&theme);
-
-// let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
-
-// let buffer = cx
-// .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
-// buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
-
-// let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-// let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
-
-// let font_cache = cx.font_cache();
-// let family_id = font_cache
-// .load_family(&["Courier"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 16.0;
-// let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-
-// enum MyType {}
-
-// let style = HighlightStyle {
-// color: Some(Hsla::blue()),
-// ..Default::default()
-// };
-
-// map.update(cx, |map, _cx| {
-// map.highlight_text(
-// TypeId::of::<MyType>(),
-// highlighted_ranges
-// .into_iter()
-// .map(|range| {
-// buffer_snapshot.anchor_before(range.start)
-// ..buffer_snapshot.anchor_before(range.end)
-// })
-// .collect(),
-// style,
-// );
-// });
-
-// assert_eq!(
-// cx.update(|cx| chunks(0..10, &map, &theme, cx)),
-// [
-// ("const ".to_string(), None, None),
-// ("a".to_string(), None, Some(Hsla::blue())),
-// (":".to_string(), Some(Hsla::red()), None),
-// (" B = ".to_string(), None, None),
-// ("\"c ".to_string(), Some(Hsla::green()), None),
-// ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
-// ("\"".to_string(), Some(Hsla::green()), None),
-// ]
-// );
-// }
-
-// #[gpui::test]
-// fn test_clip_point(cx: &mut gpui::AppContext) {
-// init_test(cx, |_| {});
-
-// fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
-// let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
-
-// match bias {
-// Bias::Left => {
-// if shift_right {
-// *markers[1].column_mut() += 1;
-// }
-
-// assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
-// }
-// Bias::Right => {
-// if shift_right {
-// *markers[0].column_mut() += 1;
-// }
-
-// assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
-// }
-// };
-// }
-
-// use Bias::{Left, Right};
-// assert("ˇˇα", false, Left, cx);
-// assert("ˇˇα", true, Left, cx);
-// assert("ˇˇα", false, Right, cx);
-// assert("ˇαˇ", true, Right, cx);
-// assert("ˇˇ✋", false, Left, cx);
-// assert("ˇˇ✋", true, Left, cx);
-// assert("ˇˇ✋", false, Right, cx);
-// assert("ˇ✋ˇ", true, Right, cx);
-// assert("ˇˇ🍐", false, Left, cx);
-// assert("ˇˇ🍐", true, Left, cx);
-// assert("ˇˇ🍐", false, Right, cx);
-// assert("ˇ🍐ˇ", true, Right, cx);
-// assert("ˇˇ\t", false, Left, cx);
-// assert("ˇˇ\t", true, Left, cx);
-// assert("ˇˇ\t", false, Right, cx);
-// assert("ˇ\tˇ", true, Right, cx);
-// assert(" ˇˇ\t", false, Left, cx);
-// assert(" ˇˇ\t", true, Left, cx);
-// assert(" ˇˇ\t", false, Right, cx);
-// assert(" ˇ\tˇ", true, Right, cx);
-// assert(" ˇˇ\t", false, Left, cx);
-// assert(" ˇˇ\t", false, Right, cx);
-// }
-
-// #[gpui::test]
-// fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
-// init_test(cx, |_| {});
-
-// fn assert(text: &str, cx: &mut gpui::AppContext) {
-// let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
-// unmarked_snapshot.clip_at_line_ends = true;
-// assert_eq!(
-// unmarked_snapshot.clip_point(markers[1], Bias::Left),
-// markers[0]
-// );
-// }
-
-// assert("ˇˇ", cx);
-// assert("ˇaˇ", cx);
-// assert("aˇbˇ", cx);
-// assert("aˇαˇ", cx);
-// }
-
-// #[gpui::test]
-// fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
-// init_test(cx, |_| {});
-
-// let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
-// let buffer = MultiBuffer::build_simple(text, cx);
-// let font_cache = cx.font_cache();
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 14.0;
-
-// let map =
-// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
-// let map = map.update(cx, |map, cx| map.snapshot(cx));
-// assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
-// assert_eq!(
-// map.text_chunks(0).collect::<String>(),
-// "✅ α\nβ \n🏀β γ"
-// );
-// assert_eq!(map.text_chunks(1).collect::<String>(), "β \n🏀β γ");
-// assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β γ");
-
-// let point = Point::new(0, "✅\t\t".len() as u32);
-// let display_point = DisplayPoint::new(0, "✅ ".len() as u32);
-// assert_eq!(point.to_display_point(&map), display_point);
-// assert_eq!(display_point.to_point(&map), point);
-
-// let point = Point::new(1, "β\t".len() as u32);
-// let display_point = DisplayPoint::new(1, "β ".len() as u32);
-// assert_eq!(point.to_display_point(&map), display_point);
-// assert_eq!(display_point.to_point(&map), point,);
-
-// let point = Point::new(2, "🏀β\t\t".len() as u32);
-// let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
-// assert_eq!(point.to_display_point(&map), display_point);
-// assert_eq!(display_point.to_point(&map), point,);
-
-// // Display points inside of expanded tabs
-// assert_eq!(
-// DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
-// Point::new(0, "✅\t".len() as u32),
-// );
-// assert_eq!(
-// DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
-// Point::new(0, "✅".len() as u32),
-// );
-
-// // Clipping display points inside of multi-byte characters
-// assert_eq!(
-// map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
-// DisplayPoint::new(0, 0)
-// );
-// assert_eq!(
-// map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
-// DisplayPoint::new(0, "✅".len() as u32)
-// );
-// }
-
-// #[gpui::test]
-// fn test_max_point(cx: &mut gpui::AppContext) {
-// init_test(cx, |_| {});
-
-// let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
-// let font_cache = cx.font_cache();
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 14.0;
-// let map =
-// cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
-// assert_eq!(
-// map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
-// DisplayPoint::new(1, 11)
-// )
-// }
-
-// fn syntax_chunks<'a>(
-// rows: Range<u32>,
-// map: &Model<DisplayMap>,
-// theme: &'a SyntaxTheme,
-// cx: &mut AppContext,
-// ) -> Vec<(String, Option<Hsla>)> {
-// chunks(rows, map, theme, cx)
-// .into_iter()
-// .map(|(text, color, _)| (text, color))
-// .collect()
-// }
-
-// fn chunks<'a>(
-// rows: Range<u32>,
-// map: &Model<DisplayMap>,
-// theme: &'a SyntaxTheme,
-// cx: &mut AppContext,
-// ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
-// let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
-// let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
-// for chunk in snapshot.chunks(rows, true, None, None) {
-// let syntax_color = chunk
-// .syntax_highlight_id
-// .and_then(|id| id.style(theme)?.color);
-// let highlight_color = chunk.highlight_style.and_then(|style| style.color);
-// if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
-// if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
-// last_chunk.push_str(chunk.text);
-// continue;
-// }
-// }
-// chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
-// }
-// chunks
-// }
-
-// fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
-// cx.foreground().forbid_parking();
-// cx.set_global(SettingsStore::test(cx));
-// language::init(cx);
-// crate::init(cx);
-// Project::init_settings(cx);
-// theme::init((), cx);
-// cx.update_global::<SettingsStore, _, _>(|store, cx| {
-// store.update_user_settings::<AllLanguageSettings>(cx, f);
-// });
-// }
-// }
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::{
+ movement,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ };
+ use gpui::{div, font, observe, px, AppContext, Context, Element, Hsla};
+ use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+ Buffer, Language, LanguageConfig, SelectionGoal,
+ };
+ use project::Project;
+ use rand::{prelude::*, Rng};
+ use settings::SettingsStore;
+ use smol::stream::StreamExt;
+ use std::{env, sync::Arc};
+ use theme::{LoadThemes, SyntaxTheme};
+ use util::test::{marked_text_ranges, sample_text};
+ use Bias::*;
+
+ #[gpui::test(iterations = 100)]
+ async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ cx.background_executor.set_block_on_ticks(0..=50);
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let test_platform = &cx.test_platform;
+ let mut tab_size = rng.gen_range(1..=4);
+ let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
+ let excerpt_header_height = rng.gen_range(1..=5);
+ let font_size = px(14.0);
+ let max_wrap_width = 300.0;
+ let mut wrap_width = if rng.gen_bool(0.1) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=max_wrap_width)))
+ };
+
+ log::info!("tab size: {}", tab_size);
+ log::info!("wrap width: {:?}", wrap_width);
+
+ cx.update(|cx| {
+ init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
+ });
+
+ let buffer = cx.update(|cx| {
+ if rng.gen() {
+ let len = rng.gen_range(0..10);
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ }
+ });
+
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(
+ buffer.clone(),
+ font("Helvetica"),
+ font_size,
+ wrap_width,
+ buffer_start_excerpt_header_height,
+ excerpt_header_height,
+ cx,
+ )
+ });
+ let mut notifications = observe(&map, cx);
+ let mut fold_count = 0;
+ let mut blocks = Vec::new();
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+ log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+ log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+ log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+ log::info!("block text: {:?}", snapshot.block_snapshot.text());
+ log::info!("display text: {:?}", snapshot.text());
+
+ for _i in 0..operations {
+ match rng.gen_range(0..100) {
+ 0..=19 => {
+ wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=max_wrap_width)))
+ };
+ log::info!("setting wrap width to {:?}", wrap_width);
+ map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+ }
+ 20..=29 => {
+ let mut tab_sizes = vec![1, 2, 3, 4];
+ tab_sizes.remove((tab_size - 1) as usize);
+ tab_size = *tab_sizes.choose(&mut rng).unwrap();
+ log::info!("setting tab size to {:?}", tab_size);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(tab_size);
+ });
+ });
+ });
+ }
+ 30..=44 => {
+ map.update(cx, |map, cx| {
+ if rng.gen() || blocks.is_empty() {
+ let buffer = map.snapshot(cx).buffer_snapshot;
+ let block_properties = (0..rng.gen_range(1..=1))
+ .map(|_| {
+ let position =
+ buffer.anchor_after(buffer.clip_offset(
+ rng.gen_range(0..=buffer.len()),
+ Bias::Left,
+ ));
+
+ let disposition = if rng.gen() {
+ BlockDisposition::Above
+ } else {
+ BlockDisposition::Below
+ };
+ let height = rng.gen_range(1..5);
+ log::info!(
+ "inserting block {:?} {:?} with height {}",
+ disposition,
+ position.to_point(&buffer),
+ height
+ );
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position,
+ height,
+ disposition,
+ render: Arc::new(|_| div().into_any()),
+ }
+ })
+ .collect::<Vec<_>>();
+ blocks.extend(map.insert_blocks(block_properties, cx));
+ } else {
+ blocks.shuffle(&mut rng);
+ let remove_count = rng.gen_range(1..=4.min(blocks.len()));
+ let block_ids_to_remove = (0..remove_count)
+ .map(|_| blocks.remove(rng.gen_range(0..blocks.len())))
+ .collect();
+ log::info!("removing block ids {:?}", block_ids_to_remove);
+ map.remove_blocks(block_ids_to_remove, cx);
+ }
+ });
+ }
+ 45..=79 => {
+ let mut ranges = Vec::new();
+ for _ in 0..rng.gen_range(1..=3) {
+ buffer.read_with(cx, |buffer, cx| {
+ let buffer = buffer.read(cx);
+ let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ ranges.push(start..end);
+ });
+ }
+
+ if rng.gen() && fold_count > 0 {
+ log::info!("unfolding ranges: {:?}", ranges);
+ map.update(cx, |map, cx| {
+ map.unfold(ranges, true, cx);
+ });
+ } else {
+ log::info!("folding ranges: {:?}", ranges);
+ map.update(cx, |map, cx| {
+ map.fold(ranges, cx);
+ });
+ }
+ }
+ _ => {
+ buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
+ }
+ }
+
+ if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
+ notifications.next().await.unwrap();
+ }
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ fold_count = snapshot.fold_count();
+ log::info!("buffer text: {:?}", snapshot.buffer_snapshot.text());
+ log::info!("fold text: {:?}", snapshot.fold_snapshot.text());
+ log::info!("tab text: {:?}", snapshot.tab_snapshot.text());
+ log::info!("wrap text: {:?}", snapshot.wrap_snapshot.text());
+ log::info!("block text: {:?}", snapshot.block_snapshot.text());
+ log::info!("display text: {:?}", snapshot.text());
+
+ // Line boundaries
+ let buffer = &snapshot.buffer_snapshot;
+ for _ in 0..5 {
+ let row = rng.gen_range(0..=buffer.max_point().row);
+ let column = rng.gen_range(0..=buffer.line_len(row));
+ let point = buffer.clip_point(Point::new(row, column), Left);
+
+ let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
+ let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
+
+ assert!(prev_buffer_bound <= point);
+ assert!(next_buffer_bound >= point);
+ assert_eq!(prev_buffer_bound.column, 0);
+ assert_eq!(prev_display_bound.column(), 0);
+ if next_buffer_bound < buffer.max_point() {
+ assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
+ }
+
+ assert_eq!(
+ prev_display_bound,
+ prev_buffer_bound.to_display_point(&snapshot),
+ "row boundary before {:?}. reported buffer row boundary: {:?}",
+ point,
+ prev_buffer_bound
+ );
+ assert_eq!(
+ next_display_bound,
+ next_buffer_bound.to_display_point(&snapshot),
+ "display row boundary after {:?}. reported buffer row boundary: {:?}",
+ point,
+ next_buffer_bound
+ );
+ assert_eq!(
+ prev_buffer_bound,
+ prev_display_bound.to_point(&snapshot),
+ "row boundary before {:?}. reported display row boundary: {:?}",
+ point,
+ prev_display_bound
+ );
+ assert_eq!(
+ next_buffer_bound,
+ next_display_bound.to_point(&snapshot),
+ "row boundary after {:?}. reported display row boundary: {:?}",
+ point,
+ next_display_bound
+ );
+ }
+
+ // Movement
+ let min_point = snapshot.clip_point(DisplayPoint::new(0, 0), Left);
+ let max_point = snapshot.clip_point(snapshot.max_point(), Right);
+ for _ in 0..5 {
+ let row = rng.gen_range(0..=snapshot.max_point().row());
+ let column = rng.gen_range(0..=snapshot.line_len(row));
+ let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
+
+ log::info!("Moving from point {:?}", point);
+
+ let moved_right = movement::right(&snapshot, point);
+ log::info!("Right {:?}", moved_right);
+ if point < max_point {
+ assert!(moved_right > point);
+ if point.column() == snapshot.line_len(point.row())
+ || snapshot.soft_wrap_indent(point.row()).is_some()
+ && point.column() == snapshot.line_len(point.row()) - 1
+ {
+ assert!(moved_right.row() > point.row());
+ }
+ } else {
+ assert_eq!(moved_right, point);
+ }
+
+ let moved_left = movement::left(&snapshot, point);
+ log::info!("Left {:?}", moved_left);
+ if point > min_point {
+ assert!(moved_left < point);
+ if point.column() == 0 {
+ assert!(moved_left.row() < point.row());
+ }
+ } else {
+ assert_eq!(moved_left, point);
+ }
+ }
+ }
+ }
+
+ #[gpui::test(retries = 5)]
+ async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
+ cx.background_executor
+ .set_block_on_ticks(usize::MAX..=usize::MAX);
+ cx.update(|cx| {
+ init_test(cx, |_| {});
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
+
+ cx.update_window(window, |_, cx| {
+ let text_layout_details =
+ editor.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+ let font_size = px(12.0);
+ let wrap_width = Some(px(64.));
+
+ let text = "one two three four five\nsix seven eight";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(
+ buffer.clone(),
+ font("Helvetica"),
+ font_size,
+ wrap_width,
+ 1,
+ 1,
+ cx,
+ )
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(0).collect::<String>(),
+ "one two \nthree four \nfive\nsix seven \neight"
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+ DisplayPoint::new(0, 7)
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::right(&snapshot, DisplayPoint::new(0, 7)),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::left(&snapshot, DisplayPoint::new(1, 0)),
+ DisplayPoint::new(0, 7)
+ );
+
+ let x = snapshot.x_for_display_point(DisplayPoint::new(1, 10), &text_layout_details);
+ assert_eq!(
+ movement::up(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::None,
+ false,
+ &text_layout_details,
+ ),
+ (
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x.0)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x.0)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 4),
+ SelectionGoal::HorizontalPosition(x.0)
+ )
+ );
+
+ let ix = snapshot.buffer_snapshot.text().find("seven").unwrap();
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(ix..ix, "and ")], None, cx);
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(1).collect::<String>(),
+ "three four \nfive\nsix and \nseven eight"
+ );
+
+ // Re-wrap on font size changes
+ map.update(cx, |map, cx| {
+ map.set_font(font("Helvetica"), px(font_size.0 + 3.), cx)
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(1).collect::<String>(),
+ "three \nfour five\nsix and \nseven \neight"
+ )
+ });
+ }
+
+ #[gpui::test]
+ fn test_text_chunks(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let text = sample_text(6, 6, 'a');
+ let buffer = MultiBuffer::build_simple(&text, cx);
+
+ let font_size = px(14.0);
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+ });
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ vec![
+ (Point::new(1, 0)..Point::new(1, 0), "\t"),
+ (Point::new(1, 1)..Point::new(1, 1), "\t"),
+ (Point::new(2, 1)..Point::new(2, 1), "\t"),
+ ],
+ None,
+ cx,
+ )
+ });
+
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx))
+ .text_chunks(1)
+ .collect::<String>()
+ .lines()
+ .next(),
+ Some(" b bbbbb")
+ );
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx))
+ .text_chunks(2)
+ .collect::<String>()
+ .lines()
+ .next(),
+ Some("c ccccc")
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks(cx: &mut gpui::TestAppContext) {
+ use unindent::Unindent as _;
+
+ let text = r#"
+ fn outer() {}
+
+ mod module {
+ fn inner() {}
+ }"#
+ .unindent();
+
+ let theme = SyntaxTheme::new_test(vec![
+ ("mod.body", Hsla::red().into()),
+ ("fn.name", Hsla::blue().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_highlights_query(
+ r#"
+ (mod_item name: (identifier) body: _ @mod.body)
+ (function_item name: (identifier) @fn.name)
+ "#,
+ )
+ .unwrap(),
+ );
+ language.set_theme(&theme);
+
+ cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
+
+ let buffer = cx.build_model(|cx| {
+ Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+ });
+ cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+ let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let font_size = px(14.0);
+
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+ vec![
+ ("fn ".to_string(), None),
+ ("outer".to_string(), Some(Hsla::blue())),
+ ("() {}\n\nmod module ".to_string(), None),
+ ("{\n fn ".to_string(), Some(Hsla::red())),
+ ("inner".to_string(), Some(Hsla::blue())),
+ ("() {}\n}".to_string(), Some(Hsla::red())),
+ ]
+ );
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+ vec![
+ (" fn ".to_string(), Some(Hsla::red())),
+ ("inner".to_string(), Some(Hsla::blue())),
+ ("() {}\n}".to_string(), Some(Hsla::red())),
+ ]
+ );
+
+ map.update(cx, |map, cx| {
+ map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
+ vec![
+ ("fn ".to_string(), None),
+ ("out".to_string(), Some(Hsla::blue())),
+ ("⋯".to_string(), None),
+ (" fn ".to_string(), Some(Hsla::red())),
+ ("inner".to_string(), Some(Hsla::blue())),
+ ("() {}\n}".to_string(), Some(Hsla::red())),
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
+ use unindent::Unindent as _;
+
+ cx.background_executor
+ .set_block_on_ticks(usize::MAX..=usize::MAX);
+
+ let text = r#"
+ fn outer() {}
+
+ mod module {
+ fn inner() {}
+ }"#
+ .unindent();
+
+ let theme = SyntaxTheme::new_test(vec![
+ ("mod.body", Hsla::red().into()),
+ ("fn.name", Hsla::blue().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_highlights_query(
+ r#"
+ (mod_item name: (identifier) body: _ @mod.body)
+ (function_item name: (identifier) @fn.name)
+ "#,
+ )
+ .unwrap(),
+ );
+ language.set_theme(&theme);
+
+ cx.update(|cx| init_test(cx, |_| {}));
+
+ let buffer = cx.build_model(|cx| {
+ Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+ });
+ cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+ let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let font_size = px(16.0);
+
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+ [
+ ("fn \n".to_string(), None),
+ ("oute\nr".to_string(), Some(Hsla::blue())),
+ ("() \n{}\n\n".to_string(), None),
+ ]
+ );
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+ [("{}\n\n".to_string(), None)]
+ );
+
+ map.update(cx, |map, cx| {
+ map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
+ [
+ ("out".to_string(), Some(Hsla::blue())),
+ ("⋯\n".to_string(), None),
+ (" \nfn ".to_string(), Some(Hsla::red())),
+ ("i\n".to_string(), Some(Hsla::blue()))
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| init_test(cx, |_| {}));
+
+ let theme = SyntaxTheme::new_test(vec![
+ ("operator", Hsla::red().into()),
+ ("string", Hsla::green().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_highlights_query(
+ r#"
+ ":" @operator
+ (string_literal) @string
+ "#,
+ )
+ .unwrap(),
+ );
+ language.set_theme(&theme);
+
+ let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
+
+ let buffer = cx.build_model(|cx| {
+ Buffer::new(0, cx.entity_id().as_u64(), text).with_language(language, cx)
+ });
+ cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
+
+ let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+ let font_size = px(16.0);
+ let map = cx
+ .build_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
+
+ enum MyType {}
+
+ let style = HighlightStyle {
+ color: Some(Hsla::blue()),
+ ..Default::default()
+ };
+
+ map.update(cx, |map, _cx| {
+ map.highlight_text(
+ TypeId::of::<MyType>(),
+ highlighted_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)
+ ..buffer_snapshot.anchor_before(range.end)
+ })
+ .collect(),
+ style,
+ );
+ });
+
+ assert_eq!(
+ cx.update(|cx| chunks(0..10, &map, &theme, cx)),
+ [
+ ("const ".to_string(), None, None),
+ ("a".to_string(), None, Some(Hsla::blue())),
+ (":".to_string(), Some(Hsla::red()), None),
+ (" B = ".to_string(), None, None),
+ ("\"c ".to_string(), Some(Hsla::green()), None),
+ ("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
+ ("\"".to_string(), Some(Hsla::green()), None),
+ ]
+ );
+ }
+
+ #[gpui::test]
+ fn test_clip_point(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
+ let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
+
+ match bias {
+ Bias::Left => {
+ if shift_right {
+ *markers[1].column_mut() += 1;
+ }
+
+ assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
+ }
+ Bias::Right => {
+ if shift_right {
+ *markers[0].column_mut() += 1;
+ }
+
+ assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
+ }
+ };
+ }
+
+ use Bias::{Left, Right};
+ assert("ˇˇα", false, Left, cx);
+ assert("ˇˇα", true, Left, cx);
+ assert("ˇˇα", false, Right, cx);
+ assert("ˇαˇ", true, Right, cx);
+ assert("ˇˇ✋", false, Left, cx);
+ assert("ˇˇ✋", true, Left, cx);
+ assert("ˇˇ✋", false, Right, cx);
+ assert("ˇ✋ˇ", true, Right, cx);
+ assert("ˇˇ🍐", false, Left, cx);
+ assert("ˇˇ🍐", true, Left, cx);
+ assert("ˇˇ🍐", false, Right, cx);
+ assert("ˇ🍐ˇ", true, Right, cx);
+ assert("ˇˇ\t", false, Left, cx);
+ assert("ˇˇ\t", true, Left, cx);
+ assert("ˇˇ\t", false, Right, cx);
+ assert("ˇ\tˇ", true, Right, cx);
+ assert(" ˇˇ\t", false, Left, cx);
+ assert(" ˇˇ\t", true, Left, cx);
+ assert(" ˇˇ\t", false, Right, cx);
+ assert(" ˇ\tˇ", true, Right, cx);
+ assert(" ˇˇ\t", false, Left, cx);
+ assert(" ˇˇ\t", false, Right, cx);
+ }
+
+ #[gpui::test]
+ fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ fn assert(text: &str, cx: &mut gpui::AppContext) {
+ let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
+ unmarked_snapshot.clip_at_line_ends = true;
+ assert_eq!(
+ unmarked_snapshot.clip_point(markers[1], Bias::Left),
+ markers[0]
+ );
+ }
+
+ assert("ˇˇ", cx);
+ assert("ˇaˇ", cx);
+ assert("aˇbˇ", cx);
+ assert("aˇαˇ", cx);
+ }
+
+ #[gpui::test]
+ fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let font_size = px(14.0);
+
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+ });
+ let map = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
+ assert_eq!(
+ map.text_chunks(0).collect::<String>(),
+ "✅ α\nβ \n🏀β γ"
+ );
+ assert_eq!(map.text_chunks(1).collect::<String>(), "β \n🏀β γ");
+ assert_eq!(map.text_chunks(2).collect::<String>(), "🏀β γ");
+
+ let point = Point::new(0, "✅\t\t".len() as u32);
+ let display_point = DisplayPoint::new(0, "✅ ".len() as u32);
+ assert_eq!(point.to_display_point(&map), display_point);
+ assert_eq!(display_point.to_point(&map), point);
+
+ let point = Point::new(1, "β\t".len() as u32);
+ let display_point = DisplayPoint::new(1, "β ".len() as u32);
+ assert_eq!(point.to_display_point(&map), display_point);
+ assert_eq!(display_point.to_point(&map), point,);
+
+ let point = Point::new(2, "🏀β\t\t".len() as u32);
+ let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
+ assert_eq!(point.to_display_point(&map), display_point);
+ assert_eq!(display_point.to_point(&map), point,);
+
+ // Display points inside of expanded tabs
+ assert_eq!(
+ DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
+ Point::new(0, "✅\t".len() as u32),
+ );
+ assert_eq!(
+ DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map),
+ Point::new(0, "✅".len() as u32),
+ );
+
+ // Clipping display points inside of multi-byte characters
+ assert_eq!(
+ map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Left),
+ DisplayPoint::new(0, 0)
+ );
+ assert_eq!(
+ map.clip_point(DisplayPoint::new(0, "✅".len() as u32 - 1), Bias::Right),
+ DisplayPoint::new(0, "✅".len() as u32)
+ );
+ }
+
+ #[gpui::test]
+ fn test_max_point(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
+ let font_size = px(14.0);
+ let map = cx.build_model(|cx| {
+ DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+ });
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
+ DisplayPoint::new(1, 11)
+ )
+ }
+
+ fn syntax_chunks<'a>(
+ rows: Range<u32>,
+ map: &Model<DisplayMap>,
+ theme: &'a SyntaxTheme,
+ cx: &mut AppContext,
+ ) -> Vec<(String, Option<Hsla>)> {
+ chunks(rows, map, theme, cx)
+ .into_iter()
+ .map(|(text, color, _)| (text, color))
+ .collect()
+ }
+
+ fn chunks<'a>(
+ rows: Range<u32>,
+ map: &Model<DisplayMap>,
+ theme: &'a SyntaxTheme,
+ cx: &mut AppContext,
+ ) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
+ for chunk in snapshot.chunks(rows, true, None, None) {
+ let syntax_color = chunk
+ .syntax_highlight_id
+ .and_then(|id| id.style(theme)?.color);
+ let highlight_color = chunk.highlight_style.and_then(|style| style.color);
+ if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() {
+ if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color {
+ last_chunk.push_str(chunk.text);
+ continue;
+ }
+ }
+ chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
+ }
+ chunks
+ }
+
+ fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ language::init(cx);
+ crate::init(cx);
+ Project::init_settings(cx);
+ theme::init(LoadThemes::JustBase, cx);
+ cx.update_global::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+ }
+}
@@ -988,680 +988,664 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
(row, offset)
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::display_map::inlay_map::InlayMap;
-// use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
-// use gpui::Element;
-// use multi_buffer::MultiBuffer;
-// use rand::prelude::*;
-// use settings::SettingsStore;
-// use std::env;
-// use util::RandomCharIter;
-
-// #[gpui::test]
-// fn test_offset_for_row() {
-// assert_eq!(offset_for_row("", 0), (0, 0));
-// assert_eq!(offset_for_row("", 1), (0, 0));
-// assert_eq!(offset_for_row("abcd", 0), (0, 0));
-// assert_eq!(offset_for_row("abcd", 1), (0, 4));
-// assert_eq!(offset_for_row("\n", 0), (0, 0));
-// assert_eq!(offset_for_row("\n", 1), (1, 1));
-// assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
-// assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
-// assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
-// assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
-// }
-
-// #[gpui::test]
-// fn test_basic_blocks(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// 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 text = "aaa\nbbb\nccc\nddd";
-
-// let buffer = MultiBuffer::build_simple(text, cx);
-// let buffer_snapshot = buffer.read(cx).snapshot(cx);
-// let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
-// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-// let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
-// let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
-// let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
-
-// let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
-// let block_ids = writer.insert(vec![
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position: buffer_snapshot.anchor_after(Point::new(1, 0)),
-// height: 1,
-// disposition: BlockDisposition::Above,
-// render: Arc::new(|_| Empty::new().into_any_named("block 1")),
-// },
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position: buffer_snapshot.anchor_after(Point::new(1, 2)),
-// height: 2,
-// disposition: BlockDisposition::Above,
-// render: Arc::new(|_| Empty::new().into_any_named("block 2")),
-// },
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position: buffer_snapshot.anchor_after(Point::new(3, 3)),
-// height: 3,
-// disposition: BlockDisposition::Below,
-// render: Arc::new(|_| Empty::new().into_any_named("block 3")),
-// },
-// ]);
-
-// let snapshot = block_map.read(wraps_snapshot, Default::default());
-// assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
-
-// let blocks = snapshot
-// .blocks_in_range(0..8)
-// .map(|(start_row, block)| {
-// let block = block.as_custom().unwrap();
-// (start_row..start_row + block.height as u32, block.id)
-// })
-// .collect::<Vec<_>>();
-
-// // When multiple blocks are on the same line, the newer blocks appear first.
-// assert_eq!(
-// blocks,
-// &[
-// (1..2, block_ids[0]),
-// (2..4, block_ids[1]),
-// (7..10, block_ids[2]),
-// ]
-// );
-
-// assert_eq!(
-// snapshot.to_block_point(WrapPoint::new(0, 3)),
-// BlockPoint::new(0, 3)
-// );
-// assert_eq!(
-// snapshot.to_block_point(WrapPoint::new(1, 0)),
-// BlockPoint::new(4, 0)
-// );
-// assert_eq!(
-// snapshot.to_block_point(WrapPoint::new(3, 3)),
-// BlockPoint::new(6, 3)
-// );
-
-// assert_eq!(
-// snapshot.to_wrap_point(BlockPoint::new(0, 3)),
-// WrapPoint::new(0, 3)
-// );
-// assert_eq!(
-// snapshot.to_wrap_point(BlockPoint::new(1, 0)),
-// WrapPoint::new(1, 0)
-// );
-// assert_eq!(
-// snapshot.to_wrap_point(BlockPoint::new(3, 0)),
-// WrapPoint::new(1, 0)
-// );
-// assert_eq!(
-// snapshot.to_wrap_point(BlockPoint::new(7, 0)),
-// WrapPoint::new(3, 3)
-// );
-
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
-// BlockPoint::new(0, 3)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
-// BlockPoint::new(4, 0)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
-// BlockPoint::new(0, 3)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
-// BlockPoint::new(4, 0)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
-// BlockPoint::new(4, 0)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
-// BlockPoint::new(4, 0)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
-// BlockPoint::new(6, 3)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
-// BlockPoint::new(6, 3)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
-// BlockPoint::new(6, 3)
-// );
-// assert_eq!(
-// snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
-// BlockPoint::new(6, 3)
-// );
-
-// assert_eq!(
-// snapshot.buffer_rows(0).collect::<Vec<_>>(),
-// &[
-// Some(0),
-// None,
-// None,
-// None,
-// Some(1),
-// Some(2),
-// Some(3),
-// None,
-// None,
-// None
-// ]
-// );
-
-// // Insert a line break, separating two block decorations into separate lines.
-// let buffer_snapshot = buffer.update(cx, |buffer, cx| {
-// buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
-// buffer.snapshot(cx)
-// });
-
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// let (tab_snapshot, tab_edits) =
-// tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
-// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-// wrap_map.sync(tab_snapshot, tab_edits, cx)
-// });
-// let snapshot = block_map.read(wraps_snapshot, wrap_edits);
-// assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
-// }
-
-// #[gpui::test]
-// fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// 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 text = "one two three\nfour five six\nseven eight";
-
-// let buffer = MultiBuffer::build_simple(text, cx);
-// let buffer_snapshot = buffer.read(cx).snapshot(cx);
-// let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-// let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
-// let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
-// let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
-// let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
-
-// let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
-// writer.insert(vec![
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position: buffer_snapshot.anchor_after(Point::new(1, 12)),
-// disposition: BlockDisposition::Above,
-// render: Arc::new(|_| Empty::new().into_any_named("block 1")),
-// height: 1,
-// },
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position: buffer_snapshot.anchor_after(Point::new(1, 1)),
-// disposition: BlockDisposition::Below,
-// render: Arc::new(|_| Empty::new().into_any_named("block 2")),
-// height: 1,
-// },
-// ]);
-
-// // Blocks with an 'above' disposition go above their corresponding buffer line.
-// // Blocks with a 'below' disposition go below their corresponding buffer line.
-// let snapshot = block_map.read(wraps_snapshot, Default::default());
-// assert_eq!(
-// snapshot.text(),
-// "one two \nthree\n\nfour five \nsix\n\nseven \neight"
-// );
-// }
-
-// #[gpui::test(iterations = 100)]
-// fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
-// init_test(cx);
-
-// let operations = env::var("OPERATIONS")
-// .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-// .unwrap_or(10);
-
-// let wrap_width = if rng.gen_bool(0.2) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=100.0))
-// };
-// let tab_size = 1.try_into().unwrap();
-// 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_start_header_height = rng.gen_range(1..=5);
-// let excerpt_header_height = rng.gen_range(1..=5);
-
-// log::info!("Wrap width: {:?}", wrap_width);
-// log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
-
-// let buffer = if rng.gen() {
-// let len = rng.gen_range(0..10);
-// let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
-// log::info!("initial buffer text: {:?}", text);
-// MultiBuffer::build_simple(&text, cx)
-// } else {
-// MultiBuffer::build_random(&mut rng, cx)
-// };
-
-// let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
-// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
-// let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
-// let (wrap_map, wraps_snapshot) =
-// WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
-// let mut block_map = BlockMap::new(
-// wraps_snapshot,
-// buffer_start_header_height,
-// excerpt_header_height,
-// );
-// let mut custom_blocks = Vec::new();
-
-// for _ in 0..operations {
-// let mut buffer_edits = Vec::new();
-// match rng.gen_range(0..=100) {
-// 0..=19 => {
-// let wrap_width = if rng.gen_bool(0.2) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=100.0))
-// };
-// log::info!("Setting wrap width to {:?}", wrap_width);
-// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-// }
-// 20..=39 => {
-// let block_count = rng.gen_range(1..=5);
-// let block_properties = (0..block_count)
-// .map(|_| {
-// let buffer = buffer.read(cx).read(cx);
-// let position = buffer.anchor_after(
-// buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
-// );
-
-// let disposition = if rng.gen() {
-// BlockDisposition::Above
-// } else {
-// BlockDisposition::Below
-// };
-// let height = rng.gen_range(1..5);
-// log::info!(
-// "inserting block {:?} {:?} with height {}",
-// disposition,
-// position.to_point(&buffer),
-// height
-// );
-// BlockProperties {
-// style: BlockStyle::Fixed,
-// position,
-// height,
-// disposition,
-// render: Arc::new(|_| Empty::new().into_any()),
-// }
-// })
-// .collect::<Vec<_>>();
-
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.sync(buffer_snapshot.clone(), vec![]);
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// let (tab_snapshot, tab_edits) =
-// tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-// wrap_map.sync(tab_snapshot, tab_edits, cx)
-// });
-// let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
-// let block_ids = block_map.insert(block_properties.clone());
-// for (block_id, props) in block_ids.into_iter().zip(block_properties) {
-// custom_blocks.push((block_id, props));
-// }
-// }
-// 40..=59 if !custom_blocks.is_empty() => {
-// let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
-// let block_ids_to_remove = (0..block_count)
-// .map(|_| {
-// custom_blocks
-// .remove(rng.gen_range(0..custom_blocks.len()))
-// .0
-// })
-// .collect();
-
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.sync(buffer_snapshot.clone(), vec![]);
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// let (tab_snapshot, tab_edits) =
-// tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-// wrap_map.sync(tab_snapshot, tab_edits, cx)
-// });
-// let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
-// block_map.remove(block_ids_to_remove);
-// }
-// _ => {
-// buffer.update(cx, |buffer, cx| {
-// let mutation_count = rng.gen_range(1..=5);
-// let subscription = buffer.subscribe();
-// buffer.randomly_mutate(&mut rng, mutation_count, cx);
-// buffer_snapshot = buffer.snapshot(cx);
-// buffer_edits.extend(subscription.consume());
-// log::info!("buffer text: {:?}", buffer_snapshot.text());
-// });
-// }
-// }
-
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
-// wrap_map.sync(tab_snapshot, tab_edits, cx)
-// });
-// let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
-// assert_eq!(
-// blocks_snapshot.transforms.summary().input_rows,
-// wraps_snapshot.max_point().row() + 1
-// );
-// log::info!("blocks text: {:?}", blocks_snapshot.text());
-
-// let mut expected_blocks = Vec::new();
-// expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
-// let mut position = block.position.to_point(&buffer_snapshot);
-// match block.disposition {
-// BlockDisposition::Above => {
-// position.column = 0;
-// }
-// BlockDisposition::Below => {
-// position.column = buffer_snapshot.line_len(position.row);
-// }
-// };
-// let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
-// (
-// row,
-// ExpectedBlock::Custom {
-// disposition: block.disposition,
-// id: *id,
-// height: block.height,
-// },
-// )
-// }));
-// expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
-// |boundary| {
-// let position =
-// wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
-// (
-// position.row(),
-// ExpectedBlock::ExcerptHeader {
-// height: if boundary.starts_new_buffer {
-// buffer_start_header_height
-// } else {
-// excerpt_header_height
-// },
-// starts_new_buffer: boundary.starts_new_buffer,
-// },
-// )
-// },
-// ));
-// expected_blocks.sort_unstable();
-// let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
-
-// let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
-// let mut expected_buffer_rows = Vec::new();
-// let mut expected_text = String::new();
-// let mut expected_block_positions = Vec::new();
-// let input_text = wraps_snapshot.text();
-// for (row, input_line) in input_text.split('\n').enumerate() {
-// let row = row as u32;
-// if row > 0 {
-// expected_text.push('\n');
-// }
-
-// let buffer_row = input_buffer_rows[wraps_snapshot
-// .to_point(WrapPoint::new(row, 0), Bias::Left)
-// .row as usize];
-
-// while let Some((block_row, block)) = sorted_blocks_iter.peek() {
-// if *block_row == row && block.disposition() == BlockDisposition::Above {
-// let (_, block) = sorted_blocks_iter.next().unwrap();
-// let height = block.height() as usize;
-// expected_block_positions
-// .push((expected_text.matches('\n').count() as u32, block));
-// let text = "\n".repeat(height);
-// expected_text.push_str(&text);
-// for _ in 0..height {
-// expected_buffer_rows.push(None);
-// }
-// } else {
-// break;
-// }
-// }
-
-// let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
-// expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
-// expected_text.push_str(input_line);
-
-// while let Some((block_row, block)) = sorted_blocks_iter.peek() {
-// if *block_row == row && block.disposition() == BlockDisposition::Below {
-// let (_, block) = sorted_blocks_iter.next().unwrap();
-// let height = block.height() as usize;
-// expected_block_positions
-// .push((expected_text.matches('\n').count() as u32 + 1, block));
-// let text = "\n".repeat(height);
-// expected_text.push_str(&text);
-// for _ in 0..height {
-// expected_buffer_rows.push(None);
-// }
-// } else {
-// break;
-// }
-// }
-// }
-
-// let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
-// let expected_row_count = expected_lines.len();
-// for start_row in 0..expected_row_count {
-// let expected_text = expected_lines[start_row..].join("\n");
-// let actual_text = blocks_snapshot
-// .chunks(
-// start_row as u32..blocks_snapshot.max_point().row + 1,
-// false,
-// Highlights::default(),
-// )
-// .map(|chunk| chunk.text)
-// .collect::<String>();
-// assert_eq!(
-// actual_text, expected_text,
-// "incorrect text starting from row {}",
-// start_row
-// );
-// assert_eq!(
-// blocks_snapshot
-// .buffer_rows(start_row as u32)
-// .collect::<Vec<_>>(),
-// &expected_buffer_rows[start_row..]
-// );
-// }
-
-// assert_eq!(
-// blocks_snapshot
-// .blocks_in_range(0..(expected_row_count as u32))
-// .map(|(row, block)| (row, block.clone().into()))
-// .collect::<Vec<_>>(),
-// expected_block_positions
-// );
-
-// let mut expected_longest_rows = Vec::new();
-// let mut longest_line_len = -1_isize;
-// for (row, line) in expected_lines.iter().enumerate() {
-// let row = row as u32;
-
-// assert_eq!(
-// blocks_snapshot.line_len(row),
-// line.len() as u32,
-// "invalid line len for row {}",
-// row
-// );
-
-// let line_char_count = line.chars().count() as isize;
-// match line_char_count.cmp(&longest_line_len) {
-// Ordering::Less => {}
-// Ordering::Equal => expected_longest_rows.push(row),
-// Ordering::Greater => {
-// longest_line_len = line_char_count;
-// expected_longest_rows.clear();
-// expected_longest_rows.push(row);
-// }
-// }
-// }
-
-// let longest_row = blocks_snapshot.longest_row();
-// assert!(
-// expected_longest_rows.contains(&longest_row),
-// "incorrect longest row {}. expected {:?} with length {}",
-// longest_row,
-// expected_longest_rows,
-// longest_line_len,
-// );
-
-// for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
-// let wrap_point = WrapPoint::new(row, 0);
-// let block_point = blocks_snapshot.to_block_point(wrap_point);
-// assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
-// }
-
-// let mut block_point = BlockPoint::new(0, 0);
-// for c in expected_text.chars() {
-// let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
-// let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
-// assert_eq!(
-// blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
-// left_point
-// );
-// assert_eq!(
-// left_buffer_point,
-// buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
-// "{:?} is not valid in buffer coordinates",
-// left_point
-// );
-
-// let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
-// let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
-// assert_eq!(
-// blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
-// right_point
-// );
-// assert_eq!(
-// right_buffer_point,
-// buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
-// "{:?} is not valid in buffer coordinates",
-// right_point
-// );
-
-// if c == '\n' {
-// block_point.0 += Point::new(1, 0);
-// } else {
-// block_point.column += c.len_utf8() as u32;
-// }
-// }
-// }
-
-// #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
-// enum ExpectedBlock {
-// ExcerptHeader {
-// height: u8,
-// starts_new_buffer: bool,
-// },
-// Custom {
-// disposition: BlockDisposition,
-// id: BlockId,
-// height: u8,
-// },
-// }
-
-// impl ExpectedBlock {
-// fn height(&self) -> u8 {
-// match self {
-// ExpectedBlock::ExcerptHeader { height, .. } => *height,
-// ExpectedBlock::Custom { height, .. } => *height,
-// }
-// }
-
-// fn disposition(&self) -> BlockDisposition {
-// match self {
-// ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
-// ExpectedBlock::Custom { disposition, .. } => *disposition,
-// }
-// }
-// }
-
-// impl From<TransformBlock> for ExpectedBlock {
-// fn from(block: TransformBlock) -> Self {
-// match block {
-// TransformBlock::Custom(block) => ExpectedBlock::Custom {
-// id: block.id,
-// disposition: block.disposition,
-// height: block.height,
-// },
-// TransformBlock::ExcerptHeader {
-// height,
-// starts_new_buffer,
-// ..
-// } => ExpectedBlock::ExcerptHeader {
-// height,
-// starts_new_buffer,
-// },
-// }
-// }
-// }
-// }
-
-// fn init_test(cx: &mut gpui::AppContext) {
-// cx.set_global(SettingsStore::test(cx));
-// theme::init(cx);
-// }
-
-// impl TransformBlock {
-// fn as_custom(&self) -> Option<&Block> {
-// match self {
-// TransformBlock::Custom(block) => Some(block),
-// TransformBlock::ExcerptHeader { .. } => None,
-// }
-// }
-// }
-
-// impl BlockSnapshot {
-// fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
-// self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
-// }
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::display_map::inlay_map::InlayMap;
+ use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+ use gpui::{div, font, px, Element, Platform as _};
+ use multi_buffer::MultiBuffer;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::env;
+ use util::RandomCharIter;
+
+ #[gpui::test]
+ fn test_offset_for_row() {
+ assert_eq!(offset_for_row("", 0), (0, 0));
+ assert_eq!(offset_for_row("", 1), (0, 0));
+ assert_eq!(offset_for_row("abcd", 0), (0, 0));
+ assert_eq!(offset_for_row("abcd", 1), (0, 4));
+ assert_eq!(offset_for_row("\n", 0), (0, 0));
+ assert_eq!(offset_for_row("\n", 1), (1, 1));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
+ }
+
+ #[gpui::test]
+ fn test_basic_blocks(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| init_test(cx));
+
+ let text = "aaa\nbbb\nccc\nddd";
+
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+ let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+ let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+ let (wrap_map, wraps_snapshot) =
+ cx.update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), None, cx));
+ let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+ let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+ let block_ids = writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 0)),
+ height: 1,
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| div().into_any()),
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 2)),
+ height: 2,
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| div().into_any()),
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(3, 3)),
+ height: 3,
+ disposition: BlockDisposition::Below,
+ render: Arc::new(|_| div().into_any()),
+ },
+ ]);
+
+ let snapshot = block_map.read(wraps_snapshot, Default::default());
+ assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
+
+ let blocks = snapshot
+ .blocks_in_range(0..8)
+ .map(|(start_row, block)| {
+ let block = block.as_custom().unwrap();
+ (start_row..start_row + block.height as u32, block.id)
+ })
+ .collect::<Vec<_>>();
+
+ // When multiple blocks are on the same line, the newer blocks appear first.
+ assert_eq!(
+ blocks,
+ &[
+ (1..2, block_ids[0]),
+ (2..4, block_ids[1]),
+ (7..10, block_ids[2]),
+ ]
+ );
+
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(0, 3)),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(1, 0)),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(3, 3)),
+ BlockPoint::new(6, 3)
+ );
+
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+ WrapPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+ WrapPoint::new(1, 0)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+ WrapPoint::new(1, 0)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(7, 0)),
+ WrapPoint::new(3, 3)
+ );
+
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
+ BlockPoint::new(6, 3)
+ );
+
+ assert_eq!(
+ snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ &[
+ Some(0),
+ None,
+ None,
+ None,
+ Some(1),
+ Some(2),
+ Some(3),
+ None,
+ None,
+ None
+ ]
+ );
+
+ // Insert a line break, separating two block decorations into separate lines.
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
+ buffer.snapshot(cx)
+ });
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let snapshot = block_map.read(wraps_snapshot, wrap_edits);
+ assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
+ }
+
+ #[gpui::test]
+ fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| init_test(cx));
+
+ let font_id = cx
+ .test_platform
+ .text_system()
+ .font_id(&font("Helvetica"))
+ .unwrap();
+
+ let text = "one two three\nfour five six\nseven eight";
+
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx));
+ let buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+ let (_, wraps_snapshot) = cx.update(|cx| {
+ WrapMap::new(tab_snapshot, font("Helvetica"), px(14.0), Some(px(60.)), cx)
+ });
+ let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+ let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+ writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 12)),
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| div().into_any()),
+ height: 1,
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 1)),
+ disposition: BlockDisposition::Below,
+ render: Arc::new(|_| div().into_any()),
+ height: 1,
+ },
+ ]);
+
+ // Blocks with an 'above' disposition go above their corresponding buffer line.
+ // Blocks with a 'below' disposition go below their corresponding buffer line.
+ let snapshot = block_map.read(wraps_snapshot, Default::default());
+ assert_eq!(
+ snapshot.text(),
+ "one two \nthree\n\nfour five \nsix\n\nseven \neight"
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_blocks(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ cx.update(|cx| init_test(cx));
+
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=100.0)))
+ };
+ let tab_size = 1.try_into().unwrap();
+ let font_size = px(14.0);
+ let buffer_start_header_height = rng.gen_range(1..=5);
+ let excerpt_header_height = rng.gen_range(1..=5);
+
+ log::info!("Wrap width: {:?}", wrap_width);
+ log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
+
+ let buffer = if rng.gen() {
+ let len = rng.gen_range(0..10);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ log::info!("initial buffer text: {:?}", text);
+ cx.update(|cx| MultiBuffer::build_simple(&text, cx))
+ } else {
+ cx.update(|cx| MultiBuffer::build_random(&mut rng, cx))
+ };
+
+ let mut buffer_snapshot = cx.update(|cx| buffer.read(cx).snapshot(cx));
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+ let (wrap_map, wraps_snapshot) = cx
+ .update(|cx| WrapMap::new(tab_snapshot, font("Helvetica"), font_size, wrap_width, cx));
+ let mut block_map = BlockMap::new(
+ wraps_snapshot,
+ buffer_start_header_height,
+ excerpt_header_height,
+ );
+ let mut custom_blocks = Vec::new();
+
+ for _ in 0..operations {
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=19 => {
+ let wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=100.0)))
+ };
+ log::info!("Setting wrap width to {:?}", wrap_width);
+ wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+ }
+ 20..=39 => {
+ let block_count = rng.gen_range(1..=5);
+ let block_properties = (0..block_count)
+ .map(|_| {
+ let buffer = cx.update(|cx| buffer.read(cx).read(cx).clone());
+ let position = buffer.anchor_after(
+ buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
+ );
+
+ let disposition = if rng.gen() {
+ BlockDisposition::Above
+ } else {
+ BlockDisposition::Below
+ };
+ let height = rng.gen_range(1..5);
+ log::info!(
+ "inserting block {:?} {:?} with height {}",
+ disposition,
+ position.to_point(&buffer),
+ height
+ );
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position,
+ height,
+ disposition,
+ render: Arc::new(|_| div().into_any()),
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+ let block_ids = block_map.insert(block_properties.clone());
+ for (block_id, props) in block_ids.into_iter().zip(block_properties) {
+ custom_blocks.push((block_id, props));
+ }
+ }
+ 40..=59 if !custom_blocks.is_empty() => {
+ let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
+ let block_ids_to_remove = (0..block_count)
+ .map(|_| {
+ custom_blocks
+ .remove(rng.gen_range(0..custom_blocks.len()))
+ .0
+ })
+ .collect();
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+ block_map.remove(block_ids_to_remove);
+ }
+ _ => {
+ buffer.update(cx, |buffer, cx| {
+ let mutation_count = rng.gen_range(1..=5);
+ let subscription = buffer.subscribe();
+ buffer.randomly_mutate(&mut rng, mutation_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ buffer_edits.extend(subscription.consume());
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ });
+ }
+ }
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+ assert_eq!(
+ blocks_snapshot.transforms.summary().input_rows,
+ wraps_snapshot.max_point().row() + 1
+ );
+ log::info!("blocks text: {:?}", blocks_snapshot.text());
+
+ let mut expected_blocks = Vec::new();
+ expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
+ let mut position = block.position.to_point(&buffer_snapshot);
+ match block.disposition {
+ BlockDisposition::Above => {
+ position.column = 0;
+ }
+ BlockDisposition::Below => {
+ position.column = buffer_snapshot.line_len(position.row);
+ }
+ };
+ let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
+ (
+ row,
+ ExpectedBlock::Custom {
+ disposition: block.disposition,
+ id: *id,
+ height: block.height,
+ },
+ )
+ }));
+ expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
+ |boundary| {
+ let position =
+ wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
+ (
+ position.row(),
+ ExpectedBlock::ExcerptHeader {
+ height: if boundary.starts_new_buffer {
+ buffer_start_header_height
+ } else {
+ excerpt_header_height
+ },
+ starts_new_buffer: boundary.starts_new_buffer,
+ },
+ )
+ },
+ ));
+ expected_blocks.sort_unstable();
+ let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
+
+ let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
+ let mut expected_buffer_rows = Vec::new();
+ let mut expected_text = String::new();
+ let mut expected_block_positions = Vec::new();
+ let input_text = wraps_snapshot.text();
+ for (row, input_line) in input_text.split('\n').enumerate() {
+ let row = row as u32;
+ if row > 0 {
+ expected_text.push('\n');
+ }
+
+ let buffer_row = input_buffer_rows[wraps_snapshot
+ .to_point(WrapPoint::new(row, 0), Bias::Left)
+ .row as usize];
+
+ while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+ if *block_row == row && block.disposition() == BlockDisposition::Above {
+ let (_, block) = sorted_blocks_iter.next().unwrap();
+ let height = block.height() as usize;
+ expected_block_positions
+ .push((expected_text.matches('\n').count() as u32, block));
+ let text = "\n".repeat(height);
+ expected_text.push_str(&text);
+ for _ in 0..height {
+ expected_buffer_rows.push(None);
+ }
+ } else {
+ break;
+ }
+ }
+
+ let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+ expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
+ expected_text.push_str(input_line);
+
+ while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+ if *block_row == row && block.disposition() == BlockDisposition::Below {
+ let (_, block) = sorted_blocks_iter.next().unwrap();
+ let height = block.height() as usize;
+ expected_block_positions
+ .push((expected_text.matches('\n').count() as u32 + 1, block));
+ let text = "\n".repeat(height);
+ expected_text.push_str(&text);
+ for _ in 0..height {
+ expected_buffer_rows.push(None);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
+ let expected_row_count = expected_lines.len();
+ for start_row in 0..expected_row_count {
+ let expected_text = expected_lines[start_row..].join("\n");
+ let actual_text = blocks_snapshot
+ .chunks(
+ start_row as u32..blocks_snapshot.max_point().row + 1,
+ false,
+ Highlights::default(),
+ )
+ .map(|chunk| chunk.text)
+ .collect::<String>();
+ assert_eq!(
+ actual_text, expected_text,
+ "incorrect text starting from row {}",
+ start_row
+ );
+ assert_eq!(
+ blocks_snapshot
+ .buffer_rows(start_row as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[start_row..]
+ );
+ }
+
+ assert_eq!(
+ blocks_snapshot
+ .blocks_in_range(0..(expected_row_count as u32))
+ .map(|(row, block)| (row, block.clone().into()))
+ .collect::<Vec<_>>(),
+ expected_block_positions
+ );
+
+ let mut expected_longest_rows = Vec::new();
+ let mut longest_line_len = -1_isize;
+ for (row, line) in expected_lines.iter().enumerate() {
+ let row = row as u32;
+
+ assert_eq!(
+ blocks_snapshot.line_len(row),
+ line.len() as u32,
+ "invalid line len for row {}",
+ row
+ );
+
+ let line_char_count = line.chars().count() as isize;
+ match line_char_count.cmp(&longest_line_len) {
+ Ordering::Less => {}
+ Ordering::Equal => expected_longest_rows.push(row),
+ Ordering::Greater => {
+ longest_line_len = line_char_count;
+ expected_longest_rows.clear();
+ expected_longest_rows.push(row);
+ }
+ }
+ }
+
+ let longest_row = blocks_snapshot.longest_row();
+ assert!(
+ expected_longest_rows.contains(&longest_row),
+ "incorrect longest row {}. expected {:?} with length {}",
+ longest_row,
+ expected_longest_rows,
+ longest_line_len,
+ );
+
+ for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
+ let wrap_point = WrapPoint::new(row, 0);
+ let block_point = blocks_snapshot.to_block_point(wrap_point);
+ assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
+ }
+
+ let mut block_point = BlockPoint::new(0, 0);
+ for c in expected_text.chars() {
+ let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
+ let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
+ assert_eq!(
+ blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+ left_point
+ );
+ assert_eq!(
+ left_buffer_point,
+ buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
+ "{:?} is not valid in buffer coordinates",
+ left_point
+ );
+
+ let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
+ let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
+ assert_eq!(
+ blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+ right_point
+ );
+ assert_eq!(
+ right_buffer_point,
+ buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
+ "{:?} is not valid in buffer coordinates",
+ right_point
+ );
+
+ if c == '\n' {
+ block_point.0 += Point::new(1, 0);
+ } else {
+ block_point.column += c.len_utf8() as u32;
+ }
+ }
+ }
+
+ #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
+ enum ExpectedBlock {
+ ExcerptHeader {
+ height: u8,
+ starts_new_buffer: bool,
+ },
+ Custom {
+ disposition: BlockDisposition,
+ id: BlockId,
+ height: u8,
+ },
+ }
+
+ impl ExpectedBlock {
+ fn height(&self) -> u8 {
+ match self {
+ ExpectedBlock::ExcerptHeader { height, .. } => *height,
+ ExpectedBlock::Custom { height, .. } => *height,
+ }
+ }
+
+ fn disposition(&self) -> BlockDisposition {
+ match self {
+ ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+ ExpectedBlock::Custom { disposition, .. } => *disposition,
+ }
+ }
+ }
+
+ impl From<TransformBlock> for ExpectedBlock {
+ fn from(block: TransformBlock) -> Self {
+ match block {
+ TransformBlock::Custom(block) => ExpectedBlock::Custom {
+ id: block.id,
+ disposition: block.disposition,
+ height: block.height,
+ },
+ TransformBlock::ExcerptHeader {
+ height,
+ starts_new_buffer,
+ ..
+ } => ExpectedBlock::ExcerptHeader {
+ height,
+ starts_new_buffer,
+ },
+ }
+ }
+ }
+ }
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ }
+
+ impl TransformBlock {
+ fn as_custom(&self) -> Option<&Block> {
+ match self {
+ TransformBlock::Custom(block) => Some(block),
+ TransformBlock::ExcerptHeader { .. } => None,
+ }
+ }
+ }
+
+ impl BlockSnapshot {
+ fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
+ self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
+ }
+ }
+}
@@ -162,7 +162,7 @@ impl WrapMap {
{
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
- let edits = new_snapshot
+ edits = new_snapshot
.update(
tab_snapshot,
&[TabEdit {
@@ -741,49 +741,48 @@ impl WrapSnapshot {
}
fn check_invariants(&self) {
- // todo!()
- // #[cfg(test)]
- // {
- // assert_eq!(
- // TabPoint::from(self.transforms.summary().input.lines),
- // self.tab_snapshot.max_point()
- // );
-
- // {
- // let mut transforms = self.transforms.cursor::<()>().peekable();
- // while let Some(transform) = transforms.next() {
- // if let Some(next_transform) = transforms.peek() {
- // assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
- // }
- // }
- // }
-
- // let text = language::Rope::from(self.text().as_str());
- // let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
- // let mut expected_buffer_rows = Vec::new();
- // let mut prev_tab_row = 0;
- // for display_row in 0..=self.max_point().row() {
- // let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
- // if tab_point.row() == prev_tab_row && display_row != 0 {
- // expected_buffer_rows.push(None);
- // } else {
- // expected_buffer_rows.push(input_buffer_rows.next().unwrap());
- // }
-
- // prev_tab_row = tab_point.row();
- // assert_eq!(self.line_len(display_row), text.line_len(display_row));
- // }
-
- // for start_display_row in 0..expected_buffer_rows.len() {
- // assert_eq!(
- // self.buffer_rows(start_display_row as u32)
- // .collect::<Vec<_>>(),
- // &expected_buffer_rows[start_display_row..],
- // "invalid buffer_rows({}..)",
- // start_display_row
- // );
- // }
- // }
+ #[cfg(test)]
+ {
+ assert_eq!(
+ TabPoint::from(self.transforms.summary().input.lines),
+ self.tab_snapshot.max_point()
+ );
+
+ {
+ let mut transforms = self.transforms.cursor::<()>().peekable();
+ while let Some(transform) = transforms.next() {
+ if let Some(next_transform) = transforms.peek() {
+ assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
+ }
+ }
+ }
+
+ let text = language::Rope::from(self.text().as_str());
+ let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+ let mut expected_buffer_rows = Vec::new();
+ let mut prev_tab_row = 0;
+ for display_row in 0..=self.max_point().row() {
+ let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
+ if tab_point.row() == prev_tab_row && display_row != 0 {
+ expected_buffer_rows.push(None);
+ } else {
+ expected_buffer_rows.push(input_buffer_rows.next().unwrap());
+ }
+
+ prev_tab_row = tab_point.row();
+ assert_eq!(self.line_len(display_row), text.line_len(display_row));
+ }
+
+ for start_display_row in 0..expected_buffer_rows.len() {
+ assert_eq!(
+ self.buffer_rows(start_display_row as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[start_display_row..],
+ "invalid buffer_rows({}..)",
+ start_display_row
+ );
+ }
+ }
}
}
@@ -1026,337 +1025,334 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
}
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::{
-// display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
-// MultiBuffer,
-// };
-// use gpui::test::observe;
-// use rand::prelude::*;
-// use settings::SettingsStore;
-// use smol::stream::StreamExt;
-// use std::{cmp, env, num::NonZeroU32};
-// use text::Rope;
-
-// #[gpui::test(iterations = 100)]
-// async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
-// init_test(cx);
-
-// cx.foreground().set_block_on_ticks(0..=50);
-// let operations = env::var("OPERATIONS")
-// .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
-// .unwrap_or(10);
-
-// let font_cache = cx.font_cache().clone();
-// let font_system = cx.platform().fonts();
-// let mut wrap_width = if rng.gen_bool(0.1) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=1000.0))
-// };
-// let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
-// let family_id = font_cache
-// .load_family(&["Helvetica"], &Default::default())
-// .unwrap();
-// let font_id = font_cache
-// .select_font(family_id, &Default::default())
-// .unwrap();
-// let font_size = 14.0;
-
-// log::info!("Tab size: {}", tab_size);
-// log::info!("Wrap width: {:?}", wrap_width);
-
-// let buffer = cx.update(|cx| {
-// if rng.gen() {
-// MultiBuffer::build_random(&mut rng, cx)
-// } else {
-// let len = rng.gen_range(0..10);
-// let text = util::RandomCharIter::new(&mut rng)
-// .take(len)
-// .collect::<String>();
-// MultiBuffer::build_simple(&text, cx)
-// }
-// });
-// let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
-// log::info!("Buffer text: {:?}", buffer_snapshot.text());
-// let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
-// log::info!("InlayMap text: {:?}", inlay_snapshot.text());
-// let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
-// log::info!("FoldMap text: {:?}", fold_snapshot.text());
-// let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
-// let tabs_snapshot = tab_map.set_max_expansion_column(32);
-// log::info!("TabMap text: {:?}", tabs_snapshot.text());
-
-// let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
-// let unwrapped_text = tabs_snapshot.text();
-// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
-
-// let (wrap_map, _) =
-// cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
-// let mut notifications = observe(&wrap_map, cx);
-
-// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-// notifications.next().await.unwrap();
-// }
-
-// let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
-// assert!(!map.is_rewrapping());
-// map.sync(tabs_snapshot.clone(), Vec::new(), cx)
-// });
-
-// let actual_text = initial_snapshot.text();
-// assert_eq!(
-// actual_text, expected_text,
-// "unwrapped text is: {:?}",
-// unwrapped_text
-// );
-// log::info!("Wrapped text: {:?}", actual_text);
-
-// let mut next_inlay_id = 0;
-// let mut edits = Vec::new();
-// for _i in 0..operations {
-// log::info!("{} ==============================================", _i);
-
-// let mut buffer_edits = Vec::new();
-// match rng.gen_range(0..=100) {
-// 0..=19 => {
-// wrap_width = if rng.gen_bool(0.2) {
-// None
-// } else {
-// Some(rng.gen_range(0.0..=1000.0))
-// };
-// log::info!("Setting wrap width to {:?}", wrap_width);
-// wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
-// }
-// 20..=39 => {
-// for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
-// let (tabs_snapshot, tab_edits) =
-// tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// let (mut snapshot, wrap_edits) =
-// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
-// snapshot.check_invariants();
-// snapshot.verify_chunks(&mut rng);
-// edits.push((snapshot, wrap_edits));
-// }
-// }
-// 40..=59 => {
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// let (tabs_snapshot, tab_edits) =
-// tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// let (mut snapshot, wrap_edits) =
-// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
-// snapshot.check_invariants();
-// snapshot.verify_chunks(&mut rng);
-// edits.push((snapshot, wrap_edits));
-// }
-// _ => {
-// buffer.update(cx, |buffer, cx| {
-// let subscription = buffer.subscribe();
-// let edit_count = rng.gen_range(1..=5);
-// buffer.randomly_mutate(&mut rng, edit_count, cx);
-// buffer_snapshot = buffer.snapshot(cx);
-// buffer_edits.extend(subscription.consume());
-// });
-// }
-// }
-
-// log::info!("Buffer text: {:?}", buffer_snapshot.text());
-// let (inlay_snapshot, inlay_edits) =
-// inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
-// log::info!("InlayMap text: {:?}", inlay_snapshot.text());
-// let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
-// log::info!("FoldMap text: {:?}", fold_snapshot.text());
-// let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
-// log::info!("TabMap text: {:?}", tabs_snapshot.text());
-
-// let unwrapped_text = tabs_snapshot.text();
-// let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
-// let (mut snapshot, wrap_edits) =
-// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
-// snapshot.check_invariants();
-// snapshot.verify_chunks(&mut rng);
-// edits.push((snapshot, wrap_edits));
-
-// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
-// log::info!("Waiting for wrapping to finish");
-// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-// notifications.next().await.unwrap();
-// }
-// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
-// }
-
-// if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-// let (mut wrapped_snapshot, wrap_edits) =
-// wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
-// let actual_text = wrapped_snapshot.text();
-// let actual_longest_row = wrapped_snapshot.longest_row();
-// log::info!("Wrapping finished: {:?}", actual_text);
-// wrapped_snapshot.check_invariants();
-// wrapped_snapshot.verify_chunks(&mut rng);
-// edits.push((wrapped_snapshot.clone(), wrap_edits));
-// assert_eq!(
-// actual_text, expected_text,
-// "unwrapped text is: {:?}",
-// unwrapped_text
-// );
-
-// let mut summary = TextSummary::default();
-// for (ix, item) in wrapped_snapshot
-// .transforms
-// .items(&())
-// .into_iter()
-// .enumerate()
-// {
-// summary += &item.summary.output;
-// log::info!("{} summary: {:?}", ix, item.summary.output,);
-// }
-
-// if tab_size.get() == 1
-// || !wrapped_snapshot
-// .tab_snapshot
-// .fold_snapshot
-// .text()
-// .contains('\t')
-// {
-// let mut expected_longest_rows = Vec::new();
-// let mut longest_line_len = -1;
-// for (row, line) in expected_text.split('\n').enumerate() {
-// let line_char_count = line.chars().count() as isize;
-// if line_char_count > longest_line_len {
-// expected_longest_rows.clear();
-// longest_line_len = line_char_count;
-// }
-// if line_char_count >= longest_line_len {
-// expected_longest_rows.push(row as u32);
-// }
-// }
-
-// assert!(
-// expected_longest_rows.contains(&actual_longest_row),
-// "incorrect longest row {}. expected {:?} with length {}",
-// actual_longest_row,
-// expected_longest_rows,
-// longest_line_len,
-// )
-// }
-// }
-// }
-
-// let mut initial_text = Rope::from(initial_snapshot.text().as_str());
-// for (snapshot, patch) in edits {
-// let snapshot_text = Rope::from(snapshot.text().as_str());
-// for edit in &patch {
-// let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
-// let old_end = initial_text.point_to_offset(cmp::min(
-// Point::new(edit.new.start + edit.old.len() as u32, 0),
-// initial_text.max_point(),
-// ));
-// let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
-// let new_end = snapshot_text.point_to_offset(cmp::min(
-// Point::new(edit.new.end, 0),
-// snapshot_text.max_point(),
-// ));
-// let new_text = snapshot_text
-// .chunks_in_range(new_start..new_end)
-// .collect::<String>();
-
-// initial_text.replace(old_start..old_end, &new_text);
-// }
-// assert_eq!(initial_text.to_string(), snapshot_text.to_string());
-// }
-
-// if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-// log::info!("Waiting for wrapping to finish");
-// while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
-// notifications.next().await.unwrap();
-// }
-// }
-// wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
-// }
-
-// fn init_test(cx: &mut gpui::TestAppContext) {
-// cx.foreground().forbid_parking();
-// cx.update(|cx| {
-// cx.set_global(SettingsStore::test(cx));
-// theme::init((), cx);
-// });
-// }
-
-// fn wrap_text(
-// unwrapped_text: &str,
-// wrap_width: Option<f32>,
-// line_wrapper: &mut LineWrapper,
-// ) -> String {
-// if let Some(wrap_width) = wrap_width {
-// let mut wrapped_text = String::new();
-// for (row, line) in unwrapped_text.split('\n').enumerate() {
-// if row > 0 {
-// wrapped_text.push('\n')
-// }
-
-// let mut prev_ix = 0;
-// for boundary in line_wrapper.wrap_line(line, wrap_width) {
-// wrapped_text.push_str(&line[prev_ix..boundary.ix]);
-// wrapped_text.push('\n');
-// wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
-// prev_ix = boundary.ix;
-// }
-// wrapped_text.push_str(&line[prev_ix..]);
-// }
-// wrapped_text
-// } else {
-// unwrapped_text.to_string()
-// }
-// }
-
-// impl WrapSnapshot {
-// pub fn text(&self) -> String {
-// self.text_chunks(0).collect()
-// }
-
-// pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
-// self.chunks(
-// wrap_row..self.max_point().row() + 1,
-// false,
-// Highlights::default(),
-// )
-// .map(|h| h.text)
-// }
-
-// fn verify_chunks(&mut self, rng: &mut impl Rng) {
-// for _ in 0..5 {
-// let mut end_row = rng.gen_range(0..=self.max_point().row());
-// let start_row = rng.gen_range(0..=end_row);
-// end_row += 1;
-
-// let mut expected_text = self.text_chunks(start_row).collect::<String>();
-// if expected_text.ends_with('\n') {
-// expected_text.push('\n');
-// }
-// let mut expected_text = expected_text
-// .lines()
-// .take((end_row - start_row) as usize)
-// .collect::<Vec<_>>()
-// .join("\n");
-// if end_row <= self.max_point().row() {
-// expected_text.push('\n');
-// }
-
-// let actual_text = self
-// .chunks(start_row..end_row, true, Highlights::default())
-// .map(|c| c.text)
-// .collect::<String>();
-// assert_eq!(
-// expected_text,
-// actual_text,
-// "chunks != highlighted_chunks for rows {:?}",
-// start_row..end_row
-// );
-// }
-// }
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+ MultiBuffer,
+ };
+ use gpui::{font, px, test::observe, Platform};
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use smol::stream::StreamExt;
+ use std::{cmp, env, num::NonZeroU32};
+ use text::Rope;
+ use theme::LoadThemes;
+
+ #[gpui::test(iterations = 100)]
+ async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ // todo!() this test is flaky
+ init_test(cx);
+
+ cx.background_executor.set_block_on_ticks(0..=50);
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let text_system = cx.read(|cx| cx.text_system().clone());
+ let mut wrap_width = if rng.gen_bool(0.1) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=1000.0)))
+ };
+ let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+ let font = font("Helvetica");
+ let font_id = text_system.font_id(&font).unwrap();
+ let font_size = px(14.0);
+
+ log::info!("Tab size: {}", tab_size);
+ log::info!("Wrap width: {:?}", wrap_width);
+
+ let buffer = cx.update(|cx| {
+ if rng.gen() {
+ MultiBuffer::build_random(&mut rng, cx)
+ } else {
+ let len = rng.gen_range(0..10);
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ }
+ });
+ let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+ log::info!("Buffer text: {:?}", buffer_snapshot.text());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+ log::info!("FoldMap text: {:?}", fold_snapshot.text());
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+ let tabs_snapshot = tab_map.set_max_expansion_column(32);
+ log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+ let mut line_wrapper = text_system.line_wrapper(font.clone(), font_size).unwrap();
+ let unwrapped_text = tabs_snapshot.text();
+ let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+
+ let (wrap_map, _) =
+ cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font, font_size, wrap_width, cx));
+ let mut notifications = observe(&wrap_map, cx);
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+
+ let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
+ assert!(!map.is_rewrapping());
+ map.sync(tabs_snapshot.clone(), Vec::new(), cx)
+ });
+
+ let actual_text = initial_snapshot.text();
+ assert_eq!(
+ actual_text, expected_text,
+ "unwrapped text is: {:?}",
+ unwrapped_text
+ );
+ log::info!("Wrapped text: {:?}", actual_text);
+
+ let mut next_inlay_id = 0;
+ let mut edits = Vec::new();
+ for _i in 0..operations {
+ log::info!("{} ==============================================", _i);
+
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=19 => {
+ wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(px(rng.gen_range(0.0..=1000.0)))
+ };
+ log::info!("Setting wrap width to {:?}", wrap_width);
+ wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+ }
+ 20..=39 => {
+ for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+ }
+ }
+ 40..=59 => {
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+ }
+ _ => {
+ buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ buffer_edits.extend(subscription.consume());
+ });
+ }
+ }
+
+ log::info!("Buffer text: {:?}", buffer_snapshot.text());
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ log::info!("FoldMap text: {:?}", fold_snapshot.text());
+ let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+ let unwrapped_text = tabs_snapshot.text();
+ let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
+ log::info!("Waiting for wrapping to finish");
+ while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+ wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+ }
+
+ if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ let (mut wrapped_snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
+ let actual_text = wrapped_snapshot.text();
+ let actual_longest_row = wrapped_snapshot.longest_row();
+ log::info!("Wrapping finished: {:?}", actual_text);
+ wrapped_snapshot.check_invariants();
+ wrapped_snapshot.verify_chunks(&mut rng);
+ edits.push((wrapped_snapshot.clone(), wrap_edits));
+ assert_eq!(
+ actual_text, expected_text,
+ "unwrapped text is: {:?}",
+ unwrapped_text
+ );
+
+ let mut summary = TextSummary::default();
+ for (ix, item) in wrapped_snapshot
+ .transforms
+ .items(&())
+ .into_iter()
+ .enumerate()
+ {
+ summary += &item.summary.output;
+ log::info!("{} summary: {:?}", ix, item.summary.output,);
+ }
+
+ if tab_size.get() == 1
+ || !wrapped_snapshot
+ .tab_snapshot
+ .fold_snapshot
+ .text()
+ .contains('\t')
+ {
+ let mut expected_longest_rows = Vec::new();
+ let mut longest_line_len = -1;
+ for (row, line) in expected_text.split('\n').enumerate() {
+ let line_char_count = line.chars().count() as isize;
+ if line_char_count > longest_line_len {
+ expected_longest_rows.clear();
+ longest_line_len = line_char_count;
+ }
+ if line_char_count >= longest_line_len {
+ expected_longest_rows.push(row as u32);
+ }
+ }
+
+ assert!(
+ expected_longest_rows.contains(&actual_longest_row),
+ "incorrect longest row {}. expected {:?} with length {}",
+ actual_longest_row,
+ expected_longest_rows,
+ longest_line_len,
+ )
+ }
+ }
+ }
+
+ let mut initial_text = Rope::from(initial_snapshot.text().as_str());
+ for (snapshot, patch) in edits {
+ let snapshot_text = Rope::from(snapshot.text().as_str());
+ for edit in &patch {
+ let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
+ let old_end = initial_text.point_to_offset(cmp::min(
+ Point::new(edit.new.start + edit.old.len() as u32, 0),
+ initial_text.max_point(),
+ ));
+ let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
+ let new_end = snapshot_text.point_to_offset(cmp::min(
+ Point::new(edit.new.end, 0),
+ snapshot_text.max_point(),
+ ));
+ let new_text = snapshot_text
+ .chunks_in_range(new_start..new_end)
+ .collect::<String>();
+
+ initial_text.replace(old_start..old_end, &new_text);
+ }
+ assert_eq!(initial_text.to_string(), snapshot_text.to_string());
+ }
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ log::info!("Waiting for wrapping to finish");
+ while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+ }
+ wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+ }
+
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ let settings = SettingsStore::test(cx);
+ cx.set_global(settings);
+ theme::init(LoadThemes::JustBase, cx);
+ });
+ }
+
+ fn wrap_text(
+ unwrapped_text: &str,
+ wrap_width: Option<Pixels>,
+ line_wrapper: &mut LineWrapper,
+ ) -> String {
+ if let Some(wrap_width) = wrap_width {
+ let mut wrapped_text = String::new();
+ for (row, line) in unwrapped_text.split('\n').enumerate() {
+ if row > 0 {
+ wrapped_text.push('\n')
+ }
+
+ let mut prev_ix = 0;
+ for boundary in line_wrapper.wrap_line(line, wrap_width) {
+ wrapped_text.push_str(&line[prev_ix..boundary.ix]);
+ wrapped_text.push('\n');
+ wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
+ prev_ix = boundary.ix;
+ }
+ wrapped_text.push_str(&line[prev_ix..]);
+ }
+ wrapped_text
+ } else {
+ unwrapped_text.to_string()
+ }
+ }
+
+ impl WrapSnapshot {
+ pub fn text(&self) -> String {
+ self.text_chunks(0).collect()
+ }
+
+ pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+ self.chunks(
+ wrap_row..self.max_point().row() + 1,
+ false,
+ Highlights::default(),
+ )
+ .map(|h| h.text)
+ }
+
+ fn verify_chunks(&mut self, rng: &mut impl Rng) {
+ for _ in 0..5 {
+ let mut end_row = rng.gen_range(0..=self.max_point().row());
+ let start_row = rng.gen_range(0..=end_row);
+ end_row += 1;
+
+ let mut expected_text = self.text_chunks(start_row).collect::<String>();
+ if expected_text.ends_with('\n') {
+ expected_text.push('\n');
+ }
+ let mut expected_text = expected_text
+ .lines()
+ .take((end_row - start_row) as usize)
+ .collect::<Vec<_>>()
+ .join("\n");
+ if end_row <= self.max_point().row() {
+ expected_text.push('\n');
+ }
+
+ let actual_text = self
+ .chunks(start_row..end_row, true, Highlights::default())
+ .map(|c| c.text)
+ .collect::<String>();
+ assert_eq!(
+ expected_text,
+ actual_text,
+ "chunks != highlighted_chunks for rows {:?}",
+ start_row..end_row
+ );
+ }
+ }
+ }
+}
@@ -63,6 +63,7 @@ use language::{
use lazy_static::lazy_static;
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
use lsp::{DiagnosticSeverity, LanguageServerId};
+use mouse_context_menu::MouseContextMenu;
use movement::TextLayoutDetails;
use multi_buffer::ToOffsetUtf16;
pub use multi_buffer::{
@@ -99,8 +100,10 @@ use text::{OffsetUtf16, Rope};
use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
};
-use ui::prelude::*;
-use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, Tooltip};
+use ui::{
+ h_stack, v_stack, ButtonSize, ButtonStyle, HighlightedLabel, Icon, IconButton, Popover, Tooltip,
+};
+use ui::{prelude::*, IconSize};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::{ItemEvent, ItemHandle},
@@ -153,7 +156,6 @@ pub fn render_parsed_markdown(
}
}),
);
- let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights);
let mut links = Vec::new();
let mut link_ranges = Vec::new();
@@ -166,7 +168,7 @@ pub fn render_parsed_markdown(
InteractiveText::new(
element_id,
- StyledText::new(parsed.text.clone()).with_runs(runs),
+ StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights),
)
.on_click(link_ranges, move |clicked_range_ix, cx| {
match &links[clicked_range_ix] {
@@ -407,133 +409,17 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) {
init_settings(cx);
- // cx.register_action_type(Editor::new_file);
- // cx.register_action_type(Editor::new_file_in_direction);
- // cx.register_action_type(Editor::cancel);
- // cx.register_action_type(Editor::newline);
- // cx.register_action_type(Editor::newline_above);
- // cx.register_action_type(Editor::newline_below);
- // cx.register_action_type(Editor::backspace);
- // cx.register_action_type(Editor::delete);
- // cx.register_action_type(Editor::tab);
- // cx.register_action_type(Editor::tab_prev);
- // cx.register_action_type(Editor::indent);
- // cx.register_action_type(Editor::outdent);
- // cx.register_action_type(Editor::delete_line);
- // cx.register_action_type(Editor::join_lines);
- // cx.register_action_type(Editor::sort_lines_case_sensitive);
- // cx.register_action_type(Editor::sort_lines_case_insensitive);
- // cx.register_action_type(Editor::reverse_lines);
- // cx.register_action_type(Editor::shuffle_lines);
- // cx.register_action_type(Editor::convert_to_upper_case);
- // cx.register_action_type(Editor::convert_to_lower_case);
- // cx.register_action_type(Editor::convert_to_title_case);
- // cx.register_action_type(Editor::convert_to_snake_case);
- // cx.register_action_type(Editor::convert_to_kebab_case);
- // cx.register_action_type(Editor::convert_to_upper_camel_case);
- // cx.register_action_type(Editor::convert_to_lower_camel_case);
- // cx.register_action_type(Editor::delete_to_previous_word_start);
- // cx.register_action_type(Editor::delete_to_previous_subword_start);
- // cx.register_action_type(Editor::delete_to_next_word_end);
- // cx.register_action_type(Editor::delete_to_next_subword_end);
- // cx.register_action_type(Editor::delete_to_beginning_of_line);
- // cx.register_action_type(Editor::delete_to_end_of_line);
- // cx.register_action_type(Editor::cut_to_end_of_line);
- // cx.register_action_type(Editor::duplicate_line);
- // cx.register_action_type(Editor::move_line_up);
- // cx.register_action_type(Editor::move_line_down);
- // cx.register_action_type(Editor::transpose);
- // cx.register_action_type(Editor::cut);
- // cx.register_action_type(Editor::copy);
- // cx.register_action_type(Editor::paste);
- // cx.register_action_type(Editor::undo);
- // cx.register_action_type(Editor::redo);
- // cx.register_action_type(Editor::move_page_up);
- // cx.register_action_type::<MoveDown>();
- // cx.register_action_type(Editor::move_page_down);
- // cx.register_action_type(Editor::next_screen);
- // cx.register_action_type::<MoveLeft>();
- // cx.register_action_type::<MoveRight>();
- // cx.register_action_type(Editor::move_to_previous_word_start);
- // cx.register_action_type(Editor::move_to_previous_subword_start);
- // cx.register_action_type(Editor::move_to_next_word_end);
- // cx.register_action_type(Editor::move_to_next_subword_end);
- // cx.register_action_type(Editor::move_to_beginning_of_line);
- // cx.register_action_type(Editor::move_to_end_of_line);
- // cx.register_action_type(Editor::move_to_start_of_paragraph);
- // cx.register_action_type(Editor::move_to_end_of_paragraph);
- // cx.register_action_type(Editor::move_to_beginning);
- // cx.register_action_type(Editor::move_to_end);
- // cx.register_action_type(Editor::select_up);
- // cx.register_action_type(Editor::select_down);
- // cx.register_action_type(Editor::select_left);
- // cx.register_action_type(Editor::select_right);
- // cx.register_action_type(Editor::select_to_previous_word_start);
- // cx.register_action_type(Editor::select_to_previous_subword_start);
- // cx.register_action_type(Editor::select_to_next_word_end);
- // cx.register_action_type(Editor::select_to_next_subword_end);
- // cx.register_action_type(Editor::select_to_beginning_of_line);
- // cx.register_action_type(Editor::select_to_end_of_line);
- // cx.register_action_type(Editor::select_to_start_of_paragraph);
- // cx.register_action_type(Editor::select_to_end_of_paragraph);
- // cx.register_action_type(Editor::select_to_beginning);
- // cx.register_action_type(Editor::select_to_end);
- // cx.register_action_type(Editor::select_all);
- // cx.register_action_type(Editor::select_all_matches);
- // cx.register_action_type(Editor::select_line);
- // cx.register_action_type(Editor::split_selection_into_lines);
- // cx.register_action_type(Editor::add_selection_above);
- // cx.register_action_type(Editor::add_selection_below);
- // cx.register_action_type(Editor::select_next);
- // cx.register_action_type(Editor::select_previous);
- // cx.register_action_type(Editor::toggle_comments);
- // cx.register_action_type(Editor::select_larger_syntax_node);
- // cx.register_action_type(Editor::select_smaller_syntax_node);
- // cx.register_action_type(Editor::move_to_enclosing_bracket);
- // cx.register_action_type(Editor::undo_selection);
- // cx.register_action_type(Editor::redo_selection);
- // cx.register_action_type(Editor::go_to_diagnostic);
- // cx.register_action_type(Editor::go_to_prev_diagnostic);
- // cx.register_action_type(Editor::go_to_hunk);
- // cx.register_action_type(Editor::go_to_prev_hunk);
- // cx.register_action_type(Editor::go_to_definition);
- // cx.register_action_type(Editor::go_to_definition_split);
- // cx.register_action_type(Editor::go_to_type_definition);
- // cx.register_action_type(Editor::go_to_type_definition_split);
- // cx.register_action_type(Editor::fold);
- // cx.register_action_type(Editor::fold_at);
- // cx.register_action_type(Editor::unfold_lines);
- // cx.register_action_type(Editor::unfold_at);
- // cx.register_action_type(Editor::gutter_hover);
- // cx.register_action_type(Editor::fold_selected_ranges);
- // cx.register_action_type(Editor::show_completions);
- // cx.register_action_type(Editor::toggle_code_actions);
- // cx.register_action_type(Editor::open_excerpts);
- // cx.register_action_type(Editor::toggle_soft_wrap);
- // cx.register_action_type(Editor::toggle_inlay_hints);
- // cx.register_action_type(Editor::reveal_in_finder);
- // cx.register_action_type(Editor::copy_path);
- // cx.register_action_type(Editor::copy_relative_path);
- // cx.register_action_type(Editor::copy_highlight_json);
- // cx.add_async_action(Editor::format);
- // cx.register_action_type(Editor::restart_language_server);
- // cx.register_action_type(Editor::show_character_palette);
- // cx.add_async_action(Editor::confirm_completion);
- // cx.add_async_action(Editor::confirm_code_action);
- // cx.add_async_action(Editor::rename);
- // cx.add_async_action(Editor::confirm_rename);
- // cx.add_async_action(Editor::find_all_references);
- // cx.register_action_type(Editor::next_copilot_suggestion);
- // cx.register_action_type(Editor::previous_copilot_suggestion);
- // cx.register_action_type(Editor::copilot_suggest);
- // cx.register_action_type(Editor::context_menu_first);
- // cx.register_action_type(Editor::context_menu_prev);
- // cx.register_action_type(Editor::context_menu_next);
- // cx.register_action_type(Editor::context_menu_last);
workspace::register_project_item::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
+ cx.observe_new_views(
+ |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+ workspace.register_action(Editor::new_file);
+ workspace.register_action(Editor::new_file_in_direction);
+ },
+ )
+ .detach();
}
trait InvalidationRegion {
@@ -621,8 +507,6 @@ pub struct Editor {
ime_transaction: Option<TransactionId>,
active_diagnostics: Option<ActiveDiagnosticGroup>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
- // get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
- // override_text_style: Option<Box<OverrideTextStyle>>,
project: Option<Model<Project>>,
collaboration_hub: Option<Box<dyn CollaborationHub>>,
blink_manager: Model<BlinkManager>,
@@ -636,7 +520,7 @@ pub struct Editor {
inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
- // mouse_context_menu: View<context_menu::ContextMenu>,
+ mouse_context_menu: Option<MouseContextMenu>,
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
next_completion_id: CompletionId,
available_code_actions: Option<(Model<Buffer>, Arc<[CodeAction]>)>,
@@ -1316,11 +1200,7 @@ impl CompletionsMenu {
),
);
let completion_label = StyledText::new(completion.label.text.clone())
- .with_runs(text_runs_for_highlights(
- &completion.label.text,
- &style.text,
- highlights,
- ));
+ .with_highlights(&style.text, highlights);
let documentation_label =
if let Some(Documentation::SingleLine(text)) = documentation {
Some(SharedString::from(text.clone()))
@@ -1734,21 +1614,11 @@ impl Editor {
// Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
// }
- // pub fn auto_height(
- // max_lines: usize,
- // field_editor_style: Option<Arc<GetFieldEditorTheme>>,
- // cx: &mut ViewContext<Self>,
- // ) -> Self {
- // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
- // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
- // Self::new(
- // EditorMode::AutoHeight { max_lines },
- // buffer,
- // None,
- // field_editor_style,
- // cx,
- // )
- // }
+ pub fn auto_height(max_lines: usize, cx: &mut ViewContext<Self>) -> Self {
+ let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new()));
+ let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
+ Self::new(EditorMode::AutoHeight { max_lines }, buffer, None, cx)
+ }
pub fn for_buffer(
buffer: Model<Buffer>,
@@ -1768,14 +1638,7 @@ impl Editor {
}
pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
- let mut clone = Self::new(
- self.mode,
- self.buffer.clone(),
- self.project.clone(),
- // todo!
- // self.get_field_editor_theme.clone(),
- cx,
- );
+ let mut clone = Self::new(self.mode, self.buffer.clone(), self.project.clone(), cx);
self.display_map.update(cx, |display_map, cx| {
let snapshot = display_map.snapshot(cx);
clone.display_map.update(cx, |display_map, cx| {
@@ -1792,17 +1655,11 @@ impl Editor {
mode: EditorMode,
buffer: Model<MultiBuffer>,
project: Option<Model<Project>>,
- // todo!()
- // get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
cx: &mut ViewContext<Self>,
) -> Self {
- // let editor_view_id = cx.view_id();
let style = cx.text_style();
let font_size = style.font_size.to_pixels(cx.rem_size());
let display_map = cx.build_model(|cx| {
- // todo!()
- // let settings = settings::get::<ThemeSettings>(cx);
- // let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx)
});
@@ -1858,7 +1715,6 @@ impl Editor {
ime_transaction: Default::default(),
active_diagnostics: None,
soft_wrap_mode_override,
- // get_field_editor_theme,
collaboration_hub: project.clone().map(|project| Box::new(project) as _),
project,
blink_manager: blink_manager.clone(),
@@ -1872,8 +1728,7 @@ impl Editor {
inlay_background_highlights: Default::default(),
nav_history: None,
context_menu: RwLock::new(None),
- // mouse_context_menu: cx
- // .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
+ mouse_context_menu: None,
completion_tasks: Default::default(),
next_completion_id: 0,
next_inlay_id: 0,
@@ -1882,7 +1737,6 @@ impl Editor {
document_highlights_task: Default::default(),
pending_rename: Default::default(),
searchable: true,
- // override_text_style: None,
cursor_shape: Default::default(),
autoindent_mode: Some(AutoindentMode::EachLine),
collapse_matches: false,
@@ -2000,25 +1854,25 @@ impl Editor {
}
}
- // pub fn new_file_in_direction(
- // workspace: &mut Workspace,
- // action: &workspace::NewFileInDirection,
- // 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.split_item(
- // action.0,
- // Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
- // cx,
- // );
- // }
- // }
+ pub fn new_file_in_direction(
+ workspace: &mut Workspace,
+ action: &workspace::NewFileInDirection,
+ 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.split_item(
+ action.0,
+ Box::new(cx.build_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+ cx,
+ );
+ }
+ }
pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
self.buffer.read(cx).replica_id()
@@ -2068,14 +1922,14 @@ impl Editor {
// self.buffer.read(cx).read(cx).file_at(point).cloned()
// }
- // pub fn active_excerpt(
- // &self,
- // cx: &AppContext,
- // ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
- // self.buffer
- // .read(cx)
- // .excerpt_containing(self.selections.newest_anchor().head(), cx)
- // }
+ pub fn active_excerpt(
+ &self,
+ cx: &AppContext,
+ ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
+ self.buffer
+ .read(cx)
+ .excerpt_containing(self.selections.newest_anchor().head(), cx)
+ }
// pub fn style(&self, cx: &AppContext) -> EditorStyle {
// build_style(
@@ -3632,7 +3486,7 @@ impl Editor {
drop(context_menu);
this.discard_copilot_suggestion(cx);
cx.notify();
- } else if this.completion_tasks.is_empty() {
+ } else if this.completion_tasks.len() <= 1 {
// If there are no more completion tasks and the last menu was
// empty, we should hide it. If it was already hidden, we should
// also show the copilot suggestion when available.
@@ -8374,6 +8228,23 @@ impl Editor {
cx.notify();
}
+ pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
+ let rem_size = cx.rem_size();
+ self.display_map.update(cx, |map, cx| {
+ map.set_font(
+ style.text.font(),
+ style.text.font_size.to_pixels(rem_size),
+ cx,
+ )
+ });
+ self.style = Some(style);
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn style(&self) -> Option<&EditorStyle> {
+ self.style.as_ref()
+ }
+
pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut AppContext) -> bool {
self.display_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -8796,62 +8667,56 @@ impl Editor {
// self.searchable
// }
- // fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext<Workspace>) {
- // let active_item = workspace.active_item(cx);
- // let editor_handle = if let Some(editor) = active_item
- // .as_ref()
- // .and_then(|item| item.act_as::<Self>(cx))
- // {
- // editor
- // } else {
- // cx.propagate();
- // return;
- // };
-
- // let editor = editor_handle.read(cx);
- // let buffer = editor.buffer.read(cx);
- // if buffer.is_singleton() {
- // cx.propagate();
- // return;
- // }
+ fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx);
+ if buffer.is_singleton() {
+ cx.propagate();
+ return;
+ }
- // let mut new_selections_by_buffer = HashMap::default();
- // for selection in editor.selections.all::<usize>(cx) {
- // for (buffer, mut range, _) in
- // buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
- // {
- // if selection.reversed {
- // mem::swap(&mut range.start, &mut range.end);
- // }
- // new_selections_by_buffer
- // .entry(buffer)
- // .or_insert(Vec::new())
- // .push(range)
- // }
- // }
+ let Some(workspace) = self.workspace() else {
+ cx.propagate();
+ return;
+ };
- // editor_handle.update(cx, |editor, cx| {
- // editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
- // });
- // let pane = workspace.active_pane().clone();
- // pane.update(cx, |pane, _| pane.disable_history());
-
- // // We defer the pane interaction because we ourselves are a workspace item
- // // and activating a new item causes the pane to call a method on us reentrantly,
- // // which panics if we're on the stack.
- // cx.defer(move |workspace, cx| {
- // for (buffer, ranges) in new_selections_by_buffer.into_iter() {
- // let editor = workspace.open_project_item::<Self>(buffer, cx);
- // editor.update(cx, |editor, cx| {
- // editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
- // s.select_ranges(ranges);
- // });
- // });
- // }
-
- // pane.update(cx, |pane, _| pane.enable_history());
- // });
- // }
+ let mut new_selections_by_buffer = HashMap::default();
+ for selection in self.selections.all::<usize>(cx) {
+ for (buffer, mut range, _) in
+ buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+ {
+ if selection.reversed {
+ mem::swap(&mut range.start, &mut range.end);
+ }
+ new_selections_by_buffer
+ .entry(buffer)
+ .or_insert(Vec::new())
+ .push(range)
+ }
+ }
+
+ self.push_to_nav_history(self.selections.newest_anchor().head(), None, cx);
+
+ // We defer the pane interaction because we ourselves are a workspace item
+ // and activating a new item causes the pane to call a method on us reentrantly,
+ // which panics if we're on the stack.
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ let pane = workspace.active_pane().clone();
+ pane.update(cx, |pane, _| pane.disable_history());
+
+ for (buffer, ranges) in new_selections_by_buffer.into_iter() {
+ let editor = workspace.open_project_item::<Self>(buffer, cx);
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+ s.select_ranges(ranges);
+ });
+ });
+ }
+
+ pane.update(cx, |pane, _| pane.enable_history());
+ })
+ });
+ }
fn jump(
&mut self,
@@ -9397,7 +9262,7 @@ impl Render for Editor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let settings = ThemeSettings::get_global(cx);
let text_style = match self.mode {
- EditorMode::SingleLine => TextStyle {
+ EditorMode::SingleLine | EditorMode::AutoHeight { .. } => TextStyle {
color: cx.theme().colors().text,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
@@ -9410,8 +9275,6 @@ impl Render for Editor {
white_space: WhiteSpace::Normal,
},
- EditorMode::AutoHeight { max_lines } => todo!(),
-
EditorMode::Full => TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
@@ -9446,106 +9309,6 @@ impl Render for Editor {
}
}
-// impl View for Editor {
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// let style = self.style(cx);
-// let font_changed = self.display_map.update(cx, |map, cx| {
-// map.set_fold_ellipses_color(style.folds.ellipses.text_color);
-// map.set_font_with_size(style.text.font_id, style.text.font_size, cx)
-// });
-
-// if font_changed {
-// cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
-// hide_hover(editor, cx);
-// hide_link_definition(editor, cx);
-// });
-// }
-
-// Stack::new()
-// .with_child(EditorElement::new(style.clone()))
-// .with_child(ChildView::new(&self.mouse_context_menu, cx))
-// .into_any()
-// }
-
-// fn ui_name() -> &'static str {
-// "Editor"
-// }
-
-// fn focus_in(&mut self, focused: AnyView, cx: &mut ViewContext<Self>) {
-// if cx.is_self_focused() {
-// let focused_event = EditorFocused(cx.handle());
-// cx.emit(Event::Focused);
-// cx.emit_global(focused_event);
-// }
-// if let Some(rename) = self.pending_rename.as_ref() {
-// cx.focus(&rename.editor);
-// } else if cx.is_self_focused() || !focused.is::<Editor>() {
-// if !self.focused {
-// self.blink_manager.update(cx, BlinkManager::enable);
-// }
-// self.focused = true;
-// self.buffer.update(cx, |buffer, cx| {
-// buffer.finalize_last_transaction(cx);
-// if self.leader_peer_id.is_none() {
-// buffer.set_active_selections(
-// &self.selections.disjoint_anchors(),
-// self.selections.line_mode,
-// self.cursor_shape,
-// cx,
-// );
-// }
-// });
-// }
-// }
-
-// fn focus_out(&mut self, _: AnyView, cx: &mut ViewContext<Self>) {
-// let blurred_event = EditorBlurred(cx.handle());
-// cx.emit_global(blurred_event);
-// self.focused = false;
-// self.blink_manager.update(cx, BlinkManager::disable);
-// self.buffer
-// .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
-// self.hide_context_menu(cx);
-// hide_hover(self, cx);
-// cx.emit(Event::Blurred);
-// cx.notify();
-// }
-
-// fn modifiers_changed(
-// &mut self,
-// event: &gpui::platform::ModifiersChangedEvent,
-// cx: &mut ViewContext<Self>,
-// ) -> bool {
-// let pending_selection = self.has_pending_selection();
-
-// if let Some(point) = &self.link_go_to_definition_state.last_trigger_point {
-// if event.cmd && !pending_selection {
-// let point = point.clone();
-// let snapshot = self.snapshot(cx);
-// let kind = point.definition_kind(event.shift);
-
-// show_link_definition(kind, self, point, snapshot, cx);
-// return false;
-// }
-// }
-
-// {
-// if self.link_go_to_definition_state.symbol_range.is_some()
-// || !self.link_go_to_definition_state.definitions.is_empty()
-// {
-// self.link_go_to_definition_state.symbol_range.take();
-// self.link_go_to_definition_state.definitions.clear();
-// cx.notify();
-// }
-
-// self.link_go_to_definition_state.task = None;
-
-// self.clear_highlights::<LinkGoToDefinitionState>(cx);
-// }
-
-// false
-// }
-
impl InputHandler for Editor {
fn text_for_range(
&mut self,
@@ -9792,72 +9555,6 @@ impl InputHandler for Editor {
}
}
-// fn build_style(
-// settings: &ThemeSettings,
-// get_field_editor_theme: Option<&GetFieldEditorTheme>,
-// override_text_style: Option<&OverrideTextStyle>,
-// cx: &mut AppContext,
-// ) -> EditorStyle {
-// let font_cache = cx.font_cache();
-// let line_height_scalar = settings.line_height();
-// let theme_id = settings.theme.meta.id;
-// let mut theme = settings.theme.editor.clone();
-// let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme {
-// let field_editor_theme = get_field_editor_theme(&settings.theme);
-// theme.text_color = field_editor_theme.text.color;
-// theme.selection = field_editor_theme.selection;
-// theme.background = field_editor_theme
-// .container
-// .background_color
-// .unwrap_or_default();
-// EditorStyle {
-// text: field_editor_theme.text,
-// placeholder_text: field_editor_theme.placeholder_text,
-// line_height_scalar,
-// theme,
-// theme_id,
-// }
-// } else {
-// todo!();
-// // let font_family_id = settings.buffer_font_family;
-// // let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
-// // let font_properties = Default::default();
-// // let font_id = font_cache
-// // .select_font(font_family_id, &font_properties)
-// // .unwrap();
-// // let font_size = settings.buffer_font_size(cx);
-// // EditorStyle {
-// // text: TextStyle {
-// // color: settings.theme.editor.text_color,
-// // font_family_name,
-// // font_family_id,
-// // font_id,
-// // font_size,
-// // font_properties,
-// // underline: Default::default(),
-// // soft_wrap: false,
-// // },
-// // placeholder_text: None,
-// // line_height_scalar,
-// // theme,
-// // theme_id,
-// // }
-// };
-
-// if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) {
-// if let Some(highlighted) = style
-// .text
-// .clone()
-// .highlight(highlight_style, font_cache)
-// .log_err()
-// {
-// style.text = highlighted;
-// }
-// }
-
-// style
-// }
-
trait SelectionExt {
fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
@@ -9999,20 +9696,42 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
let message = diagnostic.message;
Arc::new(move |cx: &mut BlockContext| {
let message = message.clone();
+ let copy_id: SharedString = format!("copy-{}", cx.block_id.clone()).to_string().into();
+ let write_to_clipboard = cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+
+ // TODO: Nate: We should tint the background of the block with the severity color
+ // We need to extend the theme before we can do this
v_stack()
.id(cx.block_id)
+ .relative()
.size_full()
.bg(gpui::red())
.children(highlighted_lines.iter().map(|(line, highlights)| {
- div()
+ let group_id = cx.block_id.to_string();
+
+ h_stack()
+ .group(group_id.clone())
+ .gap_2()
+ .absolute()
+ .left(cx.anchor_x)
+ .px_1p5()
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
- .ml(cx.anchor_x)
- }))
- .cursor_pointer()
- .on_click(cx.listener(move |_, _, cx| {
- cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+ .child(
+ div()
+ .border()
+ .border_color(gpui::red())
+ .invisible()
+ .group_hover(group_id, |style| style.visible())
+ .child(
+ IconButton::new(copy_id.clone(), Icon::Copy)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::Compact)
+ .style(ButtonStyle::Transparent)
+ .on_click(cx.listener(move |_, _, cx| write_to_clipboard))
+ .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
+ ),
+ )
}))
- .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx))
.into_any_element()
})
}
@@ -10060,31 +9779,6 @@ pub fn diagnostic_style(
}
}
-pub fn text_runs_for_highlights(
- text: &str,
- default_style: &TextStyle,
- highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
-) -> Vec<TextRun> {
- let mut runs = Vec::new();
- let mut ix = 0;
- for (range, highlight) in highlights {
- if ix < range.start {
- runs.push(default_style.clone().to_run(range.start - ix));
- }
- runs.push(
- default_style
- .clone()
- .highlight(highlight)
- .to_run(range.len()),
- );
- ix = range.end;
- }
- if ix < text.len() {
- runs.push(default_style.to_run(text.len() - ix));
- }
- runs
-}
-
pub fn styled_runs_for_code_label<'a>(
label: &'a CodeLabel,
syntax_theme: &'a theme::SyntaxTheme,
@@ -12,7 +12,7 @@ use futures::StreamExt;
use gpui::{
div,
serde_json::{self, json},
- Div, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
+ Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions,
};
use indoc::indoc;
use language::{
@@ -36,121 +36,120 @@ use workspace::{
NavigationEntry, ViewId,
};
-// todo(finish edit tests)
-// #[gpui::test]
-// fn test_edit_events(cx: &mut TestAppContext) {
-// init_test(cx, |_| {});
-
-// let buffer = cx.build_model(|cx| {
-// let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456");
-// buffer.set_group_interval(Duration::from_secs(1));
-// buffer
-// });
-
-// let events = Rc::new(RefCell::new(Vec::new()));
-// let editor1 = cx.add_window({
-// let events = events.clone();
-// |cx| {
-// let view = cx.view().clone();
-// cx.subscribe(&view, move |_, _, event, _| {
-// if matches!(event, Event::Edited | Event::BufferEdited) {
-// events.borrow_mut().push(("editor1", event.clone()));
-// }
-// })
-// .detach();
-// Editor::for_buffer(buffer.clone(), None, cx)
-// }
-// });
-
-// let editor2 = cx.add_window({
-// let events = events.clone();
-// |cx| {
-// cx.subscribe(&cx.view().clone(), move |_, _, event, _| {
-// if matches!(event, Event::Edited | Event::BufferEdited) {
-// events.borrow_mut().push(("editor2", event.clone()));
-// }
-// })
-// .detach();
-// Editor::for_buffer(buffer.clone(), None, cx)
-// }
-// });
-
-// assert_eq!(mem::take(&mut *events.borrow_mut()), []);
-
-// // Mutating editor 1 will emit an `Edited` event only for that editor.
-// editor1.update(cx, |editor, cx| editor.insert("X", cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor1", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // Mutating editor 2 will emit an `Edited` event only for that editor.
-// editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor2", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // Undoing on editor 1 will emit an `Edited` event only for that editor.
-// editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor1", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // Redoing on editor 1 will emit an `Edited` event only for that editor.
-// editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor1", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // Undoing on editor 2 will emit an `Edited` event only for that editor.
-// editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor2", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // Redoing on editor 2 will emit an `Edited` event only for that editor.
-// editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
-// assert_eq!(
-// mem::take(&mut *events.borrow_mut()),
-// [
-// ("editor2", Event::Edited),
-// ("editor1", Event::BufferEdited),
-// ("editor2", Event::BufferEdited),
-// ]
-// );
-
-// // No event is emitted when the mutation is a no-op.
-// editor2.update(cx, |editor, cx| {
-// editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
-
-// editor.backspace(&Backspace, cx);
-// });
-// assert_eq!(mem::take(&mut *events.borrow_mut()), []);
-// }
+#[gpui::test]
+fn test_edit_events(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.build_model(|cx| {
+ let mut buffer = language::Buffer::new(0, cx.entity_id().as_u64(), "123456");
+ buffer.set_group_interval(Duration::from_secs(1));
+ buffer
+ });
+
+ let events = Rc::new(RefCell::new(Vec::new()));
+ let editor1 = cx.add_window({
+ let events = events.clone();
+ |cx| {
+ let view = cx.view().clone();
+ cx.subscribe(&view, move |_, _, event: &EditorEvent, _| {
+ if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
+ events.borrow_mut().push(("editor1", event.clone()));
+ }
+ })
+ .detach();
+ Editor::for_buffer(buffer.clone(), None, cx)
+ }
+ });
+
+ let editor2 = cx.add_window({
+ let events = events.clone();
+ |cx| {
+ cx.subscribe(&cx.view().clone(), move |_, _, event: &EditorEvent, _| {
+ if matches!(event, EditorEvent::Edited | EditorEvent::BufferEdited) {
+ events.borrow_mut().push(("editor2", event.clone()));
+ }
+ })
+ .detach();
+ Editor::for_buffer(buffer.clone(), None, cx)
+ }
+ });
+
+ assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+
+ // Mutating editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.insert("X", cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // Mutating editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // Undoing on editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // Redoing on editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // Undoing on editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // Redoing on editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", EditorEvent::Edited),
+ ("editor1", EditorEvent::BufferEdited),
+ ("editor2", EditorEvent::BufferEdited),
+ ]
+ );
+
+ // No event is emitted when the mutation is a no-op.
+ editor2.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
+
+ editor.backspace(&Backspace, cx);
+ });
+ assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+}
#[gpui::test]
fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
@@ -515,123 +514,123 @@ fn test_clone(cx: &mut TestAppContext) {
}
//todo!(editor navigate)
-// #[gpui::test]
-// async fn test_navigation_history(cx: &mut TestAppContext) {
-// init_test(cx, |_| {});
-
-// use workspace::item::Item;
-
-// let fs = FakeFs::new(cx.executor());
-// let project = Project::test(fs, [], cx).await;
-// let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
-// let pane = workspace
-// .update(cx, |workspace, _| workspace.active_pane().clone())
-// .unwrap();
-
-// workspace.update(cx, |v, cx| {
-// cx.build_view(|cx| {
-// let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
-// let mut editor = build_editor(buffer.clone(), cx);
-// let handle = cx.view();
-// editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
-
-// fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
-// editor.nav_history.as_mut().unwrap().pop_backward(cx)
-// }
-
-// // Move the cursor a small distance.
-// // Nothing is added to the navigation history.
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
-// });
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
-// });
-// assert!(pop_history(&mut editor, cx).is_none());
-
-// // Move the cursor a large distance.
-// // The history can jump back to the previous position.
-// editor.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
-// });
-// let nav_entry = pop_history(&mut editor, cx).unwrap();
-// editor.navigate(nav_entry.data.unwrap(), cx);
-// assert_eq!(nav_entry.item.id(), cx.entity_id());
-// assert_eq!(
-// editor.selections.display_ranges(cx),
-// &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
-// );
-// assert!(pop_history(&mut editor, cx).is_none());
-
-// // Move the cursor a small distance via the mouse.
-// // Nothing is added to the navigation history.
-// editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
-// editor.end_selection(cx);
-// assert_eq!(
-// editor.selections.display_ranges(cx),
-// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
-// );
-// assert!(pop_history(&mut editor, cx).is_none());
-
-// // Move the cursor a large distance via the mouse.
-// // The history can jump back to the previous position.
-// editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
-// editor.end_selection(cx);
-// assert_eq!(
-// editor.selections.display_ranges(cx),
-// &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
-// );
-// let nav_entry = pop_history(&mut editor, cx).unwrap();
-// editor.navigate(nav_entry.data.unwrap(), cx);
-// assert_eq!(nav_entry.item.id(), cx.entity_id());
-// assert_eq!(
-// editor.selections.display_ranges(cx),
-// &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
-// );
-// assert!(pop_history(&mut editor, cx).is_none());
-
-// // Set scroll position to check later
-// editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
-// let original_scroll_position = editor.scroll_manager.anchor();
-
-// // Jump to the end of the document and adjust scroll
-// editor.move_to_end(&MoveToEnd, cx);
-// editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
-// assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
-
-// let nav_entry = pop_history(&mut editor, cx).unwrap();
-// editor.navigate(nav_entry.data.unwrap(), cx);
-// assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
-
-// // Ensure we don't panic when navigation data contains invalid anchors *and* points.
-// let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
-// invalid_anchor.text_anchor.buffer_id = Some(999);
-// let invalid_point = Point::new(9999, 0);
-// editor.navigate(
-// Box::new(NavigationData {
-// cursor_anchor: invalid_anchor,
-// cursor_position: invalid_point,
-// scroll_anchor: ScrollAnchor {
-// anchor: invalid_anchor,
-// offset: Default::default(),
-// },
-// scroll_top_row: invalid_point.row,
-// }),
-// cx,
-// );
-// assert_eq!(
-// editor.selections.display_ranges(cx),
-// &[editor.max_point(cx)..editor.max_point(cx)]
-// );
-// assert_eq!(
-// editor.scroll_position(cx),
-// gpui::Point::new(0., editor.max_point(cx).row() as f32)
-// );
-
-// editor
-// })
-// });
-// }
+#[gpui::test]
+async fn test_navigation_history(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ use workspace::item::Item;
+
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let workspace = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let pane = workspace
+ .update(cx, |workspace, _| workspace.active_pane().clone())
+ .unwrap();
+
+ workspace.update(cx, |v, cx| {
+ cx.build_view(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
+ let mut editor = build_editor(buffer.clone(), cx);
+ let handle = cx.view();
+ editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+
+ fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
+ editor.nav_history.as_mut().unwrap().pop_backward(cx)
+ }
+
+ // Move the cursor a small distance.
+ // Nothing is added to the navigation history.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+ });
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance.
+ // The history can jump back to the previous position.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+ });
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.entity_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a small distance via the mouse.
+ // Nothing is added to the navigation history.
+ editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance via the mouse.
+ // The history can jump back to the previous position.
+ editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+ );
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.entity_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Set scroll position to check later
+ editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), cx);
+ let original_scroll_position = editor.scroll_manager.anchor();
+
+ // Jump to the end of the document and adjust scroll
+ editor.move_to_end(&MoveToEnd, cx);
+ editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), cx);
+ assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ // Ensure we don't panic when navigation data contains invalid anchors *and* points.
+ let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+ invalid_anchor.text_anchor.buffer_id = Some(999);
+ let invalid_point = Point::new(9999, 0);
+ editor.navigate(
+ Box::new(NavigationData {
+ cursor_anchor: invalid_anchor,
+ cursor_position: invalid_point,
+ scroll_anchor: ScrollAnchor {
+ anchor: invalid_anchor,
+ offset: Default::default(),
+ },
+ scroll_top_row: invalid_point.row,
+ }),
+ cx,
+ );
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[editor.max_point(cx)..editor.max_point(cx)]
+ );
+ assert_eq!(
+ editor.scroll_position(cx),
+ gpui::Point::new(0., editor.max_point(cx).row() as f32)
+ );
+
+ editor
+ })
+ });
+}
#[gpui::test]
fn test_cancel(cx: &mut TestAppContext) {
@@ -959,55 +958,55 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
}
//todo!(finish editor tests)
-// #[gpui::test]
-// fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
-// init_test(cx, |_| {});
-
-// let view = cx.add_window(|cx| {
-// let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
-// build_editor(buffer.clone(), cx)
-// });
-// view.update(cx, |view, cx| {
-// view.change_selections(None, cx, |s| {
-// s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
-// });
-// view.move_down(&MoveDown, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(1, "abcd".len())]
-// );
-
-// view.move_down(&MoveDown, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(2, "αβγ".len())]
-// );
-
-// view.move_down(&MoveDown, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(3, "abcd".len())]
-// );
-
-// view.move_down(&MoveDown, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
-// );
-
-// view.move_up(&MoveUp, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(3, "abcd".len())]
-// );
-
-// view.move_up(&MoveUp, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[empty_range(2, "αβγ".len())]
-// );
-// });
-// }
+#[gpui::test]
+fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
+ build_editor(buffer.clone(), cx)
+ });
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
+ });
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abcd".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "αβγ".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(3, "abcd".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(3, "abcd".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "αβγ".len())]
+ );
+ });
+}
#[gpui::test]
fn test_beginning_end_of_line(cx: &mut TestAppContext) {
@@ -1225,532 +1224,551 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
}
//todo!(finish editor tests)
-// #[gpui::test]
-// fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
-// init_test(cx, |_| {});
-
-// let view = cx.add_window(|cx| {
-// let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
-// build_editor(buffer, cx)
-// });
-
-// view.update(cx, |view, cx| {
-// view.set_wrap_width(Some(140.0.into()), cx);
-// assert_eq!(
-// view.display_text(cx),
-// "use one::{\n two::three::\n four::five\n};"
-// );
-
-// view.change_selections(None, cx, |s| {
-// s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
-// });
-
-// view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
-// );
-
-// view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-// );
-
-// view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-// );
-
-// view.move_to_next_word_end(&MoveToNextWordEnd, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
-// );
-
-// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
-// );
-
-// view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
-// assert_eq!(
-// view.selections.display_ranges(cx),
-// &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
-// );
-// });
-// }
-
-//todo!(simulate_resize)
-// #[gpui::test]
-// async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-// let mut cx = EditorTestContext::new(cx).await;
-
-// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-// let window = cx.window;
-// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
-
-// cx.set_state(
-// &r#"ˇone
-// two
-
-// three
-// fourˇ
-// five
-
-// six"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"one
-// two
-// ˇ
-// three
-// four
-// five
-// ˇ
-// six"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"one
-// two
-
-// three
-// four
-// five
-// ˇ
-// sixˇ"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"one
-// two
-
-// three
-// four
-// five
-
-// sixˇ"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"one
-// two
-
-// three
-// four
-// five
-// ˇ
-// six"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"one
-// two
-// ˇ
-// three
-// four
-// five
-
-// six"#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
-// cx.assert_editor_state(
-// &r#"ˇone
-// two
-
-// three
-// four
-// five
-
-// six"#
-// .unindent(),
-// );
-// }
-
-// #[gpui::test]
-// async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-// let mut cx = EditorTestContext::new(cx).await;
-// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-// let window = cx.window;
-// window.simulate_resize(Point::new(1000., 4. * line_height + 0.5), &mut cx);
-
-// cx.set_state(
-// &r#"ˇone
-// two
-// three
-// four
-// five
-// six
-// seven
-// eight
-// nine
-// ten
-// "#,
-// );
-
-// cx.update_editor(|editor, cx| {
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 0.)
-// );
-// editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 3.)
-// );
-// editor.scroll_screen(&ScrollAmount::Page(1.), cx);
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 6.)
-// );
-// editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 3.)
-// );
-
-// editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 1.)
-// );
-// editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 3.)
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-// let mut cx = EditorTestContext::new(cx).await;
-
-// let line_height = cx.update_editor(|editor, cx| {
-// editor.set_vertical_scroll_margin(2, cx);
-// editor.style(cx).text.line_height(cx.font_cache())
-// });
-
-// let window = cx.window;
-// window.simulate_resize(gpui::Point::new(1000., 6.0 * line_height), &mut cx);
-
-// cx.set_state(
-// &r#"ˇone
-// two
-// three
-// four
-// five
-// six
-// seven
-// eight
-// nine
-// ten
-// "#,
-// );
-// cx.update_editor(|editor, cx| {
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 0.0)
-// );
-// });
-
-// // Add a cursor below the visible area. Since both cursors cannot fit
-// // on screen, the editor autoscrolls to reveal the newest cursor, and
-// // allows the vertical scroll margin below that cursor.
-// cx.update_editor(|editor, cx| {
-// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-// selections.select_ranges([
-// Point::new(0, 0)..Point::new(0, 0),
-// Point::new(6, 0)..Point::new(6, 0),
-// ]);
-// })
-// });
-// cx.update_editor(|editor, cx| {
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 3.0)
-// );
-// });
-
-// // Move down. The editor cursor scrolls down to track the newest cursor.
-// cx.update_editor(|editor, cx| {
-// editor.move_down(&Default::default(), cx);
-// });
-// cx.update_editor(|editor, cx| {
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 4.0)
-// );
-// });
-
-// // Add a cursor above the visible area. Since both cursors fit on screen,
-// // the editor scrolls to show both.
-// cx.update_editor(|editor, cx| {
-// editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
-// selections.select_ranges([
-// Point::new(1, 0)..Point::new(1, 0),
-// Point::new(6, 0)..Point::new(6, 0),
-// ]);
-// })
-// });
-// cx.update_editor(|editor, cx| {
-// assert_eq!(
-// editor.snapshot(cx).scroll_position(),
-// gpui::Point::new(0., 1.0)
-// );
-// });
-// }
-
-// #[gpui::test]
-// async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-// let mut cx = EditorTestContext::new(cx).await;
-
-// let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
-// let window = cx.window;
-// window.simulate_resize(gpui::Point::new(100., 4. * line_height), &mut cx);
-
-// cx.set_state(
-// &r#"
-// ˇone
-// two
-// threeˇ
-// four
-// five
-// six
-// seven
-// eight
-// nine
-// ten
-// "#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-// cx.assert_editor_state(
-// &r#"
-// one
-// two
-// three
-// ˇfour
-// five
-// sixˇ
-// seven
-// eight
-// nine
-// ten
-// "#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
-// cx.assert_editor_state(
-// &r#"
-// one
-// two
-// three
-// four
-// five
-// six
-// ˇseven
-// eight
-// nineˇ
-// ten
-// "#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-// cx.assert_editor_state(
-// &r#"
-// one
-// two
-// three
-// ˇfour
-// five
-// sixˇ
-// seven
-// eight
-// nine
-// ten
-// "#
-// .unindent(),
-// );
-
-// cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
-// cx.assert_editor_state(
-// &r#"
-// ˇone
-// two
-// threeˇ
-// four
-// five
-// six
-// seven
-// eight
-// nine
-// ten
-// "#
-// .unindent(),
-// );
-
-// // Test select collapsing
-// cx.update_editor(|editor, cx| {
-// editor.move_page_down(&MovePageDown::default(), cx);
-// editor.move_page_down(&MovePageDown::default(), cx);
-// editor.move_page_down(&MovePageDown::default(), cx);
-// });
-// cx.assert_editor_state(
-// &r#"
-// one
-// two
-// three
-// four
-// five
-// six
-// seven
-// eight
-// nine
-// ˇten
-// ˇ"#
-// .unindent(),
-// );
-// }
-
-#[gpui::test]
-async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
- init_test(cx, |_| {});
- let mut cx = EditorTestContext::new(cx).await;
- cx.set_state("one «two threeˇ» four");
- cx.update_editor(|editor, cx| {
- editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
- assert_eq!(editor.text(cx), " four");
- });
-}
-
#[gpui::test]
-fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let view = cx.add_window(|cx| {
- let buffer = MultiBuffer::build_simple("one two three four", cx);
- build_editor(buffer.clone(), cx)
+ let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
+ build_editor(buffer, cx)
});
view.update(cx, |view, cx| {
- view.change_selections(None, cx, |s| {
- s.select_display_ranges([
- // an empty selection - the preceding word fragment is deleted
- DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
- // characters selected - they are deleted
- DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
- ])
- });
- view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
- assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
- });
+ view.set_wrap_width(Some(140.0.into()), cx);
+ assert_eq!(
+ view.display_text(cx),
+ "use one::{\n two::three::\n four::five\n};"
+ );
- view.update(cx, |view, cx| {
view.change_selections(None, cx, |s| {
- s.select_display_ranges([
- // an empty selection - the following word fragment is deleted
- DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
- // characters selected - they are deleted
- DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
- ])
+ s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
});
- view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
- assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
- });
-}
-#[gpui::test]
-fn test_newline(cx: &mut TestAppContext) {
- init_test(cx, |_| {});
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
+ );
- let view = cx.add_window(|cx| {
- let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
- build_editor(buffer.clone(), cx)
- });
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+ );
- view.update(cx, |view, cx| {
- view.change_selections(None, cx, |s| {
- s.select_display_ranges([
- DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
- DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
- DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
- ])
- });
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+ );
- view.newline(&Newline, cx);
- assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
+ );
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+ );
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+ );
});
}
+//todo!(simulate_resize)
#[gpui::test]
-fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
- let editor = cx.add_window(|cx| {
- let buffer = MultiBuffer::build_simple(
- "
- a
- b(
- X
- )
- c(
- X
- )
- "
- .unindent()
- .as_str(),
- cx,
- );
- let mut editor = build_editor(buffer.clone(), cx);
- editor.change_selections(None, cx, |s| {
- s.select_ranges([
- Point::new(2, 4)..Point::new(2, 5),
- Point::new(5, 4)..Point::new(5, 5),
- ])
- });
+ let line_height = cx.editor(|editor, cx| {
editor
+ .style()
+ .unwrap()
+ .text
+ .line_height_in_pixels(cx.rem_size())
});
+ cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
- editor.update(cx, |editor, cx| {
- // Edit the buffer directly, deleting ranges surrounding the editor's selections
- editor.buffer.update(cx, |buffer, cx| {
- buffer.edit(
- [
- (Point::new(1, 2)..Point::new(3, 0), ""),
- (Point::new(4, 2)..Point::new(6, 0), ""),
- ],
- None,
- cx,
- );
- assert_eq!(
- buffer.read(cx).text(),
- "
- a
- b()
+ cx.set_state(
+ &r#"ˇone
+ two
+
+ three
+ fourˇ
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ˇ
+ three
+ four
+ five
+ ˇ
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ˇ
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+
+ sixˇ"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ˇ
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ˇ
+ three
+ four
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"ˇone
+ two
+
+ three
+ four
+ five
+
+ six"#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+ let line_height = cx.editor(|editor, cx| {
+ editor
+ .style()
+ .unwrap()
+ .text
+ .line_height_in_pixels(cx.rem_size())
+ });
+ let window = cx.window;
+ cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
+
+ cx.set_state(
+ &r#"ˇone
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#,
+ );
+
+ cx.update_editor(|editor, cx| {
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 0.)
+ );
+ editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 3.)
+ );
+ editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 6.)
+ );
+ editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 3.)
+ );
+
+ editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 1.)
+ );
+ editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 3.)
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.update_editor(|editor, cx| {
+ editor.set_vertical_scroll_margin(2, cx);
+ editor
+ .style()
+ .unwrap()
+ .text
+ .line_height_in_pixels(cx.rem_size())
+ });
+ let window = cx.window;
+ cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
+
+ cx.set_state(
+ &r#"ˇone
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#,
+ );
+ cx.update_editor(|editor, cx| {
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 0.0)
+ );
+ });
+
+ // Add a cursor below the visible area. Since both cursors cannot fit
+ // on screen, the editor autoscrolls to reveal the newest cursor, and
+ // allows the vertical scroll margin below that cursor.
+ cx.update_editor(|editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+ selections.select_ranges([
+ Point::new(0, 0)..Point::new(0, 0),
+ Point::new(6, 0)..Point::new(6, 0),
+ ]);
+ })
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 3.0)
+ );
+ });
+
+ // Move down. The editor cursor scrolls down to track the newest cursor.
+ cx.update_editor(|editor, cx| {
+ editor.move_down(&Default::default(), cx);
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 4.0)
+ );
+ });
+
+ // Add a cursor above the visible area. Since both cursors fit on screen,
+ // the editor scrolls to show both.
+ cx.update_editor(|editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+ selections.select_ranges([
+ Point::new(1, 0)..Point::new(1, 0),
+ Point::new(6, 0)..Point::new(6, 0),
+ ]);
+ })
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(
+ editor.snapshot(cx).scroll_position(),
+ gpui::Point::new(0., 1.0)
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.editor(|editor, cx| {
+ editor
+ .style()
+ .unwrap()
+ .text
+ .line_height_in_pixels(cx.rem_size())
+ });
+ let window = cx.window;
+ cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
+ cx.set_state(
+ &r#"
+ ˇone
+ two
+ threeˇ
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ ˇfour
+ five
+ sixˇ
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ four
+ five
+ six
+ ˇseven
+ eight
+ nineˇ
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ ˇfour
+ five
+ sixˇ
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ ˇone
+ two
+ threeˇ
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ // Test select collapsing
+ cx.update_editor(|editor, cx| {
+ editor.move_page_down(&MovePageDown::default(), cx);
+ editor.move_page_down(&MovePageDown::default(), cx);
+ editor.move_page_down(&MovePageDown::default(), cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ˇten
+ ˇ"#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state("one «two threeˇ» four");
+ cx.update_editor(|editor, cx| {
+ editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+ assert_eq!(editor.text(cx), " four");
+ });
+}
+
+#[gpui::test]
+fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("one two three four", cx);
+ build_editor(buffer.clone(), cx)
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ // an empty selection - the preceding word fragment is deleted
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ // characters selected - they are deleted
+ DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
+ ])
+ });
+ view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
+ assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ // an empty selection - the following word fragment is deleted
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ // characters selected - they are deleted
+ DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
+ ])
+ });
+ view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
+ assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
+ });
+}
+
+#[gpui::test]
+fn test_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
+ build_editor(buffer.clone(), cx)
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+ DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
+ ])
+ });
+
+ view.newline(&Newline, cx);
+ assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
+ });
+}
+
+#[gpui::test]
+fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(
+ "
+ a
+ b(
+ X
+ )
+ c(
+ X
+ )
+ "
+ .unindent()
+ .as_str(),
+ cx,
+ );
+ let mut editor = build_editor(buffer.clone(), cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(2, 4)..Point::new(2, 5),
+ Point::new(5, 4)..Point::new(5, 5),
+ ])
+ });
+ editor
+ });
+
+ editor.update(cx, |editor, cx| {
+ // Edit the buffer directly, deleting ranges surrounding the editor's selections
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [
+ (Point::new(1, 2)..Point::new(3, 0), ""),
+ (Point::new(4, 2)..Point::new(6, 0), ""),
+ ],
+ None,
+ cx,
+ );
+ assert_eq!(
+ buffer.read(cx).text(),
+ "
+ a
+ b()
c()
"
.unindent()
@@ -9,9 +9,11 @@ use crate::{
self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
},
link_go_to_definition::{
- go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
- update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+ go_to_fetched_definition, go_to_fetched_type_definition, show_link_definition,
+ update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+ LinkGoToDefinitionState,
},
+ mouse_context_menu,
scroll::scroll_amount::ScrollAmount,
CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point,
@@ -19,14 +21,15 @@ use crate::{
};
use anyhow::Result;
use collections::{BTreeMap, HashMap};
+use git::diff::DiffHunkStatus;
use gpui::{
- div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
- BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
- ElementInputHandler, Entity, EntityId, Hsla, InteractiveBounds, InteractiveElement,
- IntoElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
- ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size,
- StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View,
- ViewContext, WeakView, WindowContext, WrappedLine,
+ div, overlay, point, px, relative, size, transparent_black, Action, AnchorCorner, AnyElement,
+ AsyncWindowContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle,
+ DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, EntityId, Hsla,
+ InteractiveBounds, InteractiveElement, IntoElement, LineLayout, ModifiersChangedEvent,
+ MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement,
+ Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -48,8 +51,10 @@ use std::{
};
use sum_tree::Bias;
use theme::{ActiveTheme, PlayerColor};
-use ui::prelude::*;
-use ui::{h_stack, IconButton, Tooltip};
+use ui::{
+ h_stack, ButtonLike, ButtonStyle, Disclosure, IconButton, IconElement, IconSize, Label, Tooltip,
+};
+use ui::{prelude::*, Icon};
use util::ResultExt;
use workspace::item::Item;
@@ -139,8 +144,6 @@ impl EditorElement {
register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down);
register_action(view, cx, Editor::move_up);
- // on_action(cx, Editor::new_file); todo!()
- // on_action(cx, Editor::new_file_in_direction); todo!()
register_action(view, cx, Editor::cancel);
register_action(view, cx, Editor::newline);
register_action(view, cx, Editor::newline_above);
@@ -263,7 +266,7 @@ impl EditorElement {
register_action(view, cx, Editor::fold_selected_ranges);
register_action(view, cx, Editor::show_completions);
register_action(view, cx, Editor::toggle_code_actions);
- // on_action(cx, Editor::open_excerpts); todo!()
+ register_action(view, cx, Editor::open_excerpts);
register_action(view, cx, Editor::toggle_soft_wrap);
register_action(view, cx, Editor::toggle_inlay_hints);
register_action(view, cx, hover_popover::hover);
@@ -312,7 +315,57 @@ impl EditorElement {
register_action(view, cx, Editor::context_menu_last);
}
- fn mouse_down(
+ fn register_key_listeners(&self, cx: &mut WindowContext) {
+ cx.on_key_event({
+ let editor = self.editor.clone();
+ move |event: &ModifiersChangedEvent, phase, cx| {
+ if phase != DispatchPhase::Bubble {
+ return;
+ }
+
+ if editor.update(cx, |editor, cx| Self::modifiers_changed(editor, event, cx)) {
+ cx.stop_propagation();
+ }
+ }
+ });
+ }
+
+ pub(crate) fn modifiers_changed(
+ editor: &mut Editor,
+ event: &ModifiersChangedEvent,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ let pending_selection = editor.has_pending_selection();
+
+ if let Some(point) = &editor.link_go_to_definition_state.last_trigger_point {
+ if event.command && !pending_selection {
+ let point = point.clone();
+ let snapshot = editor.snapshot(cx);
+ let kind = point.definition_kind(event.shift);
+
+ show_link_definition(kind, editor, point, snapshot, cx);
+ return false;
+ }
+ }
+
+ {
+ if editor.link_go_to_definition_state.symbol_range.is_some()
+ || !editor.link_go_to_definition_state.definitions.is_empty()
+ {
+ editor.link_go_to_definition_state.symbol_range.take();
+ editor.link_go_to_definition_state.definitions.clear();
+ cx.notify();
+ }
+
+ editor.link_go_to_definition_state.task = None;
+
+ editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+ }
+
+ false
+ }
+
+ fn mouse_left_down(
editor: &mut Editor,
event: &MouseDownEvent,
position_map: &PositionMap,
@@ -365,25 +418,25 @@ impl EditorElement {
true
}
- // fn mouse_right_down(
- // editor: &mut Editor,
- // position: gpui::Point<Pixels>,
- // position_map: &PositionMap,
- // text_bounds: Bounds<Pixels>,
- // cx: &mut EventContext<Editor>,
- // ) -> bool {
- // if !text_bounds.contains_point(position) {
- // return false;
- // }
- // let point_for_position = position_map.point_for_position(text_bounds, position);
- // mouse_context_menu::deploy_context_menu(
- // editor,
- // position,
- // point_for_position.previous_valid,
- // cx,
- // );
- // true
- // }
+ fn mouse_right_down(
+ editor: &mut Editor,
+ event: &MouseDownEvent,
+ position_map: &PositionMap,
+ text_bounds: Bounds<Pixels>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if !text_bounds.contains_point(&event.position) {
+ return false;
+ }
+ let point_for_position = position_map.point_for_position(text_bounds, event.position);
+ mouse_context_menu::deploy_context_menu(
+ editor,
+ event.position,
+ point_for_position.previous_valid,
+ cx,
+ );
+ true
+ }
fn mouse_up(
editor: &mut Editor,
@@ -725,87 +778,85 @@ impl EditorElement {
}
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
- // todo!()
- // let diff_style = &theme::current(cx).editor.diff.clone();
- // let line_height = layout.position_map.line_height;
-
- // let scroll_position = layout.position_map.snapshot.scroll_position();
- // let scroll_top = scroll_position.y * line_height;
-
- // for hunk in &layout.display_hunks {
- // let (display_row_range, status) = match hunk {
- // //TODO: This rendering is entirely a horrible hack
- // &DisplayDiffHunk::Folded { display_row: row } => {
- // let start_y = row as f32 * line_height - scroll_top;
- // let end_y = start_y + line_height;
-
- // let width = diff_style.removed_width_em * line_height;
- // let highlight_origin = bounds.origin + point(-width, start_y);
- // let highlight_size = point(width * 2., end_y - start_y);
- // let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
- // cx.paint_quad(Quad {
- // bounds: highlight_bounds,
- // background: Some(diff_style.modified),
- // border: Border::new(0., Color::transparent_black()).into(),
- // corner_radii: (1. * line_height).into(),
- // });
-
- // continue;
- // }
-
- // DisplayDiffHunk::Unfolded {
- // display_row_range,
- // status,
- // } => (display_row_range, status),
- // };
-
- // let color = match status {
- // DiffHunkStatus::Added => diff_style.inserted,
- // DiffHunkStatus::Modified => diff_style.modified,
-
- // //TODO: This rendering is entirely a horrible hack
- // DiffHunkStatus::Removed => {
- // let row = display_row_range.start;
-
- // let offset = line_height / 2.;
- // let start_y = row as f32 * line_height - offset - scroll_top;
- // let end_y = start_y + line_height;
-
- // let width = diff_style.removed_width_em * line_height;
- // let highlight_origin = bounds.origin + point(-width, start_y);
- // let highlight_size = point(width * 2., end_y - start_y);
- // let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
- // cx.paint_quad(Quad {
- // bounds: highlight_bounds,
- // background: Some(diff_style.deleted),
- // border: Border::new(0., Color::transparent_black()).into(),
- // corner_radii: (1. * line_height).into(),
- // });
-
- // continue;
- // }
- // };
-
- // let start_row = display_row_range.start;
- // let end_row = display_row_range.end;
-
- // let start_y = start_row as f32 * line_height - scroll_top;
- // let end_y = end_row as f32 * line_height - scroll_top;
-
- // let width = diff_style.width_em * line_height;
- // let highlight_origin = bounds.origin + point(-width, start_y);
- // let highlight_size = point(width * 2., end_y - start_y);
- // let highlight_bounds = Bounds::<Pixels>::new(highlight_origin, highlight_size);
-
- // cx.paint_quad(Quad {
- // bounds: highlight_bounds,
- // background: Some(color),
- // border: Border::new(0., Color::transparent_black()).into(),
- // corner_radii: (diff_style.corner_radius * line_height).into(),
- // });
- // }
+ let line_height = layout.position_map.line_height;
+
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let scroll_top = scroll_position.y * line_height;
+
+ for hunk in &layout.display_hunks {
+ let (display_row_range, status) = match hunk {
+ //TODO: This rendering is entirely a horrible hack
+ &DisplayDiffHunk::Folded { display_row: row } => {
+ let start_y = row as f32 * line_height - scroll_top;
+ let end_y = start_y + line_height;
+
+ let width = 0.275 * line_height;
+ let highlight_origin = bounds.origin + point(-width, start_y);
+ let highlight_size = size(width * 2., end_y - start_y);
+ let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+ cx.paint_quad(
+ highlight_bounds,
+ Corners::all(1. * line_height),
+ gpui::yellow(), // todo!("use the right color")
+ Edges::default(),
+ transparent_black(),
+ );
+
+ continue;
+ }
+
+ DisplayDiffHunk::Unfolded {
+ display_row_range,
+ status,
+ } => (display_row_range, status),
+ };
+
+ let color = match status {
+ DiffHunkStatus::Added => gpui::green(), // todo!("use the appropriate color")
+ DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the appropriate color")
+
+ //TODO: This rendering is entirely a horrible hack
+ DiffHunkStatus::Removed => {
+ let row = display_row_range.start;
+
+ let offset = line_height / 2.;
+ let start_y = row as f32 * line_height - offset - scroll_top;
+ let end_y = start_y + line_height;
+
+ let width = 0.275 * line_height;
+ let highlight_origin = bounds.origin + point(-width, start_y);
+ let highlight_size = size(width * 2., end_y - start_y);
+ let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+ cx.paint_quad(
+ highlight_bounds,
+ Corners::all(1. * line_height),
+ gpui::red(), // todo!("use the right color")
+ Edges::default(),
+ transparent_black(),
+ );
+
+ continue;
+ }
+ };
+
+ let start_row = display_row_range.start;
+ let end_row = display_row_range.end;
+
+ let start_y = start_row as f32 * line_height - scroll_top;
+ let end_y = end_row as f32 * line_height - scroll_top;
+
+ let width = 0.275 * line_height;
+ let highlight_origin = bounds.origin + point(-width, start_y);
+ let highlight_size = size(width * 2., end_y - start_y);
+ let highlight_bounds = Bounds::new(highlight_origin, highlight_size);
+ cx.paint_quad(
+ highlight_bounds,
+ Corners::all(0.05 * line_height),
+ color, // todo!("use the right color")
+ Edges::default(),
+ transparent_black(),
+ );
+ }
}
fn paint_text(
@@ -831,15 +882,19 @@ impl EditorElement {
bounds: text_bounds,
}),
|cx| {
- // todo!("cursor region")
- // cx.scene().push_cursor_region(CursorRegion {
- // bounds,
- // style: if !editor.link_go_to_definition_state.definitions.is_empty {
- // CursorStyle::PointingHand
- // } else {
- // CursorStyle::IBeam
- // },
- // });
+ if text_bounds.contains_point(&cx.mouse_position()) {
+ if self
+ .editor
+ .read(cx)
+ .link_go_to_definition_state
+ .definitions
+ .is_empty()
+ {
+ cx.set_cursor_style(CursorStyle::IBeam);
+ } else {
+ cx.set_cursor_style(CursorStyle::PointingHand);
+ }
+ }
let fold_corner_radius = 0.15 * layout.position_map.line_height;
cx.with_element_id(Some("folds"), |cx| {
@@ -1138,6 +1193,22 @@ impl EditorElement {
}
}
}
+
+ if let Some(mouse_context_menu) =
+ self.editor.read(cx).mouse_context_menu.as_ref()
+ {
+ let element = overlay()
+ .position(mouse_context_menu.position)
+ .child(mouse_context_menu.context_menu.clone())
+ .anchor(AnchorCorner::TopLeft)
+ .snap_to_window();
+ element.draw(
+ gpui::Point::default(),
+ size(AvailableSpace::MinContent, AvailableSpace::MinContent),
+ cx,
+ |_, _| {},
+ );
+ }
})
},
)
@@ -1662,11 +1733,6 @@ impl EditorElement {
cx: &mut WindowContext,
) -> LayoutState {
self.editor.update(cx, |editor, cx| {
- // let mut size = constraint.max;
- // if size.x.is_infinite() {
- // unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
- // }
-
let snapshot = editor.snapshot(cx);
let style = self.style.clone();
@@ -1689,7 +1755,7 @@ impl EditorElement {
let gutter_width;
let gutter_margin;
if snapshot.show_gutter {
- let descent = cx.text_system().descent(font_id, font_size).unwrap();
+ let descent = cx.text_system().descent(font_id, font_size);
let gutter_padding_factor = 3.5;
gutter_padding = (em_width * gutter_padding_factor).round();
@@ -1702,6 +1768,7 @@ impl EditorElement {
};
editor.gutter_width = gutter_width;
+
let text_width = bounds.size.width - gutter_width;
let overscroll = size(em_width, px(0.));
let snapshot = {
@@ -1728,25 +1795,6 @@ impl EditorElement {
.collect::<SmallVec<[_; 2]>>();
let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
- // todo!("this should happen during layout")
- let editor_mode = snapshot.mode;
- if let EditorMode::AutoHeight { max_lines } = editor_mode {
- todo!()
- // size.set_y(
- // scroll_height
- // .min(constraint.max_along(Axis::Vertical))
- // .max(constraint.min_along(Axis::Vertical))
- // .max(line_height)
- // .min(line_height * max_lines as f32),
- // )
- } else if let EditorMode::SingleLine = editor_mode {
- bounds.size.height = line_height.min(bounds.size.height);
- }
- // todo!()
- // else if size.y.is_infinite() {
- // // size.set_y(scroll_height);
- // }
- //
let gutter_size = size(gutter_width, bounds.size.height);
let text_size = size(text_width, bounds.size.height);
@@ -2064,7 +2112,7 @@ impl EditorElement {
.unwrap();
LayoutState {
- mode: editor_mode,
+ mode: snapshot.mode,
position_map: Arc::new(PositionMap {
size: bounds.size,
scroll_position: point(
@@ -2177,7 +2225,8 @@ impl EditorElement {
.as_ref()
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
.unwrap_or_default();
- let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+
+ let jump_handler = project::File::from_dyn(buffer.file()).map(|file| {
let jump_path = ProjectPath {
worktree_id: file.worktree_id(cx),
path: file.path.clone(),
@@ -2188,11 +2237,11 @@ impl EditorElement {
.map_or(range.context.start, |primary| primary.start);
let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
- IconButton::new(block_id, ui::Icon::ArrowUpRight)
- .on_click(cx.listener_for(&self.editor, move |editor, e, cx| {
- editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
- }))
- .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx))
+ let jump_handler = cx.listener_for(&self.editor, move |editor, e, cx| {
+ editor.jump(jump_path.clone(), jump_position, jump_anchor, cx);
+ });
+
+ jump_handler
});
let element = if *starts_new_buffer {
@@ -2207,25 +2256,108 @@ impl EditorElement {
.map(|p| SharedString::from(p.to_string_lossy().to_string() + "/"));
}
- h_stack()
- .id("path header block")
- .size_full()
- .bg(gpui::red())
- .child(
- filename
- .map(SharedString::from)
- .unwrap_or_else(|| "untitled".into()),
- )
- .children(parent_path)
- .children(jump_icon) // .p_x(gutter_padding)
+ let is_open = true;
+
+ div().id("path header container").size_full().p_1p5().child(
+ h_stack()
+ .id("path header block")
+ .py_1p5()
+ .pl_3()
+ .pr_2()
+ .rounded_lg()
+ .shadow_md()
+ .border()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().editor_subheader_background)
+ .justify_between()
+ .cursor_pointer()
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .on_click(cx.listener(|_editor, _event, _cx| {
+ // TODO: Implement collapsing path headers
+ todo!("Clicking path header")
+ }))
+ .child(
+ h_stack()
+ .gap_3()
+ // TODO: Add open/close state and toggle action
+ .child(
+ div().border().border_color(gpui::red()).child(
+ ButtonLike::new("path-header-disclosure-control")
+ .style(ButtonStyle::Subtle)
+ .child(IconElement::new(match is_open {
+ true => Icon::ChevronDown,
+ false => Icon::ChevronRight,
+ })),
+ ),
+ )
+ .child(
+ h_stack()
+ .gap_2()
+ .child(Label::new(
+ filename
+ .map(SharedString::from)
+ .unwrap_or_else(|| "untitled".into()),
+ ))
+ .when_some(parent_path, |then, path| {
+ then.child(Label::new(path).color(Color::Muted))
+ }),
+ ),
+ )
+ .children(jump_handler.map(|jump_handler| {
+ IconButton::new(block_id, Icon::ArrowUpRight)
+ .style(ButtonStyle::Subtle)
+ .on_click(jump_handler)
+ .tooltip(|cx| {
+ Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)
+ })
+ })), // .p_x(gutter_padding)
+ )
} else {
let text_style = style.text.clone();
h_stack()
.id("collapsed context")
.size_full()
- .bg(gpui::red())
- .child("⋯")
- .children(jump_icon) // .p_x(gutter_padding)
+ .gap(gutter_padding)
+ .child(
+ h_stack()
+ .justify_end()
+ .flex_none()
+ .w(gutter_width - gutter_padding)
+ .h_full()
+ .text_buffer(cx)
+ .text_color(cx.theme().colors().editor_line_number)
+ .child("..."),
+ )
+ .map(|this| {
+ if let Some(jump_handler) = jump_handler {
+ this.child(
+ ButtonLike::new("jump to collapsed context")
+ .style(ButtonStyle::Transparent)
+ .full_width()
+ .on_click(jump_handler)
+ .tooltip(|cx| {
+ Tooltip::for_action(
+ "Jump to Buffer",
+ &OpenExcerpts,
+ cx,
+ )
+ })
+ .child(
+ div()
+ .h_px()
+ .w_full()
+ .bg(cx.theme().colors().border_variant)
+ .group_hover("", |style| {
+ style.bg(cx.theme().colors().border)
+ }),
+ ),
+ )
+ } else {
+ this.child(div().size_full().bg(gpui::green()))
+ }
+ })
+ // .child("⋯")
+ // .children(jump_icon) // .p_x(gutter_padding)
};
element.into_any()
}
@@ -2308,10 +2440,10 @@ impl EditorElement {
return;
}
- let should_cancel = editor.update(cx, |editor, cx| {
+ let handled = editor.update(cx, |editor, cx| {
Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
});
- if should_cancel {
+ if handled {
cx.stop_propagation();
}
}
@@ -2327,19 +2459,25 @@ impl EditorElement {
return;
}
- let should_cancel = editor.update(cx, |editor, cx| {
- Self::mouse_down(
- editor,
- event,
- &position_map,
- text_bounds,
- gutter_bounds,
- &stacking_order,
- cx,
- )
- });
+ let handled = match event.button {
+ MouseButton::Left => editor.update(cx, |editor, cx| {
+ Self::mouse_left_down(
+ editor,
+ event,
+ &position_map,
+ text_bounds,
+ gutter_bounds,
+ &stacking_order,
+ cx,
+ )
+ }),
+ MouseButton::Right => editor.update(cx, |editor, cx| {
+ Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
+ }),
+ _ => false,
+ };
- if should_cancel {
+ if handled {
cx.stop_propagation()
}
}
@@ -2351,7 +2489,7 @@ impl EditorElement {
let stacking_order = cx.stacking_order().clone();
move |event: &MouseUpEvent, phase, cx| {
- let should_cancel = editor.update(cx, |editor, cx| {
+ let handled = editor.update(cx, |editor, cx| {
Self::mouse_up(
editor,
event,
@@ -2362,26 +2500,11 @@ impl EditorElement {
)
});
- if should_cancel {
+ if handled {
cx.stop_propagation()
}
}
});
- //todo!()
- // on_down(MouseButton::Right, {
- // let position_map = layout.position_map.clone();
- // move |event, editor, cx| {
- // if !Self::mouse_right_down(
- // editor,
- // event.position,
- // position_map.as_ref(),
- // text_bounds,
- // cx,
- // ) {
- // cx.propagate_event();
- // }
- // }
- // });
cx.on_mouse_event({
let position_map = layout.position_map.clone();
let editor = self.editor.clone();
@@ -2617,19 +2740,44 @@ impl Element for EditorElement {
cx: &mut gpui::WindowContext,
) -> (gpui::LayoutId, Self::State) {
self.editor.update(cx, |editor, cx| {
- editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
+ editor.set_style(self.style.clone(), cx);
- let rem_size = cx.rem_size();
- let mut style = Style::default();
- style.size.width = relative(1.).into();
- style.size.height = match editor.mode {
+ let layout_id = match editor.mode {
EditorMode::SingleLine => {
- self.style.text.line_height_in_pixels(cx.rem_size()).into()
+ let rem_size = cx.rem_size();
+ let mut style = Style::default();
+ style.size.width = relative(1.).into();
+ style.size.height = self.style.text.line_height_in_pixels(rem_size).into();
+ cx.request_layout(&style, None)
+ }
+ EditorMode::AutoHeight { max_lines } => {
+ let editor_handle = cx.view().clone();
+ let max_line_number_width =
+ self.max_line_number_width(&editor.snapshot(cx), cx);
+ cx.request_measured_layout(
+ Style::default(),
+ move |known_dimensions, available_space, cx| {
+ editor_handle
+ .update(cx, |editor, cx| {
+ compute_auto_height_layout(
+ editor,
+ max_lines,
+ max_line_number_width,
+ known_dimensions,
+ cx,
+ )
+ })
+ .unwrap_or_default()
+ },
+ )
+ }
+ EditorMode::Full => {
+ let mut style = Style::default();
+ style.size.width = relative(1.).into();
+ style.size.height = relative(1.).into();
+ cx.request_layout(&style, None)
}
- EditorMode::AutoHeight { .. } => todo!(),
- EditorMode::Full => relative(1.).into(),
};
- let layout_id = cx.request_layout(&style, None);
(layout_id, ())
})
@@ -2657,6 +2805,7 @@ impl Element for EditorElement {
let dispatch_context = self.editor.read(cx).dispatch_context(cx);
cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| {
self.register_actions(cx);
+ self.register_key_listeners(cx);
// We call with_z_index to establish a new stacking context.
cx.with_z_index(0, |cx| {
@@ -2698,604 +2847,6 @@ impl IntoElement for EditorElement {
}
}
-// impl EditorElement {
-// type LayoutState = LayoutState;
-// type PaintState = ();
-
-// fn layout(
-// &mut self,
-// constraint: SizeConstraint,
-// editor: &mut Editor,
-// cx: &mut ViewContext<Editor>,
-// ) -> (gpui::Point<Pixels>, Self::LayoutState) {
-// let mut size = constraint.max;
-// if size.x.is_infinite() {
-// unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
-// }
-
-// let snapshot = editor.snapshot(cx);
-// let style = self.style.clone();
-
-// let line_height = (style.text.font_size * style.line_height_scalar).round();
-
-// let gutter_padding;
-// let gutter_width;
-// let gutter_margin;
-// if snapshot.show_gutter {
-// let em_width = style.text.em_width(cx.font_cache());
-// gutter_padding = (em_width * style.gutter_padding_factor).round();
-// gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
-// gutter_margin = -style.text.descent(cx.font_cache());
-// } else {
-// gutter_padding = 0.0;
-// gutter_width = 0.0;
-// gutter_margin = 0.0;
-// };
-
-// let text_width = size.x - gutter_width;
-// let em_width = style.text.em_width(cx.font_cache());
-// let em_advance = style.text.em_advance(cx.font_cache());
-// let overscroll = point(em_width, 0.);
-// let snapshot = {
-// editor.set_visible_line_count(size.y / line_height, cx);
-
-// let editor_width = text_width - gutter_margin - overscroll.x - em_width;
-// let wrap_width = match editor.soft_wrap_mode(cx) {
-// SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
-// SoftWrap::EditorWidth => editor_width,
-// SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
-// };
-
-// if editor.set_wrap_width(Some(wrap_width), cx) {
-// editor.snapshot(cx)
-// } else {
-// snapshot
-// }
-// };
-
-// let wrap_guides = editor
-// .wrap_guides(cx)
-// .iter()
-// .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
-// .collect();
-
-// let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
-// if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
-// size.set_y(
-// scroll_height
-// .min(constraint.max_along(Axis::Vertical))
-// .max(constraint.min_along(Axis::Vertical))
-// .max(line_height)
-// .min(line_height * max_lines as f32),
-// )
-// } else if let EditorMode::SingleLine = snapshot.mode {
-// size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
-// } else if size.y.is_infinite() {
-// size.set_y(scroll_height);
-// }
-// let gutter_size = point(gutter_width, size.y);
-// let text_size = point(text_width, size.y);
-
-// let autoscroll_horizontally = editor.autoscroll_vertically(size.y, line_height, cx);
-// let mut snapshot = editor.snapshot(cx);
-
-// let scroll_position = snapshot.scroll_position();
-// // The scroll position is a fractional point, the whole number of which represents
-// // the top of the window in terms of display rows.
-// let start_row = scroll_position.y as u32;
-// let height_in_lines = size.y / line_height;
-// let max_row = snapshot.max_point().row();
-
-// // Add 1 to ensure selections bleed off screen
-// let end_row = 1 + cmp::min(
-// (scroll_position.y + height_in_lines).ceil() as u32,
-// max_row,
-// );
-
-// let start_anchor = if start_row == 0 {
-// Anchor::min()
-// } else {
-// snapshot
-// .buffer_snapshot
-// .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
-// };
-// let end_anchor = if end_row > max_row {
-// Anchor::max
-// } else {
-// snapshot
-// .buffer_snapshot
-// .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
-// };
-
-// let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
-// let mut active_rows = BTreeMap::new();
-// let mut fold_ranges = Vec::new();
-// let is_singleton = editor.is_singleton(cx);
-
-// let highlighted_rows = editor.highlighted_rows();
-// let theme = theme::current(cx);
-// let highlighted_ranges = editor.background_highlights_in_range(
-// start_anchor..end_anchor,
-// &snapshot.display_snapshot,
-// theme.as_ref(),
-// );
-
-// fold_ranges.extend(
-// snapshot
-// .folds_in_range(start_anchor..end_anchor)
-// .map(|anchor| {
-// let start = anchor.start.to_point(&snapshot.buffer_snapshot);
-// (
-// start.row,
-// start.to_display_point(&snapshot.display_snapshot)
-// ..anchor.end.to_display_point(&snapshot),
-// )
-// }),
-// );
-
-// let mut newest_selection_head = None;
-
-// if editor.show_local_selections {
-// let mut local_selections: Vec<Selection<Point>> = editor
-// .selections
-// .disjoint_in_range(start_anchor..end_anchor, cx);
-// local_selections.extend(editor.selections.pending(cx));
-// let mut layouts = Vec::new();
-// let newest = editor.selections.newest(cx);
-// for selection in local_selections.drain(..) {
-// let is_empty = selection.start == selection.end;
-// let is_newest = selection == newest;
-
-// let layout = SelectionLayout::new(
-// selection,
-// editor.selections.line_mode,
-// editor.cursor_shape,
-// &snapshot.display_snapshot,
-// is_newest,
-// true,
-// );
-// if is_newest {
-// newest_selection_head = Some(layout.head);
-// }
-
-// for row in cmp::max(layout.active_rows.start, start_row)
-// ..=cmp::min(layout.active_rows.end, end_row)
-// {
-// let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
-// *contains_non_empty_selection |= !is_empty;
-// }
-// layouts.push(layout);
-// }
-
-// selections.push((style.selection, layouts));
-// }
-
-// if let Some(collaboration_hub) = &editor.collaboration_hub {
-// // When following someone, render the local selections in their color.
-// if let Some(leader_id) = editor.leader_peer_id {
-// if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
-// if let Some(participant_index) = collaboration_hub
-// .user_participant_indices(cx)
-// .get(&collaborator.user_id)
-// {
-// if let Some((local_selection_style, _)) = selections.first_mut() {
-// *local_selection_style =
-// style.selection_style_for_room_participant(participant_index.0);
-// }
-// }
-// }
-// }
-
-// let mut remote_selections = HashMap::default();
-// for selection in snapshot.remote_selections_in_range(
-// &(start_anchor..end_anchor),
-// collaboration_hub.as_ref(),
-// cx,
-// ) {
-// let selection_style = if let Some(participant_index) = selection.participant_index {
-// style.selection_style_for_room_participant(participant_index.0)
-// } else {
-// style.absent_selection
-// };
-
-// // Don't re-render the leader's selections, since the local selections
-// // match theirs.
-// if Some(selection.peer_id) == editor.leader_peer_id {
-// continue;
-// }
-
-// remote_selections
-// .entry(selection.replica_id)
-// .or_insert((selection_style, Vec::new()))
-// .1
-// .push(SelectionLayout::new(
-// selection.selection,
-// selection.line_mode,
-// selection.cursor_shape,
-// &snapshot.display_snapshot,
-// false,
-// false,
-// ));
-// }
-
-// selections.extend(remote_selections.into_values());
-// }
-
-// let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
-// let show_scrollbars = match scrollbar_settings.show {
-// ShowScrollbar::Auto => {
-// // Git
-// (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
-// ||
-// // Selections
-// (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty)
-// // Scrollmanager
-// || editor.scroll_manager.scrollbars_visible()
-// }
-// ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
-// ShowScrollbar::Always => true,
-// ShowScrollbar::Never => false,
-// };
-
-// let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
-// .into_iter()
-// .map(|(id, fold)| {
-// let color = self
-// .style
-// .folds
-// .ellipses
-// .background
-// .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
-// .color;
-
-// (id, fold, color)
-// })
-// .collect();
-
-// let head_for_relative = newest_selection_head.unwrap_or_else(|| {
-// let newest = editor.selections.newest::<Point>(cx);
-// SelectionLayout::new(
-// newest,
-// editor.selections.line_mode,
-// editor.cursor_shape,
-// &snapshot.display_snapshot,
-// true,
-// true,
-// )
-// .head
-// });
-
-// let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
-// start_row..end_row,
-// &active_rows,
-// head_for_relative,
-// is_singleton,
-// &snapshot,
-// cx,
-// );
-
-// let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
-
-// let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
-
-// let mut max_visible_line_width = 0.0;
-// let line_layouts =
-// self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
-// for line_with_invisibles in &line_layouts {
-// if line_with_invisibles.line.width() > max_visible_line_width {
-// max_visible_line_width = line_with_invisibles.line.width();
-// }
-// }
-
-// let style = self.style.clone();
-// let longest_line_width = layout_line(
-// snapshot.longest_row(),
-// &snapshot,
-// &style,
-// cx.text_layout_cache(),
-// )
-// .width();
-// let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x;
-// let em_width = style.text.em_width(cx.font_cache());
-// let (scroll_width, blocks) = self.layout_blocks(
-// start_row..end_row,
-// &snapshot,
-// size.x,
-// scroll_width,
-// gutter_padding,
-// gutter_width,
-// em_width,
-// gutter_width + gutter_margin,
-// line_height,
-// &style,
-// &line_layouts,
-// editor,
-// cx,
-// );
-
-// let scroll_max = point(
-// ((scroll_width - text_size.x) / em_width).max(0.0),
-// max_row as f32,
-// );
-
-// let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
-
-// let autoscrolled = if autoscroll_horizontally {
-// editor.autoscroll_horizontally(
-// start_row,
-// text_size.x,
-// scroll_width,
-// em_width,
-// &line_layouts,
-// cx,
-// )
-// } else {
-// false
-// };
-
-// if clamped || autoscrolled {
-// snapshot = editor.snapshot(cx);
-// }
-
-// let style = editor.style(cx);
-
-// let mut context_menu = None;
-// let mut code_actions_indicator = None;
-// if let Some(newest_selection_head) = newest_selection_head {
-// if (start_row..end_row).contains(&newest_selection_head.row()) {
-// if editor.context_menu_visible() {
-// context_menu =
-// editor.render_context_menu(newest_selection_head, style.clone(), cx);
-// }
-
-// let active = matches!(
-// editor.context_menu.read().as_ref(),
-// Some(crate::ContextMenu::CodeActions(_))
-// );
-
-// code_actions_indicator = editor
-// .render_code_actions_indicator(&style, active, cx)
-// .map(|indicator| (newest_selection_head.row(), indicator));
-// }
-// }
-
-// let visible_rows = start_row..start_row + line_layouts.len() as u32;
-// let mut hover = editor.hover_state.render(
-// &snapshot,
-// &style,
-// visible_rows,
-// editor.workspace.as_ref().map(|(w, _)| w.clone()),
-// cx,
-// );
-// let mode = editor.mode;
-
-// let mut fold_indicators = editor.render_fold_indicators(
-// fold_statuses,
-// &style,
-// editor.gutter_hovered,
-// line_height,
-// gutter_margin,
-// cx,
-// );
-
-// if let Some((_, context_menu)) = context_menu.as_mut() {
-// context_menu.layout(
-// SizeConstraint {
-// min: gpui::Point::<Pixels>::zero(),
-// max: point(
-// cx.window_size().x * 0.7,
-// (12. * line_height).min((size.y - line_height) / 2.),
-// ),
-// },
-// editor,
-// cx,
-// );
-// }
-
-// if let Some((_, indicator)) = code_actions_indicator.as_mut() {
-// indicator.layout(
-// SizeConstraint::strict_along(
-// Axis::Vertical,
-// line_height * style.code_actions.vertical_scale,
-// ),
-// editor,
-// cx,
-// );
-// }
-
-// for fold_indicator in fold_indicators.iter_mut() {
-// if let Some(indicator) = fold_indicator.as_mut() {
-// indicator.layout(
-// SizeConstraint::strict_along(
-// Axis::Vertical,
-// line_height * style.code_actions.vertical_scale,
-// ),
-// editor,
-// cx,
-// );
-// }
-// }
-
-// if let Some((_, hover_popovers)) = hover.as_mut() {
-// for hover_popover in hover_popovers.iter_mut() {
-// hover_popover.layout(
-// SizeConstraint {
-// min: gpui::Point::<Pixels>::zero(),
-// max: point(
-// (120. * em_width) // Default size
-// .min(size.x / 2.) // Shrink to half of the editor width
-// .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
-// (16. * line_height) // Default size
-// .min(size.y / 2.) // Shrink to half of the editor height
-// .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
-// ),
-// },
-// editor,
-// cx,
-// );
-// }
-// }
-
-// let invisible_symbol_font_size = self.style.text.font_size / 2.0;
-// let invisible_symbol_style = RunStyle {
-// color: self.style.whitespace,
-// font_id: self.style.text.font_id,
-// underline: Default::default(),
-// };
-
-// (
-// size,
-// LayoutState {
-// mode,
-// position_map: Arc::new(PositionMap {
-// size,
-// scroll_max,
-// line_layouts,
-// line_height,
-// em_width,
-// em_advance,
-// snapshot,
-// }),
-// visible_display_row_range: start_row..end_row,
-// wrap_guides,
-// gutter_size,
-// gutter_padding,
-// text_size,
-// scrollbar_row_range,
-// show_scrollbars,
-// is_singleton,
-// max_row,
-// gutter_margin,
-// active_rows,
-// highlighted_rows,
-// highlighted_ranges,
-// fold_ranges,
-// line_number_layouts,
-// display_hunks,
-// blocks,
-// selections,
-// context_menu,
-// code_actions_indicator,
-// fold_indicators,
-// tab_invisible: cx.text_layout_cache().layout_str(
-// "→",
-// invisible_symbol_font_size,
-// &[("→".len(), invisible_symbol_style)],
-// ),
-// space_invisible: cx.text_layout_cache().layout_str(
-// "•",
-// invisible_symbol_font_size,
-// &[("•".len(), invisible_symbol_style)],
-// ),
-// hover_popovers: hover,
-// },
-// )
-// }
-
-// fn paint(
-// &mut self,
-// bounds: Bounds<Pixels>,
-// visible_bounds: Bounds<Pixels>,
-// layout: &mut Self::LayoutState,
-// editor: &mut Editor,
-// cx: &mut ViewContext<Editor>,
-// ) -> Self::PaintState {
-// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-// cx.scene().push_layer(Some(visible_bounds));
-
-// let gutter_bounds = Bounds::<Pixels>::new(bounds.origin, layout.gutter_size);
-// let text_bounds = Bounds::<Pixels>::new(
-// bounds.origin + point(layout.gutter_size.x, 0.0),
-// layout.text_size,
-// );
-
-// Self::attach_mouse_handlers(
-// &layout.position_map,
-// layout.hover_popovers.is_some(),
-// visible_bounds,
-// text_bounds,
-// gutter_bounds,
-// bounds,
-// cx,
-// );
-
-// self.paint_background(gutter_bounds, text_bounds, layout, cx);
-// if layout.gutter_size.x > 0. {
-// self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx);
-// }
-// self.paint_text(text_bounds, visible_bounds, layout, editor, cx);
-
-// cx.scene().push_layer(Some(bounds));
-// if !layout.blocks.is_empty {
-// self.paint_blocks(bounds, visible_bounds, layout, editor, cx);
-// }
-// self.paint_scrollbar(bounds, layout, &editor, cx);
-// cx.scene().pop_layer();
-// cx.scene().pop_layer();
-// }
-
-// fn rect_for_text_range(
-// &self,
-// range_utf16: Range<usize>,
-// bounds: Bounds<Pixels>,
-// _: Bounds<Pixels>,
-// layout: &Self::LayoutState,
-// _: &Self::PaintState,
-// _: &Editor,
-// _: &ViewContext<Editor>,
-// ) -> Option<Bounds<Pixels>> {
-// let text_bounds = Bounds::<Pixels>::new(
-// bounds.origin + point(layout.gutter_size.x, 0.0),
-// layout.text_size,
-// );
-// let content_origin = text_bounds.origin + point(layout.gutter_margin, 0.);
-// let scroll_position = layout.position_map.snapshot.scroll_position();
-// let start_row = scroll_position.y as u32;
-// let scroll_top = scroll_position.y * layout.position_map.line_height;
-// let scroll_left = scroll_position.x * layout.position_map.em_width;
-
-// let range_start = OffsetUtf16(range_utf16.start)
-// .to_display_point(&layout.position_map.snapshot.display_snapshot);
-// if range_start.row() < start_row {
-// return None;
-// }
-
-// let line = &layout
-// .position_map
-// .line_layouts
-// .get((range_start.row() - start_row) as usize)?
-// .line;
-// let range_start_x = line.x_for_index(range_start.column() as usize);
-// let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
-// Some(Bounds::<Pixels>::new(
-// content_origin
-// + point(
-// range_start_x,
-// range_start_y + layout.position_map.line_height,
-// )
-// - point(scroll_left, scroll_top),
-// point(
-// layout.position_map.em_width,
-// layout.position_map.line_height,
-// ),
-// ))
-// }
-
-// fn debug(
-// &self,
-// bounds: Bounds<Pixels>,
-// _: &Self::LayoutState,
-// _: &Self::PaintState,
-// _: &Editor,
-// _: &ViewContext<Editor>,
-// ) -> json::Value {
-// json!({
-// "type": "BufferElement",
-// "bounds": bounds.to_json()
-// })
-// }
-// }
-
type BufferRow = u32;
pub struct LayoutState {
@@ -88,195 +88,195 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
}
}
-// #[cfg(any(test, feature = "test_support"))]
-// mod tests {
-// // use crate::editor_tests::init_test;
-// use crate::Point;
-// use gpui::TestAppContext;
-// use multi_buffer::{ExcerptRange, MultiBuffer};
-// use project::{FakeFs, Project};
-// use unindent::Unindent;
-// #[gpui::test]
-// async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
-// use git::diff::DiffHunkStatus;
-// init_test(cx, |_| {});
+#[cfg(test)]
+mod tests {
+ use crate::editor_tests::init_test;
+ use crate::Point;
+ use gpui::{Context, TestAppContext};
+ use multi_buffer::{ExcerptRange, MultiBuffer};
+ use project::{FakeFs, Project};
+ use unindent::Unindent;
+ #[gpui::test]
+ async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+ use git::diff::DiffHunkStatus;
+ init_test(cx, |_| {});
-// let fs = FakeFs::new(cx.background());
-// let project = Project::test(fs, [], cx).await;
+ let fs = FakeFs::new(cx.background_executor.clone());
+ let project = Project::test(fs, [], cx).await;
-// // buffer has two modified hunks with two rows each
-// let buffer_1 = project
-// .update(cx, |project, cx| {
-// project.create_buffer(
-// "
-// 1.zero
-// 1.ONE
-// 1.TWO
-// 1.three
-// 1.FOUR
-// 1.FIVE
-// 1.six
-// "
-// .unindent()
-// .as_str(),
-// None,
-// cx,
-// )
-// })
-// .unwrap();
-// buffer_1.update(cx, |buffer, cx| {
-// buffer.set_diff_base(
-// Some(
-// "
-// 1.zero
-// 1.one
-// 1.two
-// 1.three
-// 1.four
-// 1.five
-// 1.six
-// "
-// .unindent(),
-// ),
-// cx,
-// );
-// });
+ // buffer has two modified hunks with two rows each
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.create_buffer(
+ "
+ 1.zero
+ 1.ONE
+ 1.TWO
+ 1.three
+ 1.FOUR
+ 1.FIVE
+ 1.six
+ "
+ .unindent()
+ .as_str(),
+ None,
+ cx,
+ )
+ })
+ .unwrap();
+ buffer_1.update(cx, |buffer, cx| {
+ buffer.set_diff_base(
+ Some(
+ "
+ 1.zero
+ 1.one
+ 1.two
+ 1.three
+ 1.four
+ 1.five
+ 1.six
+ "
+ .unindent(),
+ ),
+ cx,
+ );
+ });
-// // buffer has a deletion hunk and an insertion hunk
-// let buffer_2 = project
-// .update(cx, |project, cx| {
-// project.create_buffer(
-// "
-// 2.zero
-// 2.one
-// 2.two
-// 2.three
-// 2.four
-// 2.five
-// 2.six
-// "
-// .unindent()
-// .as_str(),
-// None,
-// cx,
-// )
-// })
-// .unwrap();
-// buffer_2.update(cx, |buffer, cx| {
-// buffer.set_diff_base(
-// Some(
-// "
-// 2.zero
-// 2.one
-// 2.one-and-a-half
-// 2.two
-// 2.three
-// 2.four
-// 2.six
-// "
-// .unindent(),
-// ),
-// cx,
-// );
-// });
+ // buffer has a deletion hunk and an insertion hunk
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.create_buffer(
+ "
+ 2.zero
+ 2.one
+ 2.two
+ 2.three
+ 2.four
+ 2.five
+ 2.six
+ "
+ .unindent()
+ .as_str(),
+ None,
+ cx,
+ )
+ })
+ .unwrap();
+ buffer_2.update(cx, |buffer, cx| {
+ buffer.set_diff_base(
+ Some(
+ "
+ 2.zero
+ 2.one
+ 2.one-and-a-half
+ 2.two
+ 2.three
+ 2.four
+ 2.six
+ "
+ .unindent(),
+ ),
+ cx,
+ );
+ });
-// cx.foreground().run_until_parked();
+ cx.background_executor.run_until_parked();
-// let multibuffer = cx.add_model(|cx| {
-// let mut multibuffer = MultiBuffer::new(0);
-// multibuffer.push_excerpts(
-// buffer_1.clone(),
-// [
-// // excerpt ends in the middle of a modified hunk
-// ExcerptRange {
-// context: Point::new(0, 0)..Point::new(1, 5),
-// primary: Default::default(),
-// },
-// // excerpt begins in the middle of a modified hunk
-// ExcerptRange {
-// context: Point::new(5, 0)..Point::new(6, 5),
-// primary: Default::default(),
-// },
-// ],
-// cx,
-// );
-// multibuffer.push_excerpts(
-// buffer_2.clone(),
-// [
-// // excerpt ends at a deletion
-// ExcerptRange {
-// context: Point::new(0, 0)..Point::new(1, 5),
-// primary: Default::default(),
-// },
-// // excerpt starts at a deletion
-// ExcerptRange {
-// context: Point::new(2, 0)..Point::new(2, 5),
-// primary: Default::default(),
-// },
-// // excerpt fully contains a deletion hunk
-// ExcerptRange {
-// context: Point::new(1, 0)..Point::new(2, 5),
-// primary: Default::default(),
-// },
-// // excerpt fully contains an insertion hunk
-// ExcerptRange {
-// context: Point::new(4, 0)..Point::new(6, 5),
-// primary: Default::default(),
-// },
-// ],
-// cx,
-// );
-// multibuffer
-// });
+ let multibuffer = cx.build_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ // excerpt ends in the middle of a modified hunk
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 5),
+ primary: Default::default(),
+ },
+ // excerpt begins in the middle of a modified hunk
+ ExcerptRange {
+ context: Point::new(5, 0)..Point::new(6, 5),
+ primary: Default::default(),
+ },
+ ],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ // excerpt ends at a deletion
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 5),
+ primary: Default::default(),
+ },
+ // excerpt starts at a deletion
+ ExcerptRange {
+ context: Point::new(2, 0)..Point::new(2, 5),
+ primary: Default::default(),
+ },
+ // excerpt fully contains a deletion hunk
+ ExcerptRange {
+ context: Point::new(1, 0)..Point::new(2, 5),
+ primary: Default::default(),
+ },
+ // excerpt fully contains an insertion hunk
+ ExcerptRange {
+ context: Point::new(4, 0)..Point::new(6, 5),
+ primary: Default::default(),
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
-// let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+ let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
-// assert_eq!(
-// snapshot.text(),
-// "
-// 1.zero
-// 1.ONE
-// 1.FIVE
-// 1.six
-// 2.zero
-// 2.one
-// 2.two
-// 2.one
-// 2.two
-// 2.four
-// 2.five
-// 2.six"
-// .unindent()
-// );
+ assert_eq!(
+ snapshot.text(),
+ "
+ 1.zero
+ 1.ONE
+ 1.FIVE
+ 1.six
+ 2.zero
+ 2.one
+ 2.two
+ 2.one
+ 2.two
+ 2.four
+ 2.five
+ 2.six"
+ .unindent()
+ );
-// let expected = [
-// (DiffHunkStatus::Modified, 1..2),
-// (DiffHunkStatus::Modified, 2..3),
-// //TODO: Define better when and where removed hunks show up at range extremities
-// (DiffHunkStatus::Removed, 6..6),
-// (DiffHunkStatus::Removed, 8..8),
-// (DiffHunkStatus::Added, 10..11),
-// ];
+ let expected = [
+ (DiffHunkStatus::Modified, 1..2),
+ (DiffHunkStatus::Modified, 2..3),
+ //TODO: Define better when and where removed hunks show up at range extremities
+ (DiffHunkStatus::Removed, 6..6),
+ (DiffHunkStatus::Removed, 8..8),
+ (DiffHunkStatus::Added, 10..11),
+ ];
-// assert_eq!(
-// snapshot
-// .git_diff_hunks_in_range(0..12)
-// .map(|hunk| (hunk.status(), hunk.buffer_range))
-// .collect::<Vec<_>>(),
-// &expected,
-// );
+ assert_eq!(
+ snapshot
+ .git_diff_hunks_in_range(0..12)
+ .map(|hunk| (hunk.status(), hunk.buffer_range))
+ .collect::<Vec<_>>(),
+ &expected,
+ );
-// assert_eq!(
-// snapshot
-// .git_diff_hunks_in_range_rev(0..12)
-// .map(|hunk| (hunk.status(), hunk.buffer_range))
-// .collect::<Vec<_>>(),
-// expected
-// .iter()
-// .rev()
-// .cloned()
-// .collect::<Vec<_>>()
-// .as_slice(),
-// );
-// }
-// }
+ assert_eq!(
+ snapshot
+ .git_diff_hunks_in_range_rev(0..12)
+ .map(|hunk| (hunk.status(), hunk.buffer_range))
+ .collect::<Vec<_>>(),
+ expected
+ .iter()
+ .rev()
+ .cloned()
+ .collect::<Vec<_>>()
+ .as_slice(),
+ );
+ }
+}
@@ -5,7 +5,7 @@ use crate::{Editor, RangeToAnchorExt};
enum MatchingBracketHighlight {}
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
- // editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+ editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
@@ -30,109 +30,109 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
}
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
-// use indoc::indoc;
-// use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+ use indoc::indoc;
+ use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
-// #[gpui::test]
-// async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
+ #[gpui::test]
+ async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
-// let mut cx = EditorLspTestContext::new(
-// Language::new(
-// LanguageConfig {
-// name: "Rust".into(),
-// path_suffixes: vec!["rs".to_string()],
-// brackets: BracketPairConfig {
-// pairs: vec![
-// BracketPair {
-// start: "{".to_string(),
-// end: "}".to_string(),
-// close: false,
-// newline: true,
-// },
-// BracketPair {
-// start: "(".to_string(),
-// end: ")".to_string(),
-// close: false,
-// newline: true,
-// },
-// ],
-// ..Default::default()
-// },
-// ..Default::default()
-// },
-// Some(tree_sitter_rust::language()),
-// )
-// .with_brackets_query(indoc! {r#"
-// ("{" @open "}" @close)
-// ("(" @open ")" @close)
-// "#})
-// .unwrap(),
-// Default::default(),
-// cx,
-// )
-// .await;
+ let mut cx = EditorLspTestContext::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: false,
+ newline: true,
+ },
+ BracketPair {
+ start: "(".to_string(),
+ end: ")".to_string(),
+ close: false,
+ newline: true,
+ },
+ ],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_brackets_query(indoc! {r#"
+ ("{" @open "}" @close)
+ ("(" @open ")" @close)
+ "#})
+ .unwrap(),
+ Default::default(),
+ cx,
+ )
+ .await;
-// // positioning cursor inside bracket highlights both
-// cx.set_state(indoc! {r#"
-// pub fn test("Test ˇargument") {
-// another_test(1, 2, 3);
-// }
-// "#});
-// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-// pub fn test«(»"Test argument"«)» {
-// another_test(1, 2, 3);
-// }
-// "#});
+ // positioning cursor inside bracket highlights both
+ cx.set_state(indoc! {r#"
+ pub fn test("Test ˇargument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test«(»"Test argument"«)» {
+ another_test(1, 2, 3);
+ }
+ "#});
-// cx.set_state(indoc! {r#"
-// pub fn test("Test argument") {
-// another_test(1, ˇ2, 3);
-// }
-// "#});
-// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-// pub fn test("Test argument") {
-// another_test«(»1, 2, 3«)»;
-// }
-// "#});
+ cx.set_state(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, ˇ2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test«(»1, 2, 3«)»;
+ }
+ "#});
-// cx.set_state(indoc! {r#"
-// pub fn test("Test argument") {
-// anotherˇ_test(1, 2, 3);
-// }
-// "#});
-// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-// pub fn test("Test argument") «{»
-// another_test(1, 2, 3);
-// «}»
-// "#});
+ cx.set_state(indoc! {r#"
+ pub fn test("Test argument") {
+ anotherˇ_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") «{»
+ another_test(1, 2, 3);
+ «}»
+ "#});
-// // positioning outside of brackets removes highlight
-// cx.set_state(indoc! {r#"
-// pub fˇn test("Test argument") {
-// another_test(1, 2, 3);
-// }
-// "#});
-// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-// pub fn test("Test argument") {
-// another_test(1, 2, 3);
-// }
-// "#});
+ // positioning outside of brackets removes highlight
+ cx.set_state(indoc! {r#"
+ pub fˇn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
-// // non empty selection dismisses highlight
-// cx.set_state(indoc! {r#"
-// pub fn test("Te«st argˇ»ument") {
-// another_test(1, 2, 3);
-// }
-// "#});
-// cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
-// pub fn test("Test argument") {
-// another_test(1, 2, 3);
-// }
-// "#});
-// }
-// }
+ // non empty selection dismisses highlight
+ cx.set_state(indoc! {r#"
+ pub fn test("Te«st argˇ»ument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ }
+}
@@ -2432,13 +2432,13 @@ pub mod tests {
let language = Arc::new(language);
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
- "/a",
- json!({
- "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
- "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
- }),
- )
- .await;
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+ }),
+ )
+ .await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language))
@@ -2598,24 +2598,22 @@ pub mod tests {
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
- let expected_hints = vec![
- "main hint #0".to_string(),
- "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,
- cached_hint_labels(editor),
- "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
- );
- assert_eq!(expected_hints, visible_hint_labels(editor, cx));
- assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
- });
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ ];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
+ });
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2630,23 +2628,23 @@ pub mod tests {
});
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
- let expected_hints = vec![
- "main hint #0".to_string(),
- "main hint #1".to_string(),
- "main hint #2".to_string(),
- "main hint #3".to_string(),
- "main hint #4".to_string(),
- "main hint #5".to_string(),
- "other hint #0".to_string(),
- "other hint #1".to_string(),
- "other hint #2".to_string(),
- ];
- assert_eq!(expected_hints, cached_hint_labels(editor),
- "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
- assert_eq!(expected_hints, visible_hint_labels(editor, cx));
- assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
- "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
- });
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
+ "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
+ });
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2658,26 +2656,26 @@ pub mod tests {
));
cx.executor().run_until_parked();
let last_scroll_update_version = editor.update(cx, |editor, cx| {
- let expected_hints = vec![
- "main hint #0".to_string(),
- "main hint #1".to_string(),
- "main hint #2".to_string(),
- "main hint #3".to_string(),
- "main hint #4".to_string(),
- "main hint #5".to_string(),
- "other hint #0".to_string(),
- "other hint #1".to_string(),
- "other hint #2".to_string(),
- "other hint #3".to_string(),
- "other hint #4".to_string(),
- "other hint #5".to_string(),
- ];
- assert_eq!(expected_hints, cached_hint_labels(editor),
- "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
- assert_eq!(expected_hints, visible_hint_labels(editor, cx));
- assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
- expected_hints.len()
- }).unwrap();
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
+ expected_hints.len()
+ }).unwrap();
editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::Next), cx, |s| {
@@ -2686,30 +2684,31 @@ pub mod tests {
});
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| {
- let expected_hints = vec![
- "main hint #0".to_string(),
- "main hint #1".to_string(),
- "main hint #2".to_string(),
- "main hint #3".to_string(),
- "main hint #4".to_string(),
- "main hint #5".to_string(),
- "other hint #0".to_string(),
- "other hint #1".to_string(),
- "other hint #2".to_string(),
- "other hint #3".to_string(),
- "other hint #4".to_string(),
- "other hint #5".to_string(),
- ];
- assert_eq!(expected_hints, cached_hint_labels(editor),
- "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
- assert_eq!(expected_hints, visible_hint_labels(editor, cx));
- assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
- });
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
+ });
editor_edited.store(true, Ordering::Release);
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
- s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+ // TODO if this gets set to hint boundary (e.g. 56) we sometimes get an extra cache version bump, why?
+ s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
});
editor.handle_input("++++more text++++", cx);
});
@@ -2729,15 +2728,15 @@ pub mod tests {
expected_hints,
cached_hint_labels(editor),
"After multibuffer edit, editor gets scolled back to the last selection; \
-all hints should be invalidated and requeried for all of its visible excerpts"
+ all hints should be invalidated and requeried for all of its visible excerpts"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let current_cache_version = editor.inlay_hint_cache().version;
- let minimum_expected_version = last_scroll_update_version + expected_hints.len();
- assert!(
- current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
- "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
+ assert_eq!(
+ current_cache_version,
+ last_scroll_update_version + expected_hints.len(),
+ "We should have updated cache N times == N of new hints arrived (separately from each excerpt)"
);
});
}
@@ -4,13 +4,14 @@ use crate::{
EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot,
NavigationData, ToPoint as _,
};
-use anyhow::{anyhow, Context, Result};
+use anyhow::{anyhow, Context as _, Result};
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,
- ViewContext, VisualContext, WeakView, WindowContext,
+ div, point, AnyElement, AppContext, AsyncAppContext, AsyncWindowContext, Context, Div, Entity,
+ EntityId, EventEmitter, FocusHandle, IntoElement, Model, ParentElement, Pixels, Render,
+ SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
+ WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@@ -20,6 +21,7 @@ use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPat
use rpc::proto::{self, update_view, PeerId};
use settings::Settings;
use smallvec::SmallVec;
+use std::fmt::Write;
use std::{
borrow::Cow,
cmp::{self, Ordering},
@@ -31,8 +33,11 @@ use std::{
use text::Selection;
use theme::{ActiveTheme, Theme};
use ui::{Color, Label};
-use util::{paths::PathExt, ResultExt, TryFutureExt};
-use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
+use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
+use workspace::{
+ item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
+ StatusItemView,
+};
use workspace::{
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
@@ -71,110 +76,108 @@ impl FollowableItem for Editor {
workspace: View<Workspace>,
remote_id: ViewId,
state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
+ cx: &mut WindowContext,
) -> Option<Task<Result<View<Self>>>> {
- todo!()
+ let project = workspace.read(cx).project().to_owned();
+ let Some(proto::view::Variant::Editor(_)) = state else {
+ return None;
+ };
+ let Some(proto::view::Variant::Editor(state)) = state.take() else {
+ unreachable!()
+ };
+
+ let client = project.read(cx).client();
+ let replica_id = project.read(cx).replica_id();
+ let buffer_ids = state
+ .excerpts
+ .iter()
+ .map(|excerpt| excerpt.buffer_id)
+ .collect::<HashSet<_>>();
+ let buffers = project.update(cx, |project, cx| {
+ buffer_ids
+ .iter()
+ .map(|id| project.open_buffer_by_id(*id, cx))
+ .collect::<Vec<_>>()
+ });
+
+ let pane = pane.downgrade();
+ Some(cx.spawn(|mut cx| async move {
+ let mut buffers = futures::future::try_join_all(buffers).await?;
+ let editor = pane.update(&mut cx, |pane, cx| {
+ let mut editors = pane.items_of_type::<Self>();
+ editors.find(|editor| {
+ let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+ let singleton_buffer_matches = state.singleton
+ && buffers.first()
+ == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+ ids_match || singleton_buffer_matches
+ })
+ })?;
+
+ let editor = if let Some(editor) = editor {
+ editor
+ } else {
+ pane.update(&mut cx, |_, cx| {
+ let multibuffer = cx.build_model(|cx| {
+ let mut multibuffer;
+ if state.singleton && buffers.len() == 1 {
+ multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+ } else {
+ multibuffer = MultiBuffer::new(replica_id);
+ let mut excerpts = state.excerpts.into_iter().peekable();
+ while let Some(excerpt) = excerpts.peek() {
+ let buffer_id = excerpt.buffer_id;
+ let buffer_excerpts = iter::from_fn(|| {
+ let excerpt = excerpts.peek()?;
+ (excerpt.buffer_id == buffer_id)
+ .then(|| excerpts.next().unwrap())
+ });
+ let buffer =
+ buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+ if let Some(buffer) = buffer {
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ buffer_excerpts.filter_map(deserialize_excerpt_range),
+ cx,
+ );
+ }
+ }
+ };
+
+ if let Some(title) = &state.title {
+ multibuffer = multibuffer.with_title(title.clone())
+ }
+
+ multibuffer
+ });
+
+ cx.build_view(|cx| {
+ let mut editor =
+ Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+ editor.remote_id = Some(remote_id);
+ editor
+ })
+ })?
+ };
+
+ update_editor_from_message(
+ editor.downgrade(),
+ project,
+ proto::update_view::Editor {
+ selections: state.selections,
+ pending_selection: state.pending_selection,
+ scroll_top_anchor: state.scroll_top_anchor,
+ scroll_x: state.scroll_x,
+ scroll_y: state.scroll_y,
+ ..Default::default()
+ },
+ &mut cx,
+ )
+ .await?;
+
+ Ok(editor)
+ }))
}
- // let project = workspace.read(cx).project().to_owned();
- // let Some(proto::view::Variant::Editor(_)) = state else {
- // return None;
- // };
- // let Some(proto::view::Variant::Editor(state)) = state.take() else {
- // unreachable!()
- // };
-
- // let client = project.read(cx).client();
- // let replica_id = project.read(cx).replica_id();
- // let buffer_ids = state
- // .excerpts
- // .iter()
- // .map(|excerpt| excerpt.buffer_id)
- // .collect::<HashSet<_>>();
- // let buffers = project.update(cx, |project, cx| {
- // buffer_ids
- // .iter()
- // .map(|id| project.open_buffer_by_id(*id, cx))
- // .collect::<Vec<_>>()
- // });
-
- // let pane = pane.downgrade();
- // Some(cx.spawn(|mut cx| async move {
- // let mut buffers = futures::future::try_join_all(buffers).await?;
- // let editor = pane.read_with(&cx, |pane, cx| {
- // let mut editors = pane.items_of_type::<Self>();
- // editors.find(|editor| {
- // let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
- // let singleton_buffer_matches = state.singleton
- // && buffers.first()
- // == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
- // ids_match || singleton_buffer_matches
- // })
- // })?;
-
- // let editor = if let Some(editor) = editor {
- // editor
- // } else {
- // pane.update(&mut cx, |_, cx| {
- // let multibuffer = cx.add_model(|cx| {
- // let mut multibuffer;
- // if state.singleton && buffers.len() == 1 {
- // multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
- // } else {
- // multibuffer = MultiBuffer::new(replica_id);
- // let mut excerpts = state.excerpts.into_iter().peekable();
- // while let Some(excerpt) = excerpts.peek() {
- // let buffer_id = excerpt.buffer_id;
- // let buffer_excerpts = iter::from_fn(|| {
- // let excerpt = excerpts.peek()?;
- // (excerpt.buffer_id == buffer_id)
- // .then(|| excerpts.next().unwrap())
- // });
- // let buffer =
- // buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
- // if let Some(buffer) = buffer {
- // multibuffer.push_excerpts(
- // buffer.clone(),
- // buffer_excerpts.filter_map(deserialize_excerpt_range),
- // cx,
- // );
- // }
- // }
- // };
-
- // if let Some(title) = &state.title {
- // multibuffer = multibuffer.with_title(title.clone())
- // }
-
- // multibuffer
- // });
-
- // cx.add_view(|cx| {
- // let mut editor =
- // Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
- // editor.remote_id = Some(remote_id);
- // editor
- // })
- // })?
- // };
-
- // update_editor_from_message(
- // editor.downgrade(),
- // project,
- // proto::update_view::Editor {
- // selections: state.selections,
- // pending_selection: state.pending_selection,
- // scroll_top_anchor: state.scroll_top_anchor,
- // scroll_x: state.scroll_x,
- // scroll_y: state.scroll_y,
- // ..Default::default()
- // },
- // &mut cx,
- // )
- // .await?;
-
- // Ok(editor)
- // }))
- // }
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
self.leader_peer_id = leader_peer_id;
@@ -195,7 +198,7 @@ impl FollowableItem for Editor {
cx.notify();
}
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let buffer = self.buffer.read(cx);
let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer
@@ -242,7 +245,7 @@ impl FollowableItem for Editor {
&self,
event: &Self::FollowableEvent,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool {
let update =
update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
@@ -315,7 +318,7 @@ impl FollowableItem for Editor {
})
}
- fn is_project_item(&self, _cx: &AppContext) -> bool {
+ fn is_project_item(&self, _cx: &WindowContext) -> bool {
true
}
}
@@ -324,132 +327,129 @@ async fn update_editor_from_message(
this: WeakView<Editor>,
project: Model<Project>,
message: proto::update_view::Editor,
- cx: &mut AsyncAppContext,
+ cx: &mut AsyncWindowContext,
) -> Result<()> {
- todo!()
+ // Open all of the buffers of which excerpts were added to the editor.
+ let inserted_excerpt_buffer_ids = message
+ .inserted_excerpts
+ .iter()
+ .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+ .collect::<HashSet<_>>();
+ let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+ inserted_excerpt_buffer_ids
+ .into_iter()
+ .map(|id| project.open_buffer_by_id(id, cx))
+ .collect::<Vec<_>>()
+ })?;
+ let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+ // Update the editor's excerpts.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |multibuffer, cx| {
+ let mut removed_excerpt_ids = message
+ .deleted_excerpts
+ .into_iter()
+ .map(ExcerptId::from_proto)
+ .collect::<Vec<_>>();
+ removed_excerpt_ids.sort_by({
+ let multibuffer = multibuffer.read(cx);
+ move |a, b| a.cmp(&b, &multibuffer)
+ });
+
+ let mut insertions = message.inserted_excerpts.into_iter().peekable();
+ while let Some(insertion) = insertions.next() {
+ let Some(excerpt) = insertion.excerpt else {
+ continue;
+ };
+ let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
+ continue;
+ };
+ let buffer_id = excerpt.buffer_id;
+ let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
+ continue;
+ };
+
+ let adjacent_excerpts = iter::from_fn(|| {
+ let insertion = insertions.peek()?;
+ if insertion.previous_excerpt_id.is_none()
+ && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+ {
+ insertions.next()?.excerpt
+ } else {
+ None
+ }
+ });
+
+ multibuffer.insert_excerpts_with_ids_after(
+ ExcerptId::from_proto(previous_excerpt_id),
+ buffer,
+ [excerpt]
+ .into_iter()
+ .chain(adjacent_excerpts)
+ .filter_map(|excerpt| {
+ Some((
+ ExcerptId::from_proto(excerpt.id),
+ deserialize_excerpt_range(excerpt)?,
+ ))
+ }),
+ cx,
+ );
+ }
+
+ multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+ });
+ })?;
+
+ // Deserialize the editor state.
+ let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+ let buffer = editor.buffer.read(cx).read(cx);
+ let selections = message
+ .selections
+ .into_iter()
+ .filter_map(|selection| deserialize_selection(&buffer, selection))
+ .collect::<Vec<_>>();
+ let pending_selection = message
+ .pending_selection
+ .and_then(|selection| deserialize_selection(&buffer, selection));
+ let scroll_top_anchor = message
+ .scroll_top_anchor
+ .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+ anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+ })??;
+
+ // Wait until the buffer has received all of the operations referenced by
+ // the editor's new state.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.wait_for_anchors(
+ selections
+ .iter()
+ .chain(pending_selection.as_ref())
+ .flat_map(|selection| [selection.start, selection.end])
+ .chain(scroll_top_anchor),
+ cx,
+ )
+ })
+ })?
+ .await?;
+
+ // Update the editor's state.
+ this.update(cx, |editor, cx| {
+ if !selections.is_empty() || pending_selection.is_some() {
+ editor.set_selections_from_remote(selections, pending_selection, cx);
+ editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+ } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+ editor.set_scroll_anchor_remote(
+ ScrollAnchor {
+ anchor: scroll_top_anchor,
+ offset: point(message.scroll_x, message.scroll_y),
+ },
+ cx,
+ );
+ }
+ })?;
+ Ok(())
}
-// Previous implementation of the above
-// // Open all of the buffers of which excerpts were added to the editor.
-// let inserted_excerpt_buffer_ids = message
-// .inserted_excerpts
-// .iter()
-// .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
-// .collect::<HashSet<_>>();
-// let inserted_excerpt_buffers = project.update(cx, |project, cx| {
-// inserted_excerpt_buffer_ids
-// .into_iter()
-// .map(|id| project.open_buffer_by_id(id, cx))
-// .collect::<Vec<_>>()
-// })?;
-// let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
-
-// // Update the editor's excerpts.
-// this.update(cx, |editor, cx| {
-// editor.buffer.update(cx, |multibuffer, cx| {
-// let mut removed_excerpt_ids = message
-// .deleted_excerpts
-// .into_iter()
-// .map(ExcerptId::from_proto)
-// .collect::<Vec<_>>();
-// removed_excerpt_ids.sort_by({
-// let multibuffer = multibuffer.read(cx);
-// move |a, b| a.cmp(&b, &multibuffer)
-// });
-
-// let mut insertions = message.inserted_excerpts.into_iter().peekable();
-// while let Some(insertion) = insertions.next() {
-// let Some(excerpt) = insertion.excerpt else {
-// continue;
-// };
-// let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
-// continue;
-// };
-// let buffer_id = excerpt.buffer_id;
-// let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
-// continue;
-// };
-
-// let adjacent_excerpts = iter::from_fn(|| {
-// let insertion = insertions.peek()?;
-// if insertion.previous_excerpt_id.is_none()
-// && insertion.excerpt.as_ref()?.buffer_id == buffer_id
-// {
-// insertions.next()?.excerpt
-// } else {
-// None
-// }
-// });
-
-// multibuffer.insert_excerpts_with_ids_after(
-// ExcerptId::from_proto(previous_excerpt_id),
-// buffer,
-// [excerpt]
-// .into_iter()
-// .chain(adjacent_excerpts)
-// .filter_map(|excerpt| {
-// Some((
-// ExcerptId::from_proto(excerpt.id),
-// deserialize_excerpt_range(excerpt)?,
-// ))
-// }),
-// cx,
-// );
-// }
-
-// multibuffer.remove_excerpts(removed_excerpt_ids, cx);
-// });
-// })?;
-
-// // Deserialize the editor state.
-// let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
-// let buffer = editor.buffer.read(cx).read(cx);
-// let selections = message
-// .selections
-// .into_iter()
-// .filter_map(|selection| deserialize_selection(&buffer, selection))
-// .collect::<Vec<_>>();
-// let pending_selection = message
-// .pending_selection
-// .and_then(|selection| deserialize_selection(&buffer, selection));
-// let scroll_top_anchor = message
-// .scroll_top_anchor
-// .and_then(|anchor| deserialize_anchor(&buffer, anchor));
-// anyhow::Ok((selections, pending_selection, scroll_top_anchor))
-// })??;
-
-// // Wait until the buffer has received all of the operations referenced by
-// // the editor's new state.
-// this.update(cx, |editor, cx| {
-// editor.buffer.update(cx, |buffer, cx| {
-// buffer.wait_for_anchors(
-// selections
-// .iter()
-// .chain(pending_selection.as_ref())
-// .flat_map(|selection| [selection.start, selection.end])
-// .chain(scroll_top_anchor),
-// cx,
-// )
-// })
-// })?
-// .await?;
-
-// // Update the editor's state.
-// this.update(cx, |editor, cx| {
-// if !selections.is_empty() || pending_selection.is_some() {
-// editor.set_selections_from_remote(selections, pending_selection, cx);
-// editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
-// } else if let Some(scroll_top_anchor) = scroll_top_anchor {
-// editor.set_scroll_anchor_remote(
-// ScrollAnchor {
-// anchor: scroll_top_anchor,
-// offset: point(message.scroll_x, message.scroll_y),
-// },
-// cx,
-// );
-// }
-// })?;
-// Ok(())
-// }
fn serialize_excerpt(
buffer_id: u64,
@@ -529,39 +529,38 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
- todo!();
- // if let Ok(data) = data.downcast::<NavigationData>() {
- // let newest_selection = self.selections.newest::<Point>(cx);
- // let buffer = self.buffer.read(cx).read(cx);
- // let offset = if buffer.can_resolve(&data.cursor_anchor) {
- // data.cursor_anchor.to_point(&buffer)
- // } else {
- // buffer.clip_point(data.cursor_position, Bias::Left)
- // };
-
- // let mut scroll_anchor = data.scroll_anchor;
- // if !buffer.can_resolve(&scroll_anchor.anchor) {
- // scroll_anchor.anchor = buffer.anchor_before(
- // buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
- // );
- // }
-
- // drop(buffer);
-
- // if newest_selection.head() == offset {
- // false
- // } else {
- // let nav_history = self.nav_history.take();
- // self.set_scroll_anchor(scroll_anchor, cx);
- // self.change_selections(Some(Autoscroll::fit()), cx, |s| {
- // s.select_ranges([offset..offset])
- // });
- // self.nav_history = nav_history;
- // true
- // }
- // } else {
- // false
- // }
+ if let Ok(data) = data.downcast::<NavigationData>() {
+ let newest_selection = self.selections.newest::<Point>(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ let offset = if buffer.can_resolve(&data.cursor_anchor) {
+ data.cursor_anchor.to_point(&buffer)
+ } else {
+ buffer.clip_point(data.cursor_position, Bias::Left)
+ };
+
+ let mut scroll_anchor = data.scroll_anchor;
+ if !buffer.can_resolve(&scroll_anchor.anchor) {
+ scroll_anchor.anchor = buffer.anchor_before(
+ buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
+ );
+ }
+
+ drop(buffer);
+
+ if newest_selection.head() == offset {
+ false
+ } else {
+ let nav_history = self.nav_history.take();
+ self.set_scroll_anchor(scroll_anchor, cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([offset..offset])
+ });
+ self.nav_history = nav_history;
+ true
+ }
+ } else {
+ false
+ }
}
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
@@ -765,35 +764,34 @@ impl Item for Editor {
}
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
- todo!();
- // let cursor = self.selections.newest_anchor().head();
- // let multibuffer = &self.buffer().read(cx);
- // let (buffer_id, symbols) =
- // multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
- // let buffer = multibuffer.buffer(buffer_id)?;
-
- // let buffer = buffer.read(cx);
- // let filename = buffer
- // .snapshot()
- // .resolve_file_path(
- // cx,
- // self.project
- // .as_ref()
- // .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
- // .unwrap_or_default(),
- // )
- // .map(|path| path.to_string_lossy().to_string())
- // .unwrap_or_else(|| "untitled".to_string());
-
- // let mut breadcrumbs = vec![BreadcrumbText {
- // text: filename,
- // highlights: None,
- // }];
- // breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
- // text: symbol.text,
- // highlights: Some(symbol.highlight_ranges),
- // }));
- // Some(breadcrumbs)
+ let cursor = self.selections.newest_anchor().head();
+ let multibuffer = &self.buffer().read(cx);
+ let (buffer_id, symbols) =
+ multibuffer.symbols_containing(cursor, Some(&variant.syntax()), cx)?;
+ let buffer = multibuffer.buffer(buffer_id)?;
+
+ let buffer = buffer.read(cx);
+ let filename = buffer
+ .snapshot()
+ .resolve_file_path(
+ cx,
+ self.project
+ .as_ref()
+ .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+ .unwrap_or_default(),
+ )
+ .map(|path| path.to_string_lossy().to_string())
+ .unwrap_or_else(|| "untitled".to_string());
+
+ let mut breadcrumbs = vec![BreadcrumbText {
+ text: filename,
+ highlights: None,
+ }];
+ breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
+ text: symbol.text,
+ highlights: Some(symbol.highlight_ranges),
+ }));
+ Some(breadcrumbs)
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
@@ -1120,86 +1118,78 @@ pub struct CursorPosition {
_observe_active_editor: Option<Subscription>,
}
-// impl Default for CursorPosition {
-// fn default() -> Self {
-// Self::new()
-// }
-// }
-
-// impl CursorPosition {
-// pub fn new() -> Self {
-// Self {
-// position: None,
-// selected_count: 0,
-// _observe_active_editor: None,
-// }
-// }
-
-// fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
-// let editor = editor.read(cx);
-// let buffer = editor.buffer().read(cx).snapshot(cx);
-
-// self.selected_count = 0;
-// let mut last_selection: Option<Selection<usize>> = None;
-// for selection in editor.selections.all::<usize>(cx) {
-// self.selected_count += selection.end - selection.start;
-// if last_selection
-// .as_ref()
-// .map_or(true, |last_selection| selection.id > last_selection.id)
-// {
-// last_selection = Some(selection);
-// }
-// }
-// self.position = last_selection.map(|s| s.head().to_point(&buffer));
-
-// cx.notify();
-// }
-// }
-
-// impl Entity for CursorPosition {
-// type Event = ();
-// }
-
-// impl View for CursorPosition {
-// fn ui_name() -> &'static str {
-// "CursorPosition"
-// }
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-// if let Some(position) = self.position {
-// let theme = &theme::current(cx).workspace.status_bar;
-// let mut text = format!(
-// "{}{FILE_ROW_COLUMN_DELIMITER}{}",
-// position.row + 1,
-// position.column + 1
-// );
-// if self.selected_count > 0 {
-// write!(text, " ({} selected)", self.selected_count).unwrap();
-// }
-// Label::new(text, theme.cursor_position.clone()).into_any()
-// } else {
-// Empty::new().into_any()
-// }
-// }
-// }
-
-// impl StatusItemView for CursorPosition {
-// fn set_active_pane_item(
-// &mut self,
-// active_pane_item: Option<&dyn ItemHandle>,
-// cx: &mut ViewContext<Self>,
-// ) {
-// if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
-// self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
-// self.update_position(editor, cx);
-// } else {
-// self.position = None;
-// self._observe_active_editor = None;
-// }
-
-// cx.notify();
-// }
-// }
+impl Default for CursorPosition {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl CursorPosition {
+ pub fn new() -> Self {
+ Self {
+ position: None,
+ selected_count: 0,
+ _observe_active_editor: None,
+ }
+ }
+
+ fn update_position(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ let editor = editor.read(cx);
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+
+ self.selected_count = 0;
+ let mut last_selection: Option<Selection<usize>> = None;
+ for selection in editor.selections.all::<usize>(cx) {
+ self.selected_count += selection.end - selection.start;
+ if last_selection
+ .as_ref()
+ .map_or(true, |last_selection| selection.id > last_selection.id)
+ {
+ last_selection = Some(selection);
+ }
+ }
+ self.position = last_selection.map(|s| s.head().to_point(&buffer));
+
+ cx.notify();
+ }
+}
+
+impl Render for CursorPosition {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ div().when_some(self.position, |el, position| {
+ let mut text = format!(
+ "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+ position.row + 1,
+ position.column + 1
+ );
+ if self.selected_count > 0 {
+ write!(text, " ({} selected)", self.selected_count).unwrap();
+ }
+
+ el.child(Label::new(text))
+ })
+ }
+}
+
+impl StatusItemView for CursorPosition {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+ self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
+ self.update_position(editor, cx);
+ } else {
+ self.position = None;
+ self._observe_active_editor = None;
+ }
+
+ cx.notify();
+ }
+}
fn path_for_buffer<'a>(
buffer: &Model<MultiBuffer>,
@@ -608,671 +608,672 @@ fn go_to_fetched_definition_of_kind(
}
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::{
-// display_map::ToDisplayPoint,
-// editor_tests::init_test,
-// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
-// test::editor_lsp_test_context::EditorLspTestContext,
-// };
-// use futures::StreamExt;
-// use gpui::{
-// platform::{self, Modifiers, ModifiersChangedEvent},
-// View,
-// };
-// use indoc::indoc;
-// use language::language_settings::InlayHintSettings;
-// use lsp::request::{GotoDefinition, GotoTypeDefinition};
-// use util::assert_set_eq;
-
-// #[gpui::test]
-// async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-
-// let mut cx = EditorLspTestContext::new_rust(
-// lsp::ServerCapabilities {
-// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-// type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
-// ..Default::default()
-// },
-// cx,
-// )
-// .await;
-
-// cx.set_state(indoc! {"
-// struct A;
-// let vˇariable = A;
-// "});
-
-// // Basic hold cmd+shift, expect highlight in region if response contains type definition
-// let hover_point = cx.display_point(indoc! {"
-// struct A;
-// let vˇariable = A;
-// "});
-// let symbol_range = cx.lsp_range(indoc! {"
-// struct A;
-// let «variable» = A;
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// struct «A»;
-// let variable = A;
-// "});
-
-// let mut requests =
-// cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: Some(symbol_range),
-// target_uri: url.clone(),
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-
-// // Press cmd+shift to trigger highlight
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// true,
-// cx,
-// );
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// struct A;
-// let «variable» = A;
-// "});
-
-// // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
-// cx.update_editor(|editor, cx| {
-// editor.modifiers_changed(
-// &platform::ModifiersChangedEvent {
-// modifiers: Modifiers {
-// cmd: true,
-// ..Default::default()
-// },
-// ..Default::default()
-// },
-// cx,
-// );
-// });
-// // Assert no link highlights
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// struct A;
-// let variable = A;
-// "});
-
-// // Cmd+shift click without existing definition requests and jumps
-// let hover_point = cx.display_point(indoc! {"
-// struct A;
-// let vˇariable = A;
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// struct «A»;
-// let variable = A;
-// "});
-
-// let mut requests =
-// cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: None,
-// target_uri: url,
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-
-// cx.update_editor(|editor, cx| {
-// go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-
-// cx.assert_editor_state(indoc! {"
-// struct «Aˇ»;
-// let variable = A;
-// "});
-// }
-
-// #[gpui::test]
-// async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
-
-// let mut cx = EditorLspTestContext::new_rust(
-// lsp::ServerCapabilities {
-// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-// ..Default::default()
-// },
-// cx,
-// )
-// .await;
-
-// cx.set_state(indoc! {"
-// fn ˇtest() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Basic hold cmd, expect highlight in region if response contains definition
-// let hover_point = cx.display_point(indoc! {"
-// fn test() { do_wˇork(); }
-// fn do_work() { test(); }
-// "});
-// let symbol_range = cx.lsp_range(indoc! {"
-// fn test() { «do_work»(); }
-// fn do_work() { test(); }
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// fn test() { do_work(); }
-// fn «do_work»() { test(); }
-// "});
-
-// let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: Some(symbol_range),
-// target_uri: url.clone(),
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { «do_work»(); }
-// fn do_work() { test(); }
-// "});
-
-// // Unpress cmd causes highlight to go away
-// cx.update_editor(|editor, cx| {
-// editor.modifiers_changed(&Default::default(), cx);
-// });
-
-// // Assert no link highlights
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Response without source range still highlights word
-// cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
-// let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// // No origin range
-// origin_selection_range: None,
-// target_uri: url.clone(),
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { «do_work»(); }
-// fn do_work() { test(); }
-// "});
-
-// // Moving mouse to location with no response dismisses highlight
-// let hover_point = cx.display_point(indoc! {"
-// fˇn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-// let mut requests = cx
-// .lsp
-// .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
-// // No definitions returned
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
-// });
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-
-// // Assert no link highlights
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Move mouse without cmd and then pressing cmd triggers highlight
-// let hover_point = cx.display_point(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { teˇst(); }
-// "});
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// false,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-
-// // Assert no link highlights
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// let symbol_range = cx.lsp_range(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { «test»(); }
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// fn «test»() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: Some(symbol_range),
-// target_uri: url,
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-// cx.update_editor(|editor, cx| {
-// editor.modifiers_changed(
-// &ModifiersChangedEvent {
-// modifiers: Modifiers {
-// cmd: true,
-// ..Default::default()
-// },
-// },
-// cx,
-// );
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { «test»(); }
-// "});
-
-// // Deactivating the window dismisses the highlight
-// cx.update_workspace(|workspace, cx| {
-// workspace.on_window_activation_changed(false, cx);
-// });
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Moving the mouse restores the highlights.
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { «test»(); }
-// "});
-
-// // Moving again within the same symbol range doesn't re-request
-// let hover_point = cx.display_point(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { tesˇt(); }
-// "});
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { «test»(); }
-// "});
-
-// // Cmd click with existing definition doesn't re-request and dismisses highlight
-// cx.update_editor(|editor, cx| {
-// go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
-// });
-// // Assert selection moved to to definition
-// cx.lsp
-// .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
-// // Empty definition response to make sure we aren't hitting the lsp and using
-// // the cached location instead
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
-// });
-// cx.foreground().run_until_parked();
-// cx.assert_editor_state(indoc! {"
-// fn «testˇ»() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Assert no link highlights after jump
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-
-// // Cmd click without existing definition requests and jumps
-// let hover_point = cx.display_point(indoc! {"
-// fn test() { do_wˇork(); }
-// fn do_work() { test(); }
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// fn test() { do_work(); }
-// fn «do_work»() { test(); }
-// "});
-
-// let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: None,
-// target_uri: url,
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-// cx.update_editor(|editor, cx| {
-// go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
-// });
-// requests.next().await;
-// cx.foreground().run_until_parked();
-// cx.assert_editor_state(indoc! {"
-// fn test() { do_work(); }
-// fn «do_workˇ»() { test(); }
-// "});
-
-// // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
-// // 2. Selection is completed, hovering
-// let hover_point = cx.display_point(indoc! {"
-// fn test() { do_wˇork(); }
-// fn do_work() { test(); }
-// "});
-// let target_range = cx.lsp_range(indoc! {"
-// fn test() { do_work(); }
-// fn «do_work»() { test(); }
-// "});
-// let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
-// Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
-// lsp::LocationLink {
-// origin_selection_range: None,
-// target_uri: url,
-// target_range,
-// target_selection_range: target_range,
-// },
-// ])))
-// });
-
-// // create a pending selection
-// let selection_range = cx.ranges(indoc! {"
-// fn «test() { do_w»ork(); }
-// fn do_work() { test(); }
-// "})[0]
-// .clone();
-// cx.update_editor(|editor, cx| {
-// let snapshot = editor.buffer().read(cx).snapshot(cx);
-// let anchor_range = snapshot.anchor_before(selection_range.start)
-// ..snapshot.anchor_after(selection_range.end);
-// editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
-// s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
-// });
-// });
-// cx.update_editor(|editor, cx| {
-// update_go_to_definition_link(
-// editor,
-// Some(GoToDefinitionTrigger::Text(hover_point)),
-// true,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-// assert!(requests.try_next().is_err());
-// cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
-// fn test() { do_work(); }
-// fn do_work() { test(); }
-// "});
-// cx.foreground().run_until_parked();
-// }
-
-// #[gpui::test]
-// async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |settings| {
-// settings.defaults.inlay_hints = Some(InlayHintSettings {
-// enabled: true,
-// show_type_hints: true,
-// show_parameter_hints: true,
-// show_other_hints: true,
-// })
-// });
-
-// let mut cx = EditorLspTestContext::new_rust(
-// lsp::ServerCapabilities {
-// inlay_hint_provider: Some(lsp::OneOf::Left(true)),
-// ..Default::default()
-// },
-// cx,
-// )
-// .await;
-// cx.set_state(indoc! {"
-// struct TestStruct;
-
-// fn main() {
-// let variableˇ = TestStruct;
-// }
-// "});
-// let hint_start_offset = cx.ranges(indoc! {"
-// struct TestStruct;
-
-// fn main() {
-// let variableˇ = TestStruct;
-// }
-// "})[0]
-// .start;
-// let hint_position = cx.to_lsp(hint_start_offset);
-// let target_range = cx.lsp_range(indoc! {"
-// struct «TestStruct»;
-
-// fn main() {
-// let variable = TestStruct;
-// }
-// "});
-
-// let expected_uri = cx.buffer_lsp_url.clone();
-// let hint_label = ": TestStruct";
-// cx.lsp
-// .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
-// let expected_uri = expected_uri.clone();
-// async move {
-// assert_eq!(params.text_document.uri, expected_uri);
-// Ok(Some(vec![lsp::InlayHint {
-// position: hint_position,
-// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
-// value: hint_label.to_string(),
-// location: Some(lsp::Location {
-// uri: params.text_document.uri,
-// range: target_range,
-// }),
-// ..Default::default()
-// }]),
-// kind: Some(lsp::InlayHintKind::TYPE),
-// text_edits: None,
-// tooltip: None,
-// padding_left: Some(false),
-// padding_right: Some(false),
-// data: None,
-// }]))
-// }
-// })
-// .next()
-// .await;
-// cx.foreground().run_until_parked();
-// cx.update_editor(|editor, cx| {
-// let expected_layers = vec![hint_label.to_string()];
-// assert_eq!(expected_layers, cached_hint_labels(editor));
-// assert_eq!(expected_layers, visible_hint_labels(editor, cx));
-// });
-
-// let inlay_range = cx
-// .ranges(indoc! {"
-// struct TestStruct;
-
-// fn main() {
-// let variable« »= TestStruct;
-// }
-// "})
-// .get(0)
-// .cloned()
-// .unwrap();
-// let hint_hover_position = cx.update_editor(|editor, cx| {
-// let snapshot = editor.snapshot(cx);
-// let previous_valid = inlay_range.start.to_display_point(&snapshot);
-// let next_valid = inlay_range.end.to_display_point(&snapshot);
-// assert_eq!(previous_valid.row(), next_valid.row());
-// assert!(previous_valid.column() < next_valid.column());
-// let exact_unclipped = DisplayPoint::new(
-// previous_valid.row(),
-// previous_valid.column() + (hint_label.len() / 2) as u32,
-// );
-// PointForPosition {
-// previous_valid,
-// next_valid,
-// exact_unclipped,
-// column_overshoot_after_line_end: 0,
-// }
-// });
-// // Press cmd to trigger highlight
-// cx.update_editor(|editor, cx| {
-// update_inlay_link_and_hover_points(
-// &editor.snapshot(cx),
-// hint_hover_position,
-// editor,
-// true,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-// cx.update_editor(|editor, cx| {
-// let snapshot = editor.snapshot(cx);
-// let actual_highlights = snapshot
-// .inlay_highlights::<LinkGoToDefinitionState>()
-// .into_iter()
-// .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
-// .collect::<Vec<_>>();
-
-// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
-// let expected_highlight = InlayHighlight {
-// inlay: InlayId::Hint(0),
-// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
-// range: 0..hint_label.len(),
-// };
-// assert_set_eq!(actual_highlights, vec![&expected_highlight]);
-// });
-
-// // Unpress cmd causes highlight to go away
-// cx.update_editor(|editor, cx| {
-// editor.modifiers_changed(
-// &platform::ModifiersChangedEvent {
-// modifiers: Modifiers {
-// cmd: false,
-// ..Default::default()
-// },
-// ..Default::default()
-// },
-// cx,
-// );
-// });
-// // Assert no link highlights
-// cx.update_editor(|editor, cx| {
-// let snapshot = editor.snapshot(cx);
-// let actual_ranges = snapshot
-// .text_highlight_ranges::<LinkGoToDefinitionState>()
-// .map(|ranges| ranges.as_ref().clone().1)
-// .unwrap_or_default();
-
-// assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
-// });
-
-// // Cmd+click without existing definition requests and jumps
-// cx.update_editor(|editor, cx| {
-// editor.modifiers_changed(
-// &platform::ModifiersChangedEvent {
-// modifiers: Modifiers {
-// cmd: true,
-// ..Default::default()
-// },
-// ..Default::default()
-// },
-// cx,
-// );
-// update_inlay_link_and_hover_points(
-// &editor.snapshot(cx),
-// hint_hover_position,
-// editor,
-// true,
-// false,
-// cx,
-// );
-// });
-// cx.foreground().run_until_parked();
-// cx.update_editor(|editor, cx| {
-// go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
-// });
-// cx.foreground().run_until_parked();
-// cx.assert_editor_state(indoc! {"
-// struct «TestStructˇ»;
-
-// fn main() {
-// let variable = TestStruct;
-// }
-// "});
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::ToDisplayPoint,
+ editor_tests::init_test,
+ inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+ test::editor_lsp_test_context::EditorLspTestContext,
+ };
+ use futures::StreamExt;
+ use gpui::{Modifiers, ModifiersChangedEvent, View};
+ use indoc::indoc;
+ use language::language_settings::InlayHintSettings;
+ use lsp::request::{GotoDefinition, GotoTypeDefinition};
+ use util::assert_set_eq;
+
+ #[gpui::test]
+ async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ struct A;
+ let vˇariable = A;
+ "});
+
+ // Basic hold cmd+shift, expect highlight in region if response contains type definition
+ let hover_point = cx.display_point(indoc! {"
+ struct A;
+ let vˇariable = A;
+ "});
+ let symbol_range = cx.lsp_range(indoc! {"
+ struct A;
+ let «variable» = A;
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ struct «A»;
+ let variable = A;
+ "});
+
+ let mut requests =
+ cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ // Press cmd+shift to trigger highlight
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ true,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ struct A;
+ let «variable» = A;
+ "});
+
+ // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
+ cx.update_editor(|editor, cx| {
+ crate::element::EditorElement::modifiers_changed(
+ editor,
+ &ModifiersChangedEvent {
+ modifiers: Modifiers {
+ command: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ });
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ struct A;
+ let variable = A;
+ "});
+
+ // Cmd+shift click without existing definition requests and jumps
+ let hover_point = cx.display_point(indoc! {"
+ struct A;
+ let vˇariable = A;
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ struct «A»;
+ let variable = A;
+ "});
+
+ let mut requests =
+ cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+
+ cx.assert_editor_state(indoc! {"
+ struct «Aˇ»;
+ let variable = A;
+ "});
+ }
+
+ #[gpui::test]
+ async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ fn ˇtest() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Basic hold cmd, expect highlight in region if response contains definition
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wˇork(); }
+ fn do_work() { test(); }
+ "});
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { «do_work»(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn «do_work»() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { «do_work»(); }
+ fn do_work() { test(); }
+ "});
+
+ // Unpress cmd causes highlight to go away
+ cx.update_editor(|editor, cx| {
+ crate::element::EditorElement::modifiers_changed(editor, &Default::default(), cx);
+ });
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Response without source range still highlights word
+ cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ // No origin range
+ origin_selection_range: None,
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { «do_work»(); }
+ fn do_work() { test(); }
+ "});
+
+ // Moving mouse to location with no response dismisses highlight
+ let hover_point = cx.display_point(indoc! {"
+ fˇn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+ let mut requests = cx
+ .lsp
+ .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+ // No definitions returned
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Move mouse without cmd and then pressing cmd triggers highlight
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { teˇst(); }
+ "});
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ false,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { «test»(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn «test»() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ crate::element::EditorElement::modifiers_changed(
+ editor,
+ &ModifiersChangedEvent {
+ modifiers: Modifiers {
+ command: true,
+ ..Default::default()
+ },
+ },
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { «test»(); }
+ "});
+
+ // Deactivating the window dismisses the highlight
+ cx.update_workspace(|workspace, cx| {
+ workspace.on_window_activation_changed(cx);
+ });
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Moving the mouse restores the highlights.
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { «test»(); }
+ "});
+
+ // Moving again within the same symbol range doesn't re-request
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { tesˇt(); }
+ "});
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { «test»(); }
+ "});
+
+ // Cmd click with existing definition doesn't re-request and dismisses highlight
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ // Assert selection moved to to definition
+ cx.lsp
+ .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+ // Empty definition response to make sure we aren't hitting the lsp and using
+ // the cached location instead
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+ });
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ fn «testˇ»() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Assert no link highlights after jump
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Cmd click without existing definition requests and jumps
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wˇork(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn «do_work»() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ requests.next().await;
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ fn test() { do_work(); }
+ fn «do_workˇ»() { test(); }
+ "});
+
+ // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
+ // 2. Selection is completed, hovering
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wˇork(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn «do_work»() { test(); }
+ "});
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ // create a pending selection
+ let selection_range = cx.ranges(indoc! {"
+ fn «test() { do_w»ork(); }
+ fn do_work() { test(); }
+ "})[0]
+ .clone();
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let anchor_range = snapshot.anchor_before(selection_range.start)
+ ..snapshot.anchor_after(selection_range.end);
+ editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
+ s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
+ });
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+ assert!(requests.try_next().is_err());
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+ cx.background_executor.run_until_parked();
+ }
+
+ #[gpui::test]
+ async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+ cx.set_state(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variableˇ = TestStruct;
+ }
+ "});
+ let hint_start_offset = cx.ranges(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variableˇ = TestStruct;
+ }
+ "})[0]
+ .start;
+ let hint_position = cx.to_lsp(hint_start_offset);
+ let target_range = cx.lsp_range(indoc! {"
+ struct «TestStruct»;
+
+ fn main() {
+ let variable = TestStruct;
+ }
+ "});
+
+ let expected_uri = cx.buffer_lsp_url.clone();
+ let hint_label = ": TestStruct";
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let expected_uri = expected_uri.clone();
+ async move {
+ assert_eq!(params.text_document.uri, expected_uri);
+ Ok(Some(vec![lsp::InlayHint {
+ position: hint_position,
+ label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+ value: hint_label.to_string(),
+ location: Some(lsp::Location {
+ uri: params.text_document.uri,
+ range: target_range,
+ }),
+ ..Default::default()
+ }]),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: Some(false),
+ padding_right: Some(false),
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.background_executor.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let expected_layers = vec![hint_label.to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor));
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ });
+
+ let inlay_range = cx
+ .ranges(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variable« »= TestStruct;
+ }
+ "})
+ .get(0)
+ .cloned()
+ .unwrap();
+ let hint_hover_position = cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let previous_valid = inlay_range.start.to_display_point(&snapshot);
+ let next_valid = inlay_range.end.to_display_point(&snapshot);
+ assert_eq!(previous_valid.row(), next_valid.row());
+ assert!(previous_valid.column() < next_valid.column());
+ let exact_unclipped = DisplayPoint::new(
+ previous_valid.row(),
+ previous_valid.column() + (hint_label.len() / 2) as u32,
+ );
+ PointForPosition {
+ previous_valid,
+ next_valid,
+ exact_unclipped,
+ column_overshoot_after_line_end: 0,
+ }
+ });
+ // Press cmd to trigger highlight
+ cx.update_editor(|editor, cx| {
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ hint_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let actual_highlights = snapshot
+ .inlay_highlights::<LinkGoToDefinitionState>()
+ .into_iter()
+ .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
+ .collect::<Vec<_>>();
+
+ let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+ let expected_highlight = InlayHighlight {
+ inlay: InlayId::Hint(0),
+ inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+ range: 0..hint_label.len(),
+ };
+ assert_set_eq!(actual_highlights, vec![&expected_highlight]);
+ });
+
+ // Unpress cmd causes highlight to go away
+ cx.update_editor(|editor, cx| {
+ crate::element::EditorElement::modifiers_changed(
+ editor,
+ &ModifiersChangedEvent {
+ modifiers: Modifiers {
+ command: false,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ });
+ // Assert no link highlights
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let actual_ranges = snapshot
+ .text_highlight_ranges::<LinkGoToDefinitionState>()
+ .map(|ranges| ranges.as_ref().clone().1)
+ .unwrap_or_default();
+
+ assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+ });
+
+ // Cmd+click without existing definition requests and jumps
+ cx.update_editor(|editor, cx| {
+ crate::element::EditorElement::modifiers_changed(
+ editor,
+ &ModifiersChangedEvent {
+ modifiers: Modifiers {
+ command: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ hint_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.background_executor.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+ });
+ cx.background_executor.run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ struct «TestStructˇ»;
+
+ fn main() {
+ let variable = TestStruct;
+ }
+ "});
+ }
+}
@@ -1,5 +1,14 @@
-use crate::{DisplayPoint, Editor, EditorMode, SelectMode};
-use gpui::{Pixels, Point, ViewContext};
+use crate::{
+ DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+ Rename, RevealInFinder, SelectMode, ToggleCodeActions,
+};
+use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
+
+pub struct MouseContextMenu {
+ pub(crate) position: Point<Pixels>,
+ pub(crate) context_menu: View<ui::ContextMenu>,
+ _subscription: Subscription,
+}
pub fn deploy_context_menu(
editor: &mut Editor,
@@ -7,88 +16,95 @@ pub fn deploy_context_menu(
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
- todo!();
+ if !editor.is_focused(cx) {
+ editor.focus(cx);
+ }
- // if !editor.focused {
- // cx.focus_self();
- // }
+ // Don't show context menu for inline editors
+ if editor.mode() != EditorMode::Full {
+ return;
+ }
- // // Don't show context menu for inline editors
- // if editor.mode() != EditorMode::Full {
- // return;
- // }
+ // Don't show the context menu if there isn't a project associated with this editor
+ if editor.project.is_none() {
+ return;
+ }
- // // Don't show the context menu if there isn't a project associated with this editor
- // if editor.project.is_none() {
- // return;
- // }
+ // Move the cursor to the clicked location so that dispatched actions make sense
+ editor.change_selections(None, cx, |s| {
+ s.clear_disjoint();
+ s.set_pending_display_range(point..point, SelectMode::Character);
+ });
- // // Move the cursor to the clicked location so that dispatched actions make sense
- // editor.change_selections(None, cx, |s| {
- // s.clear_disjoint();
- // s.set_pending_display_range(point..point, SelectMode::Character);
- // });
+ let context_menu = ui::ContextMenu::build(cx, |menu, cx| {
+ menu.action("Rename Symbol", Box::new(Rename))
+ .action("Go to Definition", Box::new(GoToDefinition))
+ .action("Go to Type Definition", Box::new(GoToTypeDefinition))
+ .action("Find All References", Box::new(FindAllReferences))
+ .action(
+ "Code Actions",
+ Box::new(ToggleCodeActions {
+ deployed_from_indicator: false,
+ }),
+ )
+ .separator()
+ .action("Reveal in Finder", Box::new(RevealInFinder))
+ });
+ let context_menu_focus = context_menu.focus_handle(cx);
+ cx.focus(&context_menu_focus);
- // editor.mouse_context_menu.update(cx, |menu, cx| {
- // menu.show(
- // position,
- // AnchorCorner::TopLeft,
- // vec![
- // ContextMenuItem::action("Rename Symbol", Rename),
- // ContextMenuItem::action("Go to Definition", GoToDefinition),
- // ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
- // ContextMenuItem::action("Find All References", FindAllReferences),
- // ContextMenuItem::action(
- // "Code Actions",
- // ToggleCodeActions {
- // deployed_from_indicator: false,
- // },
- // ),
- // ContextMenuItem::Separator,
- // ContextMenuItem::action("Reveal in Finder", RevealInFinder),
- // ],
- // cx,
- // );
- // });
- // cx.notify();
+ let _subscription = cx.subscribe(&context_menu, move |this, _, event: &DismissEvent, cx| {
+ this.mouse_context_menu.take();
+ if context_menu_focus.contains_focused(cx) {
+ this.focus(cx);
+ }
+ });
+
+ editor.mouse_context_menu = Some(MouseContextMenu {
+ position,
+ context_menu,
+ _subscription,
+ });
+ cx.notify();
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
-// use indoc::indoc;
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+ use indoc::indoc;
-// #[gpui::test]
-// async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
-// init_test(cx, |_| {});
+ #[gpui::test]
+ async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
-// let mut cx = EditorLspTestContext::new_rust(
-// lsp::ServerCapabilities {
-// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
-// ..Default::default()
-// },
-// cx,
-// )
-// .await;
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
-// cx.set_state(indoc! {"
-// fn teˇst() {
-// do_work();
-// }
-// "});
-// let point = cx.display_point(indoc! {"
-// fn test() {
-// do_wˇork();
-// }
-// "});
-// cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
+ cx.set_state(indoc! {"
+ fn teˇst() {
+ do_work();
+ }
+ "});
+ let point = cx.display_point(indoc! {"
+ fn test() {
+ do_wˇork();
+ }
+ "});
+ cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_none()));
+ cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
-// cx.assert_editor_state(indoc! {"
-// fn test() {
-// do_wˇork();
-// }
-// "});
-// cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
-// }
-// }
+ cx.assert_editor_state(indoc! {"
+ fn test() {
+ do_wˇork();
+ }
+ "});
+ cx.editor(|editor, app| assert!(editor.mouse_context_menu.is_some()));
+ }
+}
@@ -452,483 +452,475 @@ pub fn split_display_range_by_lines(
result
}
-// #[cfg(test)]
-// mod tests {
-// use super::*;
-// use crate::{
-// display_map::Inlay,
-// test::{},
-// Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
-// };
-// use project::Project;
-// use settings::SettingsStore;
-// use util::post_inc;
-
-// #[gpui::test]
-// fn test_previous_word_start(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// previous_word_start(&snapshot, display_points[1]),
-// display_points[0]
-// );
-// }
-
-// assert("\nˇ ˇlorem", cx);
-// assert("ˇ\nˇ lorem", cx);
-// assert(" ˇloremˇ", cx);
-// assert("ˇ ˇlorem", cx);
-// assert(" ˇlorˇem", cx);
-// assert("\nlorem\nˇ ˇipsum", cx);
-// assert("\n\nˇ\nˇ", cx);
-// assert(" ˇlorem ˇipsum", cx);
-// assert("loremˇ-ˇipsum", cx);
-// assert("loremˇ-#$@ˇipsum", cx);
-// assert("ˇlorem_ˇipsum", cx);
-// assert(" ˇdefγˇ", cx);
-// assert(" ˇbcΔˇ", cx);
-// assert(" abˇ——ˇcd", cx);
-// }
-
-// #[gpui::test]
-// fn test_previous_subword_start(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// previous_subword_start(&snapshot, display_points[1]),
-// display_points[0]
-// );
-// }
-
-// // Subword boundaries are respected
-// assert("lorem_ˇipˇsum", cx);
-// assert("lorem_ˇipsumˇ", cx);
-// assert("ˇlorem_ˇipsum", cx);
-// assert("lorem_ˇipsum_ˇdolor", cx);
-// assert("loremˇIpˇsum", cx);
-// assert("loremˇIpsumˇ", cx);
-
-// // Word boundaries are still respected
-// assert("\nˇ ˇlorem", cx);
-// assert(" ˇloremˇ", cx);
-// assert(" ˇlorˇem", cx);
-// assert("\nlorem\nˇ ˇipsum", cx);
-// assert("\n\nˇ\nˇ", cx);
-// assert(" ˇlorem ˇipsum", cx);
-// assert("loremˇ-ˇipsum", cx);
-// assert("loremˇ-#$@ˇipsum", cx);
-// assert(" ˇdefγˇ", cx);
-// assert(" bcˇΔˇ", cx);
-// assert(" ˇbcδˇ", cx);
-// assert(" abˇ——ˇcd", cx);
-// }
-
-// #[gpui::test]
-// fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(
-// marked_text: &str,
-// cx: &mut gpui::AppContext,
-// is_boundary: impl FnMut(char, char) -> bool,
-// ) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// find_preceding_boundary(
-// &snapshot,
-// display_points[1],
-// FindRange::MultiLine,
-// is_boundary
-// ),
-// display_points[0]
-// );
-// }
-
-// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
-// left == 'c' && right == 'd'
-// });
-// assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
-// left == '\n' && right == 'g'
-// });
-// let mut line_count = 0;
-// assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
-// if left == '\n' {
-// line_count += 1;
-// line_count == 2
-// } else {
-// false
-// }
-// });
-// }
-
-// #[gpui::test]
-// fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// let input_text = "abcdefghijklmnopqrstuvwxys";
-// 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(input_text, cx);
-// let buffer_snapshot = buffer.read(cx).snapshot(cx);
-// let display_map =
-// cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
-
-// // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
-// let mut id = 0;
-// let inlays = (0..buffer_snapshot.len())
-// .map(|offset| {
-// [
-// Inlay {
-// id: InlayId::Suggestion(post_inc(&mut id)),
-// position: buffer_snapshot.anchor_at(offset, Bias::Left),
-// text: format!("test").into(),
-// },
-// Inlay {
-// id: InlayId::Suggestion(post_inc(&mut id)),
-// position: buffer_snapshot.anchor_at(offset, Bias::Right),
-// text: format!("test").into(),
-// },
-// Inlay {
-// id: InlayId::Hint(post_inc(&mut id)),
-// position: buffer_snapshot.anchor_at(offset, Bias::Left),
-// text: format!("test").into(),
-// },
-// Inlay {
-// id: InlayId::Hint(post_inc(&mut id)),
-// position: buffer_snapshot.anchor_at(offset, Bias::Right),
-// text: format!("test").into(),
-// },
-// ]
-// })
-// .flatten()
-// .collect();
-// let snapshot = display_map.update(cx, |map, cx| {
-// map.splice_inlays(Vec::new(), inlays, cx);
-// map.snapshot(cx)
-// });
-
-// assert_eq!(
-// find_preceding_boundary(
-// &snapshot,
-// buffer_snapshot.len().to_display_point(&snapshot),
-// FindRange::MultiLine,
-// |left, _| left == 'e',
-// ),
-// snapshot
-// .buffer_snapshot
-// .offset_to_point(5)
-// .to_display_point(&snapshot),
-// "Should not stop at inlays when looking for boundaries"
-// );
-// }
-
-// #[gpui::test]
-// fn test_next_word_end(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// next_word_end(&snapshot, display_points[0]),
-// display_points[1]
-// );
-// }
-
-// assert("\nˇ loremˇ", cx);
-// assert(" ˇloremˇ", cx);
-// assert(" lorˇemˇ", cx);
-// assert(" loremˇ ˇ\nipsum\n", cx);
-// assert("\nˇ\nˇ\n\n", cx);
-// assert("loremˇ ipsumˇ ", cx);
-// assert("loremˇ-ˇipsum", cx);
-// assert("loremˇ#$@-ˇipsum", cx);
-// assert("loremˇ_ipsumˇ", cx);
-// assert(" ˇbcΔˇ", cx);
-// assert(" abˇ——ˇcd", cx);
-// }
-
-// #[gpui::test]
-// fn test_next_subword_end(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// next_subword_end(&snapshot, display_points[0]),
-// display_points[1]
-// );
-// }
-
-// // Subword boundaries are respected
-// assert("loˇremˇ_ipsum", cx);
-// assert("ˇloremˇ_ipsum", cx);
-// assert("loremˇ_ipsumˇ", cx);
-// assert("loremˇ_ipsumˇ_dolor", cx);
-// assert("loˇremˇIpsum", cx);
-// assert("loremˇIpsumˇDolor", cx);
-
-// // Word boundaries are still respected
-// assert("\nˇ loremˇ", cx);
-// assert(" ˇloremˇ", cx);
-// assert(" lorˇemˇ", cx);
-// assert(" loremˇ ˇ\nipsum\n", cx);
-// assert("\nˇ\nˇ\n\n", cx);
-// assert("loremˇ ipsumˇ ", cx);
-// assert("loremˇ-ˇipsum", cx);
-// assert("loremˇ#$@-ˇipsum", cx);
-// assert("loremˇ_ipsumˇ", cx);
-// assert(" ˇbcˇΔ", cx);
-// assert(" abˇ——ˇcd", cx);
-// }
-
-// #[gpui::test]
-// fn test_find_boundary(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(
-// marked_text: &str,
-// cx: &mut gpui::AppContext,
-// is_boundary: impl FnMut(char, char) -> bool,
-// ) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// find_boundary(
-// &snapshot,
-// display_points[0],
-// FindRange::MultiLine,
-// is_boundary
-// ),
-// display_points[1]
-// );
-// }
-
-// assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
-// left == 'j' && right == 'k'
-// });
-// assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
-// left == '\n' && right == 'i'
-// });
-// let mut line_count = 0;
-// assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
-// if left == '\n' {
-// line_count += 1;
-// line_count == 2
-// } else {
-// false
-// }
-// });
-// }
-
-// #[gpui::test]
-// fn test_surrounding_word(cx: &mut gpui::AppContext) {
-// init_test(cx);
-
-// fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
-// let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
-// assert_eq!(
-// surrounding_word(&snapshot, display_points[1]),
-// display_points[0]..display_points[2],
-// "{}",
-// marked_text.to_string()
-// );
-// }
-
-// assert("ˇˇloremˇ ipsum", cx);
-// assert("ˇloˇremˇ ipsum", cx);
-// assert("ˇloremˇˇ ipsum", cx);
-// assert("loremˇ ˇ ˇipsum", cx);
-// assert("lorem\nˇˇˇ\nipsum", cx);
-// assert("lorem\nˇˇipsumˇ", cx);
-// assert("loremˇ,ˇˇ ipsum", cx);
-// assert("ˇloremˇˇ, ipsum", cx);
-// }
-
-// #[gpui::test]
-// async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
-// cx.update(|cx| {
-// init_test(cx);
-// });
-
-// let mut cx = EditorTestContext::new(cx).await;
-// let editor = cx.editor.clone();
-// let window = cx.window.clone();
-// cx.update_window(window, |cx| {
-// let text_layout_details =
-// editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
-
-// 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 buffer =
-// cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
-// let multibuffer = cx.add_model(|cx| {
-// let mut multibuffer = MultiBuffer::new(0);
-// multibuffer.push_excerpts(
-// buffer.clone(),
-// [
-// ExcerptRange {
-// context: Point::new(0, 0)..Point::new(1, 4),
-// primary: None,
-// },
-// ExcerptRange {
-// context: Point::new(2, 0)..Point::new(3, 2),
-// primary: None,
-// },
-// ],
-// cx,
-// );
-// multibuffer
-// });
-// let display_map =
-// cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
-// let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
-
-// assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
-
-// let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
-
-// // Can't move up into the first excerpt's header
-// assert_eq!(
-// up(
-// &snapshot,
-// DisplayPoint::new(2, 2),
-// SelectionGoal::HorizontalPosition(col_2_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(2, 0),
-// SelectionGoal::HorizontalPosition(0.0)
-// ),
-// );
-// assert_eq!(
-// up(
-// &snapshot,
-// DisplayPoint::new(2, 0),
-// SelectionGoal::None,
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(2, 0),
-// SelectionGoal::HorizontalPosition(0.0)
-// ),
-// );
-
-// let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
-
-// // Move up and down within first excerpt
-// assert_eq!(
-// up(
-// &snapshot,
-// DisplayPoint::new(3, 4),
-// SelectionGoal::HorizontalPosition(col_4_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(2, 3),
-// SelectionGoal::HorizontalPosition(col_4_x)
-// ),
-// );
-// assert_eq!(
-// down(
-// &snapshot,
-// DisplayPoint::new(2, 3),
-// SelectionGoal::HorizontalPosition(col_4_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(3, 4),
-// SelectionGoal::HorizontalPosition(col_4_x)
-// ),
-// );
-
-// let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
-
-// // Move up and down across second excerpt's header
-// assert_eq!(
-// up(
-// &snapshot,
-// DisplayPoint::new(6, 5),
-// SelectionGoal::HorizontalPosition(col_5_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(3, 4),
-// SelectionGoal::HorizontalPosition(col_5_x)
-// ),
-// );
-// assert_eq!(
-// down(
-// &snapshot,
-// DisplayPoint::new(3, 4),
-// SelectionGoal::HorizontalPosition(col_5_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(6, 5),
-// SelectionGoal::HorizontalPosition(col_5_x)
-// ),
-// );
-
-// let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
-
-// // Can't move down off the end
-// assert_eq!(
-// down(
-// &snapshot,
-// DisplayPoint::new(7, 0),
-// SelectionGoal::HorizontalPosition(0.0),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(7, 2),
-// SelectionGoal::HorizontalPosition(max_point_x)
-// ),
-// );
-// assert_eq!(
-// down(
-// &snapshot,
-// DisplayPoint::new(7, 2),
-// SelectionGoal::HorizontalPosition(max_point_x),
-// false,
-// &text_layout_details
-// ),
-// (
-// DisplayPoint::new(7, 2),
-// SelectionGoal::HorizontalPosition(max_point_x)
-// ),
-// );
-// });
-// }
-
-// fn init_test(cx: &mut gpui::AppContext) {
-// cx.set_global(SettingsStore::test(cx));
-// theme::init(cx);
-// language::init(cx);
-// crate::init(cx);
-// Project::init_settings(cx);
-// }
-// }
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::Inlay,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
+ };
+ use gpui::{font, Context as _};
+ use project::Project;
+ use settings::SettingsStore;
+ use util::post_inc;
+
+ #[gpui::test]
+ fn test_previous_word_start(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ previous_word_start(&snapshot, display_points[1]),
+ display_points[0]
+ );
+ }
+
+ assert("\nˇ ˇlorem", cx);
+ assert("ˇ\nˇ lorem", cx);
+ assert(" ˇloremˇ", cx);
+ assert("ˇ ˇlorem", cx);
+ assert(" ˇlorˇem", cx);
+ assert("\nlorem\nˇ ˇipsum", cx);
+ assert("\n\nˇ\nˇ", cx);
+ assert(" ˇlorem ˇipsum", cx);
+ assert("loremˇ-ˇipsum", cx);
+ assert("loremˇ-#$@ˇipsum", cx);
+ assert("ˇlorem_ˇipsum", cx);
+ assert(" ˇdefγˇ", cx);
+ assert(" ˇbcΔˇ", cx);
+ assert(" abˇ——ˇcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_previous_subword_start(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ previous_subword_start(&snapshot, display_points[1]),
+ display_points[0]
+ );
+ }
+
+ // Subword boundaries are respected
+ assert("lorem_ˇipˇsum", cx);
+ assert("lorem_ˇipsumˇ", cx);
+ assert("ˇlorem_ˇipsum", cx);
+ assert("lorem_ˇipsum_ˇdolor", cx);
+ assert("loremˇIpˇsum", cx);
+ assert("loremˇIpsumˇ", cx);
+
+ // Word boundaries are still respected
+ assert("\nˇ ˇlorem", cx);
+ assert(" ˇloremˇ", cx);
+ assert(" ˇlorˇem", cx);
+ assert("\nlorem\nˇ ˇipsum", cx);
+ assert("\n\nˇ\nˇ", cx);
+ assert(" ˇlorem ˇipsum", cx);
+ assert("loremˇ-ˇipsum", cx);
+ assert("loremˇ-#$@ˇipsum", cx);
+ assert(" ˇdefγˇ", cx);
+ assert(" bcˇΔˇ", cx);
+ assert(" ˇbcδˇ", cx);
+ assert(" abˇ——ˇcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(
+ marked_text: &str,
+ cx: &mut gpui::AppContext,
+ is_boundary: impl FnMut(char, char) -> bool,
+ ) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ find_preceding_boundary(
+ &snapshot,
+ display_points[1],
+ FindRange::MultiLine,
+ is_boundary
+ ),
+ display_points[0]
+ );
+ }
+
+ assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
+ left == 'c' && right == 'd'
+ });
+ assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
+ left == '\n' && right == 'g'
+ });
+ let mut line_count = 0;
+ assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
+ if left == '\n' {
+ line_count += 1;
+ line_count == 2
+ } else {
+ false
+ }
+ });
+ }
+
+ #[gpui::test]
+ fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ let input_text = "abcdefghijklmnopqrstuvwxys";
+ let font = font("Helvetica");
+ let font_size = px(14.0);
+ let buffer = MultiBuffer::build_simple(input_text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let display_map =
+ cx.build_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
+
+ // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+ let mut id = 0;
+ let inlays = (0..buffer_snapshot.len())
+ .map(|offset| {
+ [
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ ]
+ })
+ .flatten()
+ .collect();
+ let snapshot = display_map.update(cx, |map, cx| {
+ map.splice_inlays(Vec::new(), inlays, cx);
+ map.snapshot(cx)
+ });
+
+ assert_eq!(
+ find_preceding_boundary(
+ &snapshot,
+ buffer_snapshot.len().to_display_point(&snapshot),
+ FindRange::MultiLine,
+ |left, _| left == 'e',
+ ),
+ snapshot
+ .buffer_snapshot
+ .offset_to_point(5)
+ .to_display_point(&snapshot),
+ "Should not stop at inlays when looking for boundaries"
+ );
+ }
+
+ #[gpui::test]
+ fn test_next_word_end(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ next_word_end(&snapshot, display_points[0]),
+ display_points[1]
+ );
+ }
+
+ assert("\nˇ loremˇ", cx);
+ assert(" ˇloremˇ", cx);
+ assert(" lorˇemˇ", cx);
+ assert(" loremˇ ˇ\nipsum\n", cx);
+ assert("\nˇ\nˇ\n\n", cx);
+ assert("loremˇ ipsumˇ ", cx);
+ assert("loremˇ-ˇipsum", cx);
+ assert("loremˇ#$@-ˇipsum", cx);
+ assert("loremˇ_ipsumˇ", cx);
+ assert(" ˇbcΔˇ", cx);
+ assert(" abˇ——ˇcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_next_subword_end(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ next_subword_end(&snapshot, display_points[0]),
+ display_points[1]
+ );
+ }
+
+ // Subword boundaries are respected
+ assert("loˇremˇ_ipsum", cx);
+ assert("ˇloremˇ_ipsum", cx);
+ assert("loremˇ_ipsumˇ", cx);
+ assert("loremˇ_ipsumˇ_dolor", cx);
+ assert("loˇremˇIpsum", cx);
+ assert("loremˇIpsumˇDolor", cx);
+
+ // Word boundaries are still respected
+ assert("\nˇ loremˇ", cx);
+ assert(" ˇloremˇ", cx);
+ assert(" lorˇemˇ", cx);
+ assert(" loremˇ ˇ\nipsum\n", cx);
+ assert("\nˇ\nˇ\n\n", cx);
+ assert("loremˇ ipsumˇ ", cx);
+ assert("loremˇ-ˇipsum", cx);
+ assert("loremˇ#$@-ˇipsum", cx);
+ assert("loremˇ_ipsumˇ", cx);
+ assert(" ˇbcˇΔ", cx);
+ assert(" abˇ——ˇcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_find_boundary(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(
+ marked_text: &str,
+ cx: &mut gpui::AppContext,
+ is_boundary: impl FnMut(char, char) -> bool,
+ ) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ find_boundary(
+ &snapshot,
+ display_points[0],
+ FindRange::MultiLine,
+ is_boundary
+ ),
+ display_points[1]
+ );
+ }
+
+ assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
+ left == 'j' && right == 'k'
+ });
+ assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
+ left == '\n' && right == 'i'
+ });
+ let mut line_count = 0;
+ assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
+ if left == '\n' {
+ line_count += 1;
+ line_count == 2
+ } else {
+ false
+ }
+ });
+ }
+
+ #[gpui::test]
+ fn test_surrounding_word(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ surrounding_word(&snapshot, display_points[1]),
+ display_points[0]..display_points[2],
+ "{}",
+ marked_text.to_string()
+ );
+ }
+
+ assert("ˇˇloremˇ ipsum", cx);
+ assert("ˇloˇremˇ ipsum", cx);
+ assert("ˇloremˇˇ ipsum", cx);
+ assert("loremˇ ˇ ˇipsum", cx);
+ assert("lorem\nˇˇˇ\nipsum", cx);
+ assert("lorem\nˇˇipsumˇ", cx);
+ assert("loremˇ,ˇˇ ipsum", cx);
+ assert("ˇloremˇˇ, ipsum", cx);
+ }
+
+ #[gpui::test]
+ async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ init_test(cx);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
+ cx.update_window(window, |_, cx| {
+ let text_layout_details =
+ editor.update(cx, |editor, cx| editor.text_layout_details(cx));
+
+ let font = font("Helvetica");
+
+ let buffer = cx
+ .build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
+ let multibuffer = cx.build_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(2, 0)..Point::new(3, 2),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+ let display_map =
+ cx.build_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
+ let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+ let col_2_x =
+ snapshot.x_for_display_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+ // Can't move up into the first excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 2),
+ SelectionGoal::HorizontalPosition(col_2_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
+ );
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 0),
+ SelectionGoal::None,
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
+ );
+
+ let col_4_x =
+ snapshot.x_for_display_point(DisplayPoint::new(3, 4), &text_layout_details);
+
+ // Move up and down within first excerpt
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x.0)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x.0)
+ ),
+ );
+
+ let col_5_x =
+ snapshot.x_for_display_point(DisplayPoint::new(6, 5), &text_layout_details);
+
+ // Move up and down across second excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x.0)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x.0)
+ ),
+ );
+
+ let max_point_x =
+ snapshot.x_for_display_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+ // Can't move down off the end
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 0),
+ SelectionGoal::HorizontalPosition(0.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x.0)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x.0)
+ ),
+ );
+ });
+ }
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ language::init(cx);
+ crate::init(cx);
+ Project::init_settings(cx);
+ }
+}
@@ -315,14 +315,11 @@ impl SelectionsCollection {
let line = display_map.layout_row(row, &text_layout_details);
- dbg!("****START COL****");
let start_col = line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == line.width) {
let start = DisplayPoint::new(row, start_col);
- dbg!("****END COL****");
let end_col = 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),
@@ -1256,7 +1256,7 @@ mod tests {
//
// TODO: without closing, the opened items do not propagate their history changes for some reason
// it does work in real app though, only tests do not propagate.
- workspace.update(cx, |_, cx| dbg!(cx.focused()));
+ workspace.update(cx, |_, cx| cx.focused());
let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
assert!(
@@ -358,7 +358,7 @@ impl AppContext {
{
let entity_id = entity.entity_id();
let handle = entity.downgrade();
- self.observers.insert(
+ let (subscription, activate) = self.observers.insert(
entity_id,
Box::new(move |cx| {
if let Some(handle) = E::upgrade_from(&handle) {
@@ -367,7 +367,9 @@ impl AppContext {
false
}
}),
- )
+ );
+ self.defer(move |_| activate());
+ subscription
}
pub fn subscribe<T, E, Evt>(
@@ -398,8 +400,7 @@ impl AppContext {
{
let entity_id = entity.entity_id();
let entity = entity.downgrade();
-
- self.event_listeners.insert(
+ let (subscription, activate) = self.event_listeners.insert(
entity_id,
(
TypeId::of::<Evt>(),
@@ -412,7 +413,9 @@ impl AppContext {
}
}),
),
- )
+ );
+ self.defer(move |_| activate());
+ subscription
}
pub fn windows(&self) -> Vec<AnyWindowHandle> {
@@ -873,13 +876,15 @@ impl AppContext {
&mut self,
mut f: impl FnMut(&mut Self) + 'static,
) -> Subscription {
- self.global_observers.insert(
+ let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| {
f(cx);
true
}),
- )
+ );
+ self.defer(move |_| activate());
+ subscription
}
/// Move the global of the given type to the stack.
@@ -903,7 +908,7 @@ impl AppContext {
&mut self,
on_new: impl 'static + Fn(&mut V, &mut ViewContext<V>),
) -> Subscription {
- self.new_view_observers.insert(
+ let (subscription, activate) = self.new_view_observers.insert(
TypeId::of::<V>(),
Box::new(move |any_view: AnyView, cx: &mut WindowContext| {
any_view
@@ -913,7 +918,9 @@ impl AppContext {
on_new(view_state, cx);
})
}),
- )
+ );
+ activate();
+ subscription
}
pub fn observe_release<E, T>(
@@ -925,13 +932,15 @@ impl AppContext {
E: Entity<T>,
T: 'static,
{
- self.release_listeners.insert(
+ let (subscription, activate) = self.release_listeners.insert(
handle.entity_id(),
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
on_release(entity, cx)
}),
- )
+ );
+ activate();
+ subscription
}
pub(crate) fn push_text_style(&mut self, text_style: TextStyleRefinement) {
@@ -996,13 +1005,15 @@ impl AppContext {
where
Fut: 'static + Future<Output = ()>,
{
- self.quit_observers.insert(
+ let (subscription, activate) = self.quit_observers.insert(
(),
Box::new(move |cx| {
let future = on_quit(cx);
async move { future.await }.boxed_local()
}),
- )
+ );
+ activate();
+ subscription
}
}
@@ -482,10 +482,6 @@ impl<T: 'static> WeakModel<T> {
/// Update the entity referenced by this model with the given function if
/// the referenced entity still exists. Returns an error if the entity has
/// been released.
- ///
- /// The update function receives a context appropriate for its environment.
- /// When updating in an `AppContext`, it receives a `ModelContext`.
- /// When updating an a `WindowContext`, it receives a `ViewContext`.
pub fn update<C, R>(
&self,
cx: &mut C,
@@ -501,6 +497,21 @@ impl<T: 'static> WeakModel<T> {
.map(|this| cx.update_model(&this, update)),
)
}
+
+ /// Reads the entity referenced by this model with the given function if
+ /// the referenced entity still exists. Returns an error if the entity has
+ /// been released.
+ pub fn read_with<C, R>(&self, cx: &C, read: impl FnOnce(&T, &AppContext) -> R) -> Result<R>
+ where
+ C: Context,
+ Result<C::Result<R>>: crate::Flatten<R>,
+ {
+ crate::Flatten::flatten(
+ self.upgrade()
+ .ok_or_else(|| anyhow!("entity release"))
+ .map(|this| cx.read_model(&this, read)),
+ )
+ }
}
impl<T> Hash for WeakModel<T> {
@@ -88,13 +88,15 @@ impl<'a, T: 'static> ModelContext<'a, T> {
where
T: 'static,
{
- self.app.release_listeners.insert(
+ let (subscription, activate) = self.app.release_listeners.insert(
self.model_state.entity_id,
Box::new(move |this, cx| {
let this = this.downcast_mut().expect("invalid entity type");
on_release(this, cx);
}),
- )
+ );
+ activate();
+ subscription
}
pub fn observe_release<T2, E>(
@@ -109,7 +111,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
{
let entity_id = entity.entity_id();
let this = self.weak_model();
- self.app.release_listeners.insert(
+ let (subscription, activate) = self.app.release_listeners.insert(
entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
@@ -117,7 +119,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
this.update(cx, |this, cx| on_release(this, entity, cx));
}
}),
- )
+ );
+ activate();
+ subscription
}
pub fn observe_global<G: 'static>(
@@ -128,10 +132,12 @@ impl<'a, T: 'static> ModelContext<'a, T> {
T: 'static,
{
let handle = self.weak_model();
- self.global_observers.insert(
+ let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| handle.update(cx, |view, cx| f(view, cx)).is_ok()),
- )
+ );
+ self.defer(move |_| activate());
+ subscription
}
pub fn on_app_quit<Fut>(
@@ -143,7 +149,7 @@ impl<'a, T: 'static> ModelContext<'a, T> {
T: 'static,
{
let handle = self.weak_model();
- self.app.quit_observers.insert(
+ let (subscription, activate) = self.app.quit_observers.insert(
(),
Box::new(move |cx| {
let future = handle.update(cx, |entity, cx| on_quit(entity, cx)).ok();
@@ -154,7 +160,9 @@ impl<'a, T: 'static> ModelContext<'a, T> {
}
.boxed_local()
}),
- )
+ );
+ activate();
+ subscription
}
pub fn notify(&mut self) {
@@ -1,13 +1,13 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
- BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
- KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
- TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
- WindowOptions,
+ BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
+ KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result,
+ Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext,
+ VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
-use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration};
+use std::{future::Future, mem, ops::Deref, rc::Rc, sync::Arc, time::Duration};
#[derive(Clone)]
pub struct TestAppContext {
@@ -170,6 +170,45 @@ impl TestAppContext {
self.test_platform.has_pending_prompt()
}
+ pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size<Pixels>) {
+ let (mut handlers, scale_factor) = self
+ .app
+ .borrow_mut()
+ .update_window(window_handle, |_, cx| {
+ let platform_window = cx.window.platform_window.as_test().unwrap();
+ let scale_factor = platform_window.scale_factor();
+ match &mut platform_window.bounds {
+ WindowBounds::Fullscreen | WindowBounds::Maximized => {
+ platform_window.bounds = WindowBounds::Fixed(Bounds {
+ origin: Point::default(),
+ size: size.map(|pixels| f64::from(pixels).into()),
+ });
+ }
+ WindowBounds::Fixed(bounds) => {
+ bounds.size = size.map(|pixels| f64::from(pixels).into());
+ }
+ }
+
+ (
+ mem::take(&mut platform_window.handlers.lock().resize),
+ scale_factor,
+ )
+ })
+ .unwrap();
+
+ for handler in &mut handlers {
+ handler(size, scale_factor);
+ }
+
+ self.app
+ .borrow_mut()
+ .update_window(window_handle, |_, cx| {
+ let platform_window = cx.window.platform_window.as_test().unwrap();
+ platform_window.handlers.lock().resize = handlers;
+ })
+ .unwrap();
+ }
+
pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
where
Fut: Future<Output = R> + 'static,
@@ -343,12 +382,15 @@ impl TestAppContext {
use smol::future::FutureExt as _;
async {
- while notifications.next().await.is_some() {
+ loop {
if model.update(self, &mut predicate) {
return Ok(());
}
+
+ if notifications.next().await.is_none() {
+ bail!("model dropped")
+ }
}
- bail!("model dropped")
}
.race(timer.map(|_| Err(anyhow!("condition timed out"))))
.await
@@ -502,6 +544,19 @@ impl<'a> VisualTestContext<'a> {
self.cx.dispatch_action(self.window, action)
}
+ pub fn window_title(&mut self) -> Option<String> {
+ self.cx
+ .update_window(self.window, |_, cx| {
+ cx.window
+ .platform_window
+ .as_test()
+ .unwrap()
+ .window_title
+ .clone()
+ })
+ .unwrap()
+ }
+
pub fn simulate_keystrokes(&mut self, keystrokes: &str) {
self.cx.simulate_keystrokes(self.window, keystrokes)
}
@@ -509,6 +564,39 @@ impl<'a> VisualTestContext<'a> {
pub fn simulate_input(&mut self, input: &str) {
self.cx.simulate_input(self.window, input)
}
+
+ pub fn simulate_activation(&mut self) {
+ self.simulate_window_events(&mut |handlers| {
+ handlers
+ .active_status_change
+ .iter_mut()
+ .for_each(|f| f(true));
+ })
+ }
+
+ pub fn simulate_deactivation(&mut self) {
+ self.simulate_window_events(&mut |handlers| {
+ handlers
+ .active_status_change
+ .iter_mut()
+ .for_each(|f| f(false));
+ })
+ }
+
+ fn simulate_window_events(&mut self, f: &mut dyn FnMut(&mut TestWindowHandlers)) {
+ let handlers = self
+ .cx
+ .update_window(self.window, |_, cx| {
+ cx.window
+ .platform_window
+ .as_test()
+ .unwrap()
+ .handlers
+ .clone()
+ })
+ .unwrap();
+ f(&mut *handlers.lock());
+ }
}
impl<'a> Context for VisualTestContext<'a> {
@@ -0,0 +1,48 @@
+use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
+
+pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
+ Canvas {
+ paint_callback: Box::new(callback),
+ style: Default::default(),
+ }
+}
+
+pub struct Canvas {
+ paint_callback: Box<dyn FnOnce(Bounds<Pixels>, &mut WindowContext)>,
+ style: StyleRefinement,
+}
+
+impl IntoElement for Canvas {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for Canvas {
+ type State = ();
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (crate::LayoutId, Self::State) {
+ let layout_id = cx.request_layout(&self.style.clone().into(), []);
+ (layout_id, ())
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, _: &mut (), cx: &mut WindowContext) {
+ (self.paint_callback)(bounds, cx)
+ }
+}
+
+impl Styled for Canvas {
+ fn style(&mut self) -> &mut crate::StyleRefinement {
+ &mut self.style
+ }
+}
@@ -221,20 +221,6 @@ pub trait InteractiveElement: Sized + Element {
/// Add a listener for the given action, fires during the bubble event phase
fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
- // NOTE: this debug assert has the side-effect of working around
- // a bug where a crate consisting only of action definitions does
- // not register the actions in debug builds:
- //
- // https://github.com/rust-lang/rust/issues/47384
- // https://github.com/mmastrac/rust-ctor/issues/280
- //
- // if we are relying on this side-effect still, removing the debug_assert!
- // likely breaks the command_palette tests.
- // debug_assert!(
- // A::is_registered(),
- // "{:?} is not registered as an action",
- // A::qualified_name()
- // );
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
Box::new(move |action, phase, cx| {
@@ -247,6 +233,23 @@ pub trait InteractiveElement: Sized + Element {
self
}
+ fn on_boxed_action(
+ mut self,
+ action: &Box<dyn Action>,
+ listener: impl Fn(&Box<dyn Action>, &mut WindowContext) + 'static,
+ ) -> Self {
+ let action = action.boxed_clone();
+ self.interactivity().action_listeners.push((
+ (*action).type_id(),
+ Box::new(move |_, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ (listener)(&action, cx)
+ }
+ }),
+ ));
+ self
+ }
+
fn on_key_down(
mut self,
listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
@@ -866,6 +869,7 @@ impl Interactivity {
}
if self.hover_style.is_some()
+ || self.base_style.mouse_cursor.is_some()
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
{
let bounds = bounds.intersect(&cx.content_mask().bounds);
@@ -992,10 +996,6 @@ impl Interactivity {
let interactive_bounds = interactive_bounds.clone();
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
- if phase != DispatchPhase::Bubble {
- return;
- }
-
let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
&& pending_mouse_down.borrow().is_none();
if !is_hovered {
@@ -1003,6 +1003,10 @@ impl Interactivity {
return;
}
+ if phase != DispatchPhase::Bubble {
+ return;
+ }
+
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
@@ -1,3 +1,4 @@
+mod canvas;
mod div;
mod img;
mod overlay;
@@ -5,6 +6,7 @@ mod svg;
mod text;
mod uniform_list;
+pub use canvas::*;
pub use div::*;
pub use img::*;
pub use overlay::*;
@@ -1,6 +1,7 @@
use crate::{
- Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
- Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
+ Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
+ MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
+ WhiteSpace, WindowContext, WrappedLine,
};
use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
@@ -87,7 +88,28 @@ impl StyledText {
}
}
- pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
+ pub fn with_highlights(
+ mut self,
+ default_style: &TextStyle,
+ highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+ ) -> Self {
+ let mut runs = Vec::new();
+ let mut ix = 0;
+ for (range, highlight) in highlights {
+ if ix < range.start {
+ runs.push(default_style.clone().to_run(range.start - ix));
+ }
+ runs.push(
+ default_style
+ .clone()
+ .highlight(highlight)
+ .to_run(range.len()),
+ );
+ ix = range.end;
+ }
+ if ix < self.text.len() {
+ runs.push(default_style.to_run(self.text.len() - ix));
+ }
self.runs = Some(runs);
self
}
@@ -144,7 +166,6 @@ impl TextState {
runs: Option<Vec<TextRun>>,
cx: &mut WindowContext,
) -> LayoutId {
- let text_system = cx.text_system().clone();
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let line_height = text_style
@@ -152,18 +173,16 @@ impl TextState {
.to_pixels(font_size.into(), cx.rem_size());
let text = SharedString::from(text);
- let rem_size = cx.rem_size();
-
let runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
- let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
+ let layout_id = cx.request_measured_layout(Default::default(), {
let element_state = self.clone();
- move |known_dimensions, available_space| {
+ move |known_dimensions, available_space, cx| {
let wrap_width = if text_style.white_space == WhiteSpace::Normal {
known_dimensions.width.or(match available_space.width {
crate::AvailableSpace::Definite(x) => Some(x),
@@ -181,7 +200,8 @@ impl TextState {
}
}
- let Some(lines) = text_system
+ let Some(lines) = cx
+ .text_system()
.shape_text(
&text, font_size, &runs, wrap_width, // Wrap if we know the width.
)
@@ -109,7 +109,6 @@ impl Element for UniformList {
cx: &mut WindowContext,
) -> (LayoutId, Self::State) {
let max_items = self.item_count;
- let rem_size = cx.rem_size();
let item_size = state
.as_ref()
.map(|s| s.item_size)
@@ -120,9 +119,7 @@ impl Element for UniformList {
.layout(state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout(
style,
- rem_size,
- move |known_dimensions: Size<Option<Pixels>>,
- available_space: Size<AvailableSpace>| {
+ move |known_dimensions, available_space, _cx| {
let desired_height = item_size.height * max_items;
let width =
known_dimensions
@@ -128,11 +128,19 @@ impl BackgroundExecutor {
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn block_test<R>(&self, future: impl Future<Output = R>) -> R {
- self.block_internal(false, future)
+ if let Ok(value) = self.block_internal(false, future, usize::MAX) {
+ value
+ } else {
+ unreachable!()
+ }
}
pub fn block<R>(&self, future: impl Future<Output = R>) -> R {
- self.block_internal(true, future)
+ if let Ok(value) = self.block_internal(true, future, usize::MAX) {
+ value
+ } else {
+ unreachable!()
+ }
}
#[track_caller]
@@ -140,7 +148,8 @@ impl BackgroundExecutor {
&self,
background_only: bool,
future: impl Future<Output = R>,
- ) -> R {
+ mut max_ticks: usize,
+ ) -> Result<R, ()> {
pin_mut!(future);
let unparker = self.dispatcher.unparker();
let awoken = Arc::new(AtomicBool::new(false));
@@ -156,8 +165,13 @@ impl BackgroundExecutor {
loop {
match future.as_mut().poll(&mut cx) {
- Poll::Ready(result) => return result,
+ Poll::Ready(result) => return Ok(result),
Poll::Pending => {
+ if max_ticks == 0 {
+ return Err(());
+ }
+ max_ticks -= 1;
+
if !self.dispatcher.tick(background_only) {
if awoken.swap(false, SeqCst) {
continue;
@@ -192,16 +206,25 @@ impl BackgroundExecutor {
return Err(future);
}
+ #[cfg(any(test, feature = "test-support"))]
+ let max_ticks = self
+ .dispatcher
+ .as_test()
+ .map_or(usize::MAX, |dispatcher| dispatcher.gen_block_on_ticks());
+ #[cfg(not(any(test, feature = "test-support")))]
+ let max_ticks = usize::MAX;
+
let mut timer = self.timer(duration).fuse();
+
let timeout = async {
futures::select_biased! {
value = future => Ok(value),
_ = timer => Err(()),
}
};
- match self.block(timeout) {
- Ok(value) => Ok(value),
- Err(_) => Err(future),
+ match self.block_internal(true, timeout, max_ticks) {
+ Ok(Ok(value)) => Ok(value),
+ _ => Err(future),
}
}
@@ -281,6 +304,11 @@ impl BackgroundExecutor {
pub fn is_main_thread(&self) -> bool {
self.dispatcher.is_main_thread()
}
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+ self.dispatcher.as_test().unwrap().set_block_on_ticks(range);
+ }
}
impl ForegroundExecutor {
@@ -655,6 +655,20 @@ pub struct Corners<T: Clone + Default + Debug> {
pub bottom_left: T,
}
+impl<T> Corners<T>
+where
+ T: Clone + Default + Debug,
+{
+ pub fn all(value: T) -> Self {
+ Self {
+ top_left: value.clone(),
+ top_right: value.clone(),
+ bottom_right: value.clone(),
+ bottom_left: value,
+ }
+ }
+}
+
impl Corners<AbsoluteLength> {
pub fn to_pixels(&self, size: Size<Pixels>, rem_size: Pixels) -> Corners<Pixels> {
let max = size.width.max(size.height) / 2.;
@@ -21,7 +21,7 @@ mod subscription;
mod svg_renderer;
mod taffy;
#[cfg(any(test, feature = "test-support"))]
-mod test;
+pub mod test;
mod text_system;
mod util;
mod view;
@@ -16,7 +16,7 @@ pub struct DispatchNodeId(usize);
pub(crate) struct DispatchTree {
node_stack: Vec<DispatchNodeId>,
- context_stack: Vec<KeyContext>,
+ pub(crate) context_stack: Vec<KeyContext>,
nodes: Vec<DispatchNode>,
focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
@@ -163,11 +163,25 @@ impl DispatchTree {
actions
}
- pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
+ pub fn bindings_for_action(
+ &self,
+ action: &dyn Action,
+ context_stack: &Vec<KeyContext>,
+ ) -> Vec<KeyBinding> {
self.keymap
.lock()
.bindings_for_action(action.type_id())
- .filter(|candidate| candidate.action.partial_eq(action))
+ .filter(|candidate| {
+ if !candidate.action.partial_eq(action) {
+ return false;
+ }
+ for i in 1..context_stack.len() {
+ if candidate.matches_context(&context_stack[0..=i]) {
+ return true;
+ }
+ }
+ return false;
+ })
.cloned()
.collect()
}
@@ -44,7 +44,7 @@ pub(crate) fn current_platform() -> Rc<dyn Platform> {
Rc::new(MacPlatform::new())
}
-pub(crate) trait Platform: 'static {
+pub trait Platform: 'static {
fn background_executor(&self) -> BackgroundExecutor;
fn foreground_executor(&self) -> ForegroundExecutor;
fn text_system(&self) -> Arc<dyn PlatformTextSystem>;
@@ -128,7 +128,7 @@ impl Debug for DisplayId {
unsafe impl Send for DisplayId {}
-pub(crate) trait PlatformWindow {
+pub trait PlatformWindow {
fn bounds(&self) -> WindowBounds;
fn content_size(&self) -> Size<Pixels>;
fn scale_factor(&self) -> f32;
@@ -158,6 +158,11 @@ pub(crate) trait PlatformWindow {
fn draw(&self, scene: Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn as_test(&mut self) -> Option<&mut TestWindow> {
+ None
+ }
}
pub trait PlatformDispatcher: Send + Sync {
@@ -467,13 +472,27 @@ pub enum PromptLevel {
Critical,
}
+/// The style of the cursor (pointer)
#[derive(Copy, Clone, Debug)]
pub enum CursorStyle {
Arrow,
+ IBeam,
+ Crosshair,
+ ClosedHand,
+ OpenHand,
+ PointingHand,
+ ResizeLeft,
+ ResizeRight,
ResizeLeftRight,
+ ResizeUp,
+ ResizeDown,
ResizeUpDown,
- PointingHand,
- IBeam,
+ DisappearingItem,
+ IBeamCursorForVerticalLayout,
+ OperationNotAllowed,
+ DragLink,
+ DragCopy,
+ ContextualMenu,
}
impl Default for CursorStyle {
@@ -724,16 +724,35 @@ impl Platform for MacPlatform {
}
}
+ /// Match cursor style to one of the styles available
+ /// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
fn set_cursor_style(&self, style: CursorStyle) {
unsafe {
let new_cursor: id = match style {
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
- CursorStyle::ResizeLeftRight => {
- msg_send![class!(NSCursor), resizeLeftRightCursor]
- }
- CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
- CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
+ CursorStyle::Crosshair => msg_send![class!(NSCursor), crosshairCursor],
+ CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
+ CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
+ CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
+ CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
+ CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
+ CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
+ CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
+ CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
+ CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
+ CursorStyle::DisappearingItem => {
+ msg_send![class!(NSCursor), disappearingItemCursor]
+ }
+ CursorStyle::IBeamCursorForVerticalLayout => {
+ msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
+ }
+ CursorStyle::OperationNotAllowed => {
+ msg_send![class!(NSCursor), operationNotAllowedCursor]
+ }
+ CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
+ CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
+ CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
};
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
@@ -7,6 +7,7 @@ use parking_lot::Mutex;
use rand::prelude::*;
use std::{
future::Future,
+ ops::RangeInclusive,
pin::Pin,
sync::Arc,
task::{Context, Poll},
@@ -36,6 +37,7 @@ struct TestDispatcherState {
allow_parking: bool,
waiting_backtrace: Option<Backtrace>,
deprioritized_task_labels: HashSet<TaskLabel>,
+ block_on_ticks: RangeInclusive<usize>,
}
impl TestDispatcher {
@@ -53,6 +55,7 @@ impl TestDispatcher {
allow_parking: false,
waiting_backtrace: None,
deprioritized_task_labels: Default::default(),
+ block_on_ticks: 0..=1000,
};
TestDispatcher {
@@ -82,8 +85,8 @@ impl TestDispatcher {
}
pub fn simulate_random_delay(&self) -> impl 'static + Send + Future<Output = ()> {
- pub struct YieldNow {
- count: usize,
+ struct YieldNow {
+ pub(crate) count: usize,
}
impl Future for YieldNow {
@@ -142,6 +145,16 @@ impl TestDispatcher {
pub fn rng(&self) -> StdRng {
self.state.lock().random.clone()
}
+
+ pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive<usize>) {
+ self.state.lock().block_on_ticks = range;
+ }
+
+ pub fn gen_block_on_ticks(&self) -> usize {
+ let mut lock = self.state.lock();
+ let block_on_ticks = lock.block_on_ticks.clone();
+ lock.random.gen_range(block_on_ticks)
+ }
}
impl Clone for TestDispatcher {
@@ -1,6 +1,6 @@
use crate::{
- AnyWindowHandle, BackgroundExecutor, CursorStyle, DisplayId, ForegroundExecutor, Platform,
- PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
+ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor,
+ Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
};
use anyhow::{anyhow, Result};
use collections::VecDeque;
@@ -20,6 +20,7 @@ pub struct TestPlatform {
active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
active_display: Rc<dyn PlatformDisplay>,
active_cursor: Mutex<CursorStyle>,
+ current_clipboard_item: Mutex<Option<ClipboardItem>>,
pub(crate) prompts: RefCell<TestPrompts>,
weak: Weak<Self>,
}
@@ -39,6 +40,7 @@ impl TestPlatform {
active_cursor: Default::default(),
active_display: Rc::new(TestDisplay::new()),
active_window: Default::default(),
+ current_clipboard_item: Mutex::new(None),
weak: weak.clone(),
})
}
@@ -189,13 +191,9 @@ impl Platform for TestPlatform {
unimplemented!()
}
- fn on_become_active(&self, _callback: Box<dyn FnMut()>) {
- unimplemented!()
- }
+ fn on_become_active(&self, _callback: Box<dyn FnMut()>) {}
- fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {
- unimplemented!()
- }
+ fn on_resign_active(&self, _callback: Box<dyn FnMut()>) {}
fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
@@ -240,12 +238,12 @@ impl Platform for TestPlatform {
true
}
- fn write_to_clipboard(&self, _item: crate::ClipboardItem) {
- unimplemented!()
+ fn write_to_clipboard(&self, item: ClipboardItem) {
+ *self.current_clipboard_item.lock() = Some(item);
}
- fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
- unimplemented!()
+ fn read_from_clipboard(&self) -> Option<ClipboardItem> {
+ self.current_clipboard_item.lock().clone()
}
fn write_credentials(&self, _url: &str, _username: &str, _password: &[u8]) -> Result<()> {
@@ -11,19 +11,20 @@ use std::{
};
#[derive(Default)]
-struct Handlers {
- active_status_change: Vec<Box<dyn FnMut(bool)>>,
- input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
- moved: Vec<Box<dyn FnMut()>>,
- resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
+pub(crate) struct TestWindowHandlers {
+ pub(crate) active_status_change: Vec<Box<dyn FnMut(bool)>>,
+ pub(crate) input: Vec<Box<dyn FnMut(crate::InputEvent) -> bool>>,
+ pub(crate) moved: Vec<Box<dyn FnMut()>>,
+ pub(crate) resize: Vec<Box<dyn FnMut(Size<Pixels>, f32)>>,
}
pub struct TestWindow {
- bounds: WindowBounds,
+ pub(crate) bounds: WindowBounds,
current_scene: Mutex<Option<Scene>>,
display: Rc<dyn PlatformDisplay>,
+ pub(crate) window_title: Option<String>,
pub(crate) input_handler: Option<Arc<Mutex<Box<dyn PlatformInputHandler>>>>,
- handlers: Mutex<Handlers>,
+ pub(crate) handlers: Arc<Mutex<TestWindowHandlers>>,
platform: Weak<TestPlatform>,
sprite_atlas: Arc<dyn PlatformAtlas>,
}
@@ -42,6 +43,7 @@ impl TestWindow {
input_handler: None,
sprite_atlas: Arc::new(TestAtlas::new()),
handlers: Default::default(),
+ window_title: Default::default(),
}
}
}
@@ -100,8 +102,8 @@ impl PlatformWindow for TestWindow {
todo!()
}
- fn set_title(&mut self, _title: &str) {
- todo!()
+ fn set_title(&mut self, title: &str) {
+ self.window_title = Some(title.to_owned());
}
fn set_edited(&mut self, _edited: bool) {
@@ -167,6 +169,10 @@ impl PlatformWindow for TestWindow {
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {
self.sprite_atlas.clone()
}
+
+ fn as_test(&mut self) -> Option<&mut TestWindow> {
+ Some(self)
+ }
}
pub struct TestAtlasState {
@@ -198,7 +198,7 @@ impl SceneBuilder {
}
}
-pub(crate) struct Scene {
+pub struct Scene {
pub shadows: Vec<Shadow>,
pub quads: Vec<Quad>,
pub paths: Vec<Path<ScaledPixels>>,
@@ -214,7 +214,7 @@ impl Scene {
&self.paths
}
- pub fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
+ pub(crate) fn batches(&self) -> impl Iterator<Item = PrimitiveBatch> {
BatchIterator {
shadows: &self.shadows,
shadows_start: 0,
@@ -208,8 +208,9 @@ impl TextStyle {
}
}
+ /// Returns the rounded line height in pixels.
pub fn line_height_in_pixels(&self, rem_size: Pixels) -> Pixels {
- self.line_height.to_pixels(self.font_size, rem_size)
+ self.line_height.to_pixels(self.font_size, rem_size).round()
}
pub fn to_run(&self, len: usize) -> TextRun {
@@ -101,6 +101,125 @@ pub trait Styled: Sized {
self
}
+ /// Sets cursor style when hovering over an element to `text`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_text(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::IBeam);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `move`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_move(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `not-allowed`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_not_allowed(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `context-menu`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_context_menu(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ContextualMenu);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `crosshair`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_crosshair(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::Crosshair);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `vertical-text`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_vertical_text(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::IBeamCursorForVerticalLayout);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `alias`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_alias(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::DragLink);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `copy`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_copy(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::DragCopy);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `no-drop`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_no_drop(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::OperationNotAllowed);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `grab`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_grab(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::OpenHand);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `grabbing`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_grabbing(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ClosedHand);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `col-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_col_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeLeftRight);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `row-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_row_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeUpDown);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `n-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_n_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeUp);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `e-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_e_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeRight);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `s-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_s_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeDown);
+ self
+ }
+
+ /// Sets cursor style when hovering over an element to `w-resize`.
+ /// [Docs](https://tailwindcss.com/docs/cursor)
+ fn cursor_w_resize(mut self) -> Self {
+ self.style().mouse_cursor = Some(CursorStyle::ResizeLeft);
+ self
+ }
+
/// Sets the whitespace of the element to `normal`.
/// [Docs](https://tailwindcss.com/docs/whitespace#normal)
fn whitespace_normal(mut self) -> Self {
@@ -1,6 +1,6 @@
use collections::{BTreeMap, BTreeSet};
use parking_lot::Mutex;
-use std::{fmt::Debug, mem, sync::Arc};
+use std::{cell::Cell, fmt::Debug, mem, rc::Rc, sync::Arc};
use util::post_inc;
pub(crate) struct SubscriberSet<EmitterKey, Callback>(
@@ -14,11 +14,16 @@ impl<EmitterKey, Callback> Clone for SubscriberSet<EmitterKey, Callback> {
}
struct SubscriberSetState<EmitterKey, Callback> {
- subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Callback>>>,
+ subscribers: BTreeMap<EmitterKey, Option<BTreeMap<usize, Subscriber<Callback>>>>,
dropped_subscribers: BTreeSet<(EmitterKey, usize)>,
next_subscriber_id: usize,
}
+struct Subscriber<Callback> {
+ active: Rc<Cell<bool>>,
+ callback: Callback,
+}
+
impl<EmitterKey, Callback> SubscriberSet<EmitterKey, Callback>
where
EmitterKey: 'static + Ord + Clone + Debug,
@@ -32,16 +37,33 @@ where
})))
}
- pub fn insert(&self, emitter_key: EmitterKey, callback: Callback) -> Subscription {
+ /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions
+ /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`.
+ /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter
+ /// to activate the `[Subscription]`.
+ #[must_use]
+ pub fn insert(
+ &self,
+ emitter_key: EmitterKey,
+ callback: Callback,
+ ) -> (Subscription, impl FnOnce()) {
+ let active = Rc::new(Cell::new(false));
let mut lock = self.0.lock();
let subscriber_id = post_inc(&mut lock.next_subscriber_id);
lock.subscribers
.entry(emitter_key.clone())
.or_default()
.get_or_insert_with(|| Default::default())
- .insert(subscriber_id, callback);
+ .insert(
+ subscriber_id,
+ Subscriber {
+ active: active.clone(),
+ callback,
+ },
+ );
let this = self.0.clone();
- Subscription {
+
+ let subscription = Subscription {
unsubscribe: Some(Box::new(move || {
let mut lock = this.lock();
let Some(subscribers) = lock.subscribers.get_mut(&emitter_key) else {
@@ -63,7 +85,8 @@ where
lock.dropped_subscribers
.insert((emitter_key, subscriber_id));
})),
- }
+ };
+ (subscription, move || active.set(true))
}
pub fn remove(&self, emitter: &EmitterKey) -> impl IntoIterator<Item = Callback> {
@@ -73,6 +96,13 @@ where
.map(|s| s.into_values())
.into_iter()
.flatten()
+ .filter_map(|subscriber| {
+ if subscriber.active.get() {
+ Some(subscriber.callback)
+ } else {
+ None
+ }
+ })
}
/// Call the given callback for each subscriber to the given emitter.
@@ -91,7 +121,13 @@ where
return;
};
- subscribers.retain(|_, callback| f(callback));
+ subscribers.retain(|_, subscriber| {
+ if subscriber.active.get() {
+ f(&mut subscriber.callback)
+ } else {
+ true
+ }
+ });
let mut lock = self.0.lock();
// Add any new subscribers that were added while invoking the callback.
@@ -1,4 +1,7 @@
-use super::{AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style};
+use crate::{
+ AbsoluteLength, Bounds, DefiniteLength, Edges, Length, Pixels, Point, Size, Style,
+ WindowContext,
+};
use collections::{HashMap, HashSet};
use smallvec::SmallVec;
use std::fmt::Debug;
@@ -9,13 +12,21 @@ use taffy::{
Taffy,
};
-type Measureable = dyn Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync;
-
pub struct TaffyLayoutEngine {
- taffy: Taffy<Box<Measureable>>,
+ taffy: Taffy,
children_to_parents: HashMap<LayoutId, LayoutId>,
absolute_layout_bounds: HashMap<LayoutId, Bounds<Pixels>>,
computed_layouts: HashSet<LayoutId>,
+ nodes_to_measure: HashMap<
+ LayoutId,
+ Box<
+ dyn FnMut(
+ Size<Option<Pixels>>,
+ Size<AvailableSpace>,
+ &mut WindowContext,
+ ) -> Size<Pixels>,
+ >,
+ >,
}
static EXPECT_MESSAGE: &'static str =
@@ -28,6 +39,7 @@ impl TaffyLayoutEngine {
children_to_parents: HashMap::default(),
absolute_layout_bounds: HashMap::default(),
computed_layouts: HashSet::default(),
+ nodes_to_measure: HashMap::default(),
}
}
@@ -36,6 +48,7 @@ impl TaffyLayoutEngine {
self.children_to_parents.clear();
self.absolute_layout_bounds.clear();
self.computed_layouts.clear();
+ self.nodes_to_measure.clear();
}
pub fn request_layout(
@@ -65,18 +78,18 @@ impl TaffyLayoutEngine {
&mut self,
style: Style,
rem_size: Pixels,
- measure: impl Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels>
- + Send
- + Sync
+ measure: impl FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
+ 'static,
) -> LayoutId {
let style = style.to_taffy(rem_size);
- let measurable = Box::new(measure);
- self.taffy
- .new_leaf_with_context(style, measurable)
+ let layout_id = self
+ .taffy
+ .new_leaf_with_context(style, ())
.expect(EXPECT_MESSAGE)
- .into()
+ .into();
+ self.nodes_to_measure.insert(layout_id, Box::new(measure));
+ layout_id
}
// Used to understand performance
@@ -126,7 +139,12 @@ impl TaffyLayoutEngine {
Ok(edges)
}
- pub fn compute_layout(&mut self, id: LayoutId, available_space: Size<AvailableSpace>) {
+ pub fn compute_layout(
+ &mut self,
+ id: LayoutId,
+ available_space: Size<AvailableSpace>,
+ cx: &mut WindowContext,
+ ) {
// Leaving this here until we have a better instrumentation approach.
// println!("Laying out {} children", self.count_all_children(id)?);
// println!("Max layout depth: {}", self.max_depth(0, id)?);
@@ -159,8 +177,8 @@ impl TaffyLayoutEngine {
.compute_layout_with_measure(
id.into(),
available_space.into(),
- |known_dimensions, available_space, _node_id, context| {
- let Some(measure) = context else {
+ |known_dimensions, available_space, node_id, _context| {
+ let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else {
return taffy::geometry::Size::default();
};
@@ -169,10 +187,11 @@ impl TaffyLayoutEngine {
height: known_dimensions.height.map(Pixels),
};
- measure(known_dimensions, available_space.into()).into()
+ measure(known_dimensions, available_space.into(), cx).into()
},
)
.expect(EXPECT_MESSAGE);
+
// println!("compute_layout took {:?}", started_at.elapsed());
}
@@ -1,5 +1,7 @@
-use crate::TestDispatcher;
+use crate::{Entity, Subscription, TestAppContext, TestDispatcher};
+use futures::StreamExt as _;
use rand::prelude::*;
+use smol::channel;
use std::{
env,
panic::{self, RefUnwindSafe},
@@ -49,3 +51,30 @@ pub fn run_test(
}
}
}
+
+pub struct Observation<T> {
+ rx: channel::Receiver<T>,
+ _subscription: Subscription,
+}
+
+impl<T: 'static> futures::Stream for Observation<T> {
+ type Item = T;
+
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Option<Self::Item>> {
+ self.rx.poll_next_unpin(cx)
+ }
+}
+
+pub fn observe<T: 'static>(entity: &impl Entity<T>, cx: &mut TestAppContext) -> Observation<()> {
+ let (tx, rx) = smol::channel::unbounded();
+ let _subscription = cx.update(|cx| {
+ cx.observe(entity, move |_, _| {
+ let _ = smol::block_on(tx.send(()));
+ })
+ });
+
+ Observation { rx, _subscription }
+}
@@ -72,7 +72,7 @@ impl TextSystem {
}
}
- pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Result<Bounds<Pixels>> {
+ pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
}
@@ -89,9 +89,9 @@ impl TextSystem {
let bounds = self
.platform_text_system
.typographic_bounds(font_id, glyph_id)?;
- self.read_metrics(font_id, |metrics| {
+ Ok(self.read_metrics(font_id, |metrics| {
(bounds / metrics.units_per_em as f32 * font_size.0).map(px)
- })
+ }))
}
pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
@@ -100,28 +100,28 @@ impl TextSystem {
.glyph_for_char(font_id, ch)
.ok_or_else(|| anyhow!("glyph not found for character '{}'", ch))?;
let result = self.platform_text_system.advance(font_id, glyph_id)?
- / self.units_per_em(font_id)? as f32;
+ / self.units_per_em(font_id) as f32;
Ok(result * font_size)
}
- pub fn units_per_em(&self, font_id: FontId) -> Result<u32> {
+ pub fn units_per_em(&self, font_id: FontId) -> u32 {
self.read_metrics(font_id, |metrics| metrics.units_per_em as u32)
}
- pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
}
- pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
}
- pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
}
- pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
+ pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
self.read_metrics(font_id, |metrics| metrics.descent(font_size))
}
@@ -130,24 +130,24 @@ impl TextSystem {
font_id: FontId,
font_size: Pixels,
line_height: Pixels,
- ) -> Result<Pixels> {
- let ascent = self.ascent(font_id, font_size)?;
- let descent = self.descent(font_id, font_size)?;
+ ) -> Pixels {
+ let ascent = self.ascent(font_id, font_size);
+ let descent = self.descent(font_id, font_size);
let padding_top = (line_height - ascent - descent) / 2.;
- Ok(padding_top + ascent)
+ padding_top + ascent
}
- fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> Result<T> {
+ fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
let lock = self.font_metrics.upgradable_read();
if let Some(metrics) = lock.get(&font_id) {
- Ok(read(metrics))
+ read(metrics)
} else {
let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
let metrics = lock
.entry(font_id)
.or_insert_with(|| self.platform_text_system.font_metrics(font_id));
- Ok(read(metrics))
+ read(metrics)
}
}
@@ -101,9 +101,7 @@ fn paint_line(
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
for (run_ix, run) in layout.runs.iter().enumerate() {
- let max_glyph_size = text_system
- .bounding_box(run.font_id, layout.font_size)?
- .size;
+ let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
@@ -209,9 +209,7 @@ impl AnyView {
) {
cx.with_absolute_element_offset(origin, |cx| {
let (layout_id, rendered_element) = (self.layout)(self, cx);
- cx.window
- .layout_engine
- .compute_layout(layout_id, available_space);
+ cx.compute_layout(layout_id, available_space);
(self.paint)(self, rendered_element, cx);
})
}
@@ -240,6 +238,10 @@ impl Element for AnyView {
}
fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ debug_assert!(
+ state.is_some(),
+ "state is None. Did you include an AnyView twice in the tree?"
+ );
(self.paint)(&self, state.take().unwrap(), cx)
}
}
@@ -209,7 +209,7 @@ pub struct Window {
sprite_atlas: Arc<dyn PlatformAtlas>,
rem_size: Pixels,
viewport_size: Size<Pixels>,
- pub(crate) layout_engine: TaffyLayoutEngine,
+ layout_engine: Option<TaffyLayoutEngine>,
pub(crate) root_view: Option<AnyView>,
pub(crate) element_id_stack: GlobalElementId,
pub(crate) previous_frame: Frame,
@@ -327,7 +327,7 @@ impl Window {
sprite_atlas,
rem_size: px(16.),
viewport_size: content_size,
- layout_engine: TaffyLayoutEngine::new(),
+ layout_engine: Some(TaffyLayoutEngine::new()),
root_view: None,
element_id_stack: GlobalElementId::default(),
previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
@@ -490,7 +490,7 @@ impl<'a> WindowContext<'a> {
let entity_id = entity.entity_id();
let entity = entity.downgrade();
let window_handle = self.window.handle;
- self.app.event_listeners.insert(
+ let (subscription, activate) = self.app.event_listeners.insert(
entity_id,
(
TypeId::of::<Evt>(),
@@ -508,7 +508,9 @@ impl<'a> WindowContext<'a> {
.unwrap_or(false)
}),
),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
/// Create an `AsyncWindowContext`, which has a static lifetime and can be held across
@@ -606,9 +608,11 @@ impl<'a> WindowContext<'a> {
self.app.layout_id_buffer.extend(children.into_iter());
let rem_size = self.rem_size();
- self.window
- .layout_engine
- .request_layout(style, rem_size, &self.app.layout_id_buffer)
+ self.window.layout_engine.as_mut().unwrap().request_layout(
+ style,
+ rem_size,
+ &self.app.layout_id_buffer,
+ )
}
/// Add a node to the layout tree for the current frame. Instead of taking a `Style` and children,
@@ -618,22 +622,25 @@ impl<'a> WindowContext<'a> {
/// The given closure is invoked at layout time with the known dimensions and available space and
/// returns a `Size`.
pub fn request_measured_layout<
- F: Fn(Size<Option<Pixels>>, Size<AvailableSpace>) -> Size<Pixels> + Send + Sync + 'static,
+ F: FnMut(Size<Option<Pixels>>, Size<AvailableSpace>, &mut WindowContext) -> Size<Pixels>
+ + 'static,
>(
&mut self,
style: Style,
- rem_size: Pixels,
measure: F,
) -> LayoutId {
+ let rem_size = self.rem_size();
self.window
.layout_engine
+ .as_mut()
+ .unwrap()
.request_measured_layout(style, rem_size, measure)
}
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
- self.window
- .layout_engine
- .compute_layout(layout_id, available_space)
+ let mut layout_engine = self.window.layout_engine.take().unwrap();
+ layout_engine.compute_layout(layout_id, available_space, self);
+ self.window.layout_engine = Some(layout_engine);
}
/// Obtain the bounds computed for the given LayoutId relative to the window. This method should not
@@ -643,6 +650,8 @@ impl<'a> WindowContext<'a> {
let mut bounds = self
.window
.layout_engine
+ .as_mut()
+ .unwrap()
.layout_bounds(layout_id)
.map(Into::into);
bounds.origin += self.element_offset();
@@ -678,6 +687,10 @@ impl<'a> WindowContext<'a> {
self.window.platform_window.zoom();
}
+ pub fn set_window_title(&mut self, title: &str) {
+ self.window.platform_window.set_title(title);
+ }
+
pub fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.platform
.displays()
@@ -1189,7 +1202,7 @@ impl<'a> WindowContext<'a> {
self.text_system().start_frame();
let window = &mut *self.window;
- window.layout_engine.clear();
+ window.layout_engine.as_mut().unwrap().clear();
mem::swap(&mut window.previous_frame, &mut window.current_frame);
let frame = &mut window.current_frame;
@@ -1337,6 +1350,8 @@ impl<'a> WindowContext<'a> {
.dispatch_tree
.dispatch_path(node_id);
+ let mut actions: Vec<Box<dyn Action>> = Vec::new();
+
// Capture phase
let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
self.propagate_event = true;
@@ -1371,22 +1386,26 @@ impl<'a> WindowContext<'a> {
let node = self.window.current_frame.dispatch_tree.node(*node_id);
if !node.context.is_empty() {
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
- if let Some(action) = self
+ if let Some(found) = self
.window
.current_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &context_stack)
{
- self.dispatch_action_on_node(*node_id, action);
- if !self.propagate_event {
- return;
- }
+ actions.push(found.boxed_clone())
}
}
context_stack.pop();
}
}
+
+ for action in actions {
+ self.dispatch_action_on_node(node_id, action);
+ if !self.propagate_event {
+ return;
+ }
+ }
}
}
@@ -1414,7 +1433,6 @@ impl<'a> WindowContext<'a> {
}
}
}
-
// Bubble phase
for node_id in dispatch_path.iter().rev() {
let node = self.window.current_frame.dispatch_tree.node(*node_id);
@@ -1442,10 +1460,12 @@ impl<'a> WindowContext<'a> {
f: impl Fn(&mut WindowContext<'_>) + 'static,
) -> Subscription {
let window_handle = self.window.handle;
- self.global_observers.insert(
+ let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| window_handle.update(cx, |_, cx| f(cx)).is_ok()),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
pub fn activate_window(&self) {
@@ -1482,9 +1502,30 @@ impl<'a> WindowContext<'a> {
pub fn bindings_for_action(&self, action: &dyn Action) -> Vec<KeyBinding> {
self.window
- .current_frame
+ .previous_frame
.dispatch_tree
- .bindings_for_action(action)
+ .bindings_for_action(
+ action,
+ &self.window.previous_frame.dispatch_tree.context_stack,
+ )
+ }
+
+ pub fn bindings_for_action_in(
+ &self,
+ action: &dyn Action,
+ focus_handle: &FocusHandle,
+ ) -> Vec<KeyBinding> {
+ let dispatch_tree = &self.window.previous_frame.dispatch_tree;
+
+ let Some(node_id) = dispatch_tree.focusable_node_id(focus_handle.id) else {
+ return vec![];
+ };
+ let context_stack = dispatch_tree
+ .dispatch_path(node_id)
+ .into_iter()
+ .map(|node_id| dispatch_tree.node(node_id).context.clone())
+ .collect();
+ dispatch_tree.bindings_for_action(action, &context_stack)
}
pub fn listener_for<V: Render, E>(
@@ -2085,7 +2126,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
let entity_id = entity.entity_id();
let entity = entity.downgrade();
let window_handle = self.window.handle;
- self.app.observers.insert(
+ let (subscription, activate) = self.app.observers.insert(
entity_id,
Box::new(move |cx| {
window_handle
@@ -2099,7 +2140,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
})
.unwrap_or(false)
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
pub fn subscribe<V2, E, Evt>(
@@ -2116,7 +2159,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
let entity_id = entity.entity_id();
let handle = entity.downgrade();
let window_handle = self.window.handle;
- self.app.event_listeners.insert(
+ let (subscription, activate) = self.app.event_listeners.insert(
entity_id,
(
TypeId::of::<Evt>(),
@@ -2134,7 +2177,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
.unwrap_or(false)
}),
),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
pub fn on_release(
@@ -2142,13 +2187,15 @@ impl<'a, V: 'static> ViewContext<'a, V> {
on_release: impl FnOnce(&mut V, &mut WindowContext) + 'static,
) -> Subscription {
let window_handle = self.window.handle;
- self.app.release_listeners.insert(
+ let (subscription, activate) = self.app.release_listeners.insert(
self.view.model.entity_id,
Box::new(move |this, cx| {
let this = this.downcast_mut().expect("invalid entity type");
let _ = window_handle.update(cx, |_, cx| on_release(this, cx));
}),
- )
+ );
+ activate();
+ subscription
}
pub fn observe_release<V2, E>(
@@ -2164,7 +2211,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
let view = self.view().downgrade();
let entity_id = entity.entity_id();
let window_handle = self.window.handle;
- self.app.release_listeners.insert(
+ let (subscription, activate) = self.app.release_listeners.insert(
entity_id,
Box::new(move |entity, cx| {
let entity = entity.downcast_mut().expect("invalid entity type");
@@ -2172,7 +2219,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
view.update(cx, |this, cx| on_release(this, entity, cx))
});
}),
- )
+ );
+ activate();
+ subscription
}
pub fn notify(&mut self) {
@@ -2187,10 +2236,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
- self.window.bounds_observers.insert(
+ let (subscription, activate) = self.window.bounds_observers.insert(
(),
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
- )
+ );
+ activate();
+ subscription
}
pub fn observe_window_activation(
@@ -2198,10 +2249,12 @@ impl<'a, V: 'static> ViewContext<'a, V> {
mut callback: impl FnMut(&mut V, &mut ViewContext<V>) + 'static,
) -> Subscription {
let view = self.view.downgrade();
- self.window.activation_observers.insert(
+ let (subscription, activate) = self.window.activation_observers.insert(
(),
Box::new(move |cx| view.update(cx, |view, cx| callback(view, cx)).is_ok()),
- )
+ );
+ activate();
+ subscription
}
/// Register a listener to be called when the given focus handle receives focus.
@@ -2214,7 +2267,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) -> Subscription {
let view = self.view.downgrade();
let focus_id = handle.id;
- self.window.focus_listeners.insert(
+ let (subscription, activate) = self.window.focus_listeners.insert(
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
@@ -2224,7 +2277,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
})
.is_ok()
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
/// Register a listener to be called when the given focus handle or one of its descendants receives focus.
@@ -2237,7 +2292,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) -> Subscription {
let view = self.view.downgrade();
let focus_id = handle.id;
- self.window.focus_listeners.insert(
+ let (subscription, activate) = self.window.focus_listeners.insert(
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
@@ -2251,7 +2306,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
})
.is_ok()
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
/// Register a listener to be called when the given focus handle loses focus.
@@ -2264,7 +2321,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) -> Subscription {
let view = self.view.downgrade();
let focus_id = handle.id;
- self.window.focus_listeners.insert(
+ let (subscription, activate) = self.window.focus_listeners.insert(
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
@@ -2274,7 +2331,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
})
.is_ok()
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
@@ -2287,7 +2346,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) -> Subscription {
let view = self.view.downgrade();
let focus_id = handle.id;
- self.window.focus_listeners.insert(
+ let (subscription, activate) = self.window.focus_listeners.insert(
(),
Box::new(move |event, cx| {
view.update(cx, |view, cx| {
@@ -2301,7 +2360,9 @@ impl<'a, V: 'static> ViewContext<'a, V> {
})
.is_ok()
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
pub fn spawn<Fut, R>(
@@ -2332,14 +2393,16 @@ impl<'a, V: 'static> ViewContext<'a, V> {
) -> Subscription {
let window_handle = self.window.handle;
let view = self.view().downgrade();
- self.global_observers.insert(
+ let (subscription, activate) = self.global_observers.insert(
TypeId::of::<G>(),
Box::new(move |cx| {
window_handle
.update(cx, |_, cx| view.update(cx, |view, cx| f(view, cx)).is_ok())
.unwrap_or(false)
}),
- )
+ );
+ self.app.defer(move |_| activate());
+ subscription
}
pub fn on_mouse_event<Event: 'static>(
@@ -2697,6 +2760,7 @@ pub enum ElementId {
Integer(usize),
Name(SharedString),
FocusHandle(FocusId),
+ NamedInteger(SharedString, usize),
}
impl ElementId {
@@ -2746,3 +2810,9 @@ impl<'a> From<&'a FocusHandle> for ElementId {
ElementId::FocusHandle(handle.id)
}
}
+
+impl From<(&'static str, EntityId)> for ElementId {
+ fn from((name, id): (&'static str, EntityId)) -> Self {
+ ElementId::NamedInteger(name.into(), id.as_u64() as usize)
+ }
+}
@@ -81,6 +81,7 @@ impl<T> Outline<T> {
let mut prev_item_ix = 0;
for mut string_match in matches {
let outline_match = &self.items[string_match.candidate_id];
+ string_match.string = outline_match.text.clone();
if is_path_query {
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
@@ -0,0 +1,26 @@
+[package]
+name = "language_selector2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/language_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+settings = { package = "settings2", path = "../settings2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -0,0 +1,82 @@
+use editor::Editor;
+use gpui::{
+ div, Div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView,
+};
+use std::sync::Arc;
+use ui::{Button, ButtonCommon, Clickable, Tooltip};
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+use crate::LanguageSelector;
+
+pub struct ActiveBufferLanguage {
+ active_language: Option<Option<Arc<str>>>,
+ workspace: WeakView<Workspace>,
+ _observe_active_editor: Option<Subscription>,
+}
+
+impl ActiveBufferLanguage {
+ pub fn new(workspace: &Workspace) -> Self {
+ Self {
+ active_language: None,
+ workspace: workspace.weak_handle(),
+ _observe_active_editor: None,
+ }
+ }
+
+ fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ self.active_language = Some(None);
+
+ let editor = editor.read(cx);
+ if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+ if let Some(language) = buffer.read(cx).language() {
+ self.active_language = Some(Some(language.name()));
+ }
+ }
+
+ cx.notify();
+ }
+}
+
+impl Render for ActiveBufferLanguage {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
+ div().when_some(self.active_language.as_ref(), |el, active_language| {
+ let active_language_text = if let Some(active_language_text) = active_language {
+ active_language_text.to_string()
+ } else {
+ "Unknown".to_string()
+ };
+
+ el.child(
+ Button::new("change-language", active_language_text)
+ .on_click(cx.listener(|this, _, cx| {
+ if let Some(workspace) = this.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ LanguageSelector::toggle(workspace, cx)
+ });
+ }
+ }))
+ .tooltip(|cx| Tooltip::text("Select Language", cx)),
+ )
+ })
+ }
+}
+
+impl StatusItemView for ActiveBufferLanguage {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+ self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
+ self.update_language(editor, cx);
+ } else {
+ self.active_language = None;
+ self._observe_active_editor = None;
+ }
+
+ cx.notify();
+ }
+}
@@ -0,0 +1,232 @@
+mod active_buffer_language;
+
+pub use active_buffer_language::ActiveBufferLanguage;
+use anyhow::anyhow;
+use editor::Editor;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+ actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+ ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+};
+use language::{Buffer, LanguageRegistry};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use std::sync::Arc;
+use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(LanguageSelector::register).detach();
+}
+
+pub struct LanguageSelector {
+ picker: View<Picker<LanguageSelectorDelegate>>,
+}
+
+impl LanguageSelector {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(move |workspace, _: &Toggle, cx| {
+ Self::toggle(workspace, cx);
+ });
+ }
+
+ fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
+ let registry = workspace.app_state().languages.clone();
+ let (_, buffer, _) = workspace
+ .active_item(cx)?
+ .act_as::<Editor>(cx)?
+ .read(cx)
+ .active_excerpt(cx)?;
+ let project = workspace.project().clone();
+
+ workspace.toggle_modal(cx, move |cx| {
+ LanguageSelector::new(buffer, project, registry, cx)
+ });
+ Some(())
+ }
+
+ fn new(
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let delegate = LanguageSelectorDelegate::new(
+ cx.view().downgrade(),
+ buffer,
+ project,
+ language_registry,
+ );
+
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+ Self { picker }
+ }
+}
+
+impl Render for LanguageSelector {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ }
+}
+
+impl FocusableView for LanguageSelector {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for LanguageSelector {}
+
+pub struct LanguageSelectorDelegate {
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ candidates: Vec<StringMatchCandidate>,
+ matches: Vec<StringMatch>,
+ selected_index: usize,
+}
+
+impl LanguageSelectorDelegate {
+ fn new(
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ ) -> Self {
+ let candidates = language_registry
+ .language_names()
+ .into_iter()
+ .enumerate()
+ .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
+ .collect::<Vec<_>>();
+
+ Self {
+ language_selector,
+ buffer,
+ project,
+ language_registry,
+ candidates,
+ matches: vec![],
+ selected_index: 0,
+ }
+ }
+}
+
+impl PickerDelegate for LanguageSelectorDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self) -> Arc<str> {
+ "Select a language...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if let Some(mat) = self.matches.get(self.selected_index) {
+ let language_name = &self.candidates[mat.candidate_id].string;
+ let language = self.language_registry.language_for_name(language_name);
+ let project = self.project.downgrade();
+ let buffer = self.buffer.downgrade();
+ cx.spawn(|_, mut cx| async move {
+ let language = language.await?;
+ let project = project
+ .upgrade()
+ .ok_or_else(|| anyhow!("project was dropped"))?;
+ let buffer = buffer
+ .upgrade()
+ .ok_or_else(|| anyhow!("buffer was dropped"))?;
+ project.update(&mut cx, |project, cx| {
+ project.set_language_for_buffer(&buffer, language, cx);
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+ self.dismissed(cx);
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ self.language_selector
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> gpui::Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self.candidates.clone();
+ cx.spawn(|this, mut cx| async move {
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ match_strings(
+ &candidates,
+ &query,
+ false,
+ 100,
+ &Default::default(),
+ background,
+ )
+ .await
+ };
+
+ this.update(&mut cx, |this, cx| {
+ let delegate = &mut this.delegate;
+ delegate.matches = matches;
+ delegate.selected_index = delegate
+ .selected_index
+ .min(delegate.matches.len().saturating_sub(1));
+ cx.notify();
+ })
+ .log_err();
+ })
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches[ix];
+ let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
+ let mut label = mat.string.clone();
+ if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
+ label.push_str(" (current)");
+ }
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .selected(selected)
+ .child(HighlightedLabel::new(label, mat.positions.clone())),
+ )
+ }
+}
@@ -0,0 +1,29 @@
+[package]
+name = "outline2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/outline.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+picker = { package = "picker2", path = "../picker2" }
+settings = { package = "settings2", path = "../settings2" }
+text = { package = "text2", path = "../text2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+
+ordered-float.workspace = true
+postage.workspace = true
+smol.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -0,0 +1,276 @@
+use editor::{
+ display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
+ DisplayPoint, Editor, ToPoint,
+};
+use fuzzy::StringMatch;
+use gpui::{
+ actions, div, rems, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+ FontWeight, ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
+};
+use language::Outline;
+use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use std::{
+ cmp::{self, Reverse},
+ sync::Arc,
+};
+use theme::ActiveTheme;
+use ui::{v_stack, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(OutlineView::register).detach();
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+ if let Some(editor) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<Editor>())
+ {
+ let outline = editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .snapshot(cx)
+ .outline(Some(&cx.theme().syntax()));
+
+ if let Some(outline) = outline {
+ workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
+ }
+ }
+}
+
+pub struct OutlineView {
+ picker: View<Picker<OutlineViewDelegate>>,
+}
+
+impl FocusableView for OutlineView {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+
+impl EventEmitter<DismissEvent> for OutlineView {}
+
+impl Render for OutlineView {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ }
+}
+
+impl OutlineView {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(toggle);
+ }
+
+ fn new(
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<Self>,
+ ) -> OutlineView {
+ let delegate = OutlineViewDelegate::new(cx.view().downgrade(), outline, editor, cx);
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+ OutlineView { picker }
+ }
+}
+
+struct OutlineViewDelegate {
+ outline_view: WeakView<OutlineView>,
+ active_editor: View<Editor>,
+ outline: Outline<Anchor>,
+ selected_match_index: usize,
+ prev_scroll_position: Option<Point<f32>>,
+ matches: Vec<StringMatch>,
+ last_query: String,
+}
+
+impl OutlineViewDelegate {
+ fn new(
+ outline_view: WeakView<OutlineView>,
+ outline: Outline<Anchor>,
+ editor: View<Editor>,
+ cx: &mut ViewContext<OutlineView>,
+ ) -> Self {
+ Self {
+ outline_view,
+ last_query: Default::default(),
+ matches: Default::default(),
+ selected_match_index: 0,
+ prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))),
+ active_editor: editor,
+ outline,
+ }
+ }
+
+ fn restore_active_editor(&mut self, cx: &mut WindowContext) {
+ self.active_editor.update(cx, |editor, cx| {
+ editor.highlight_rows(None);
+ if let Some(scroll_position) = self.prev_scroll_position {
+ editor.set_scroll_position(scroll_position, cx);
+ }
+ })
+ }
+
+ fn set_selected_index(
+ &mut self,
+ ix: usize,
+ navigate: bool,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) {
+ self.selected_match_index = ix;
+
+ if navigate && !self.matches.is_empty() {
+ let selected_match = &self.matches[self.selected_match_index];
+ let outline_item = &self.outline.items[selected_match.candidate_id];
+
+ self.active_editor.update(cx, |active_editor, cx| {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let buffer_snapshot = &snapshot.buffer_snapshot;
+ let start = outline_item.range.start.to_point(buffer_snapshot);
+ let end = outline_item.range.end.to_point(buffer_snapshot);
+ let display_rows = start.to_display_point(&snapshot).row()
+ ..end.to_display_point(&snapshot).row() + 1;
+ active_editor.highlight_rows(Some(display_rows));
+ active_editor.request_autoscroll(Autoscroll::center(), cx);
+ });
+ }
+ }
+}
+
+impl PickerDelegate for OutlineViewDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self) -> Arc<str> {
+ "Search buffer symbols...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_match_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.set_selected_index(ix, true, cx);
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<OutlineViewDelegate>>,
+ ) -> Task<()> {
+ let selected_index;
+ if query.is_empty() {
+ self.restore_active_editor(cx);
+ self.matches = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(index, _)| StringMatch {
+ candidate_id: index,
+ score: Default::default(),
+ positions: Default::default(),
+ string: Default::default(),
+ })
+ .collect();
+
+ let editor = self.active_editor.read(cx);
+ let cursor_offset = editor.selections.newest::<usize>(cx).head();
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+ selected_index = self
+ .outline
+ .items
+ .iter()
+ .enumerate()
+ .map(|(ix, item)| {
+ let range = item.range.to_offset(&buffer);
+ let distance_to_closest_endpoint = cmp::min(
+ (range.start as isize - cursor_offset as isize).abs(),
+ (range.end as isize - cursor_offset as isize).abs(),
+ );
+ let depth = if range.contains(&cursor_offset) {
+ Some(item.depth)
+ } else {
+ None
+ };
+ (ix, depth, distance_to_closest_endpoint)
+ })
+ .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
+ .map(|(ix, _, _)| ix)
+ .unwrap_or(0);
+ } else {
+ self.matches = smol::block_on(
+ self.outline
+ .search(&query, cx.background_executor().clone()),
+ );
+ selected_index = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, m)| OrderedFloat(m.score))
+ .map(|(ix, _)| ix)
+ .unwrap_or(0);
+ }
+ self.last_query = query;
+ self.set_selected_index(selected_index, !self.last_query.is_empty(), cx);
+ Task::ready(())
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.prev_scroll_position.take();
+
+ self.active_editor.update(cx, |active_editor, cx| {
+ if let Some(rows) = active_editor.highlighted_rows() {
+ let snapshot = active_editor.snapshot(cx).display_snapshot;
+ let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
+ active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+ s.select_ranges([position..position])
+ });
+ active_editor.highlight_rows(None);
+ }
+ });
+
+ self.dismissed(cx);
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<OutlineViewDelegate>>) {
+ self.outline_view
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ self.restore_active_editor(cx);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches[ix];
+ let outline_item = &self.outline.items[mat.candidate_id];
+
+ let highlights = gpui::combine_highlights(
+ mat.ranges().map(|range| (range, FontWeight::BOLD.into())),
+ outline_item.highlight_ranges.iter().cloned(),
+ );
+
+ let styled_text = StyledText::new(outline_item.text.clone())
+ .with_highlights(&TextStyle::default(), highlights);
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .selected(selected)
+ .child(div().pl(rems(outline_item.depth as f32)).child(styled_text)),
+ )
+ }
+}
@@ -178,6 +178,15 @@ impl<D: PickerDelegate> Picker<D> {
}
cx.notify();
}
+
+ pub fn query(&self, cx: &AppContext) -> String {
+ self.editor.read(cx).text(cx)
+ }
+
+ pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.set_text(query, cx));
+ }
}
impl<D: PickerDelegate> Render for Picker<D> {
@@ -1121,20 +1121,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
let project_path = project_path.into();
- let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+ let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+ return Task::ready(Ok(None));
+ };
if self.is_local() {
- Some(worktree.update(cx, |worktree, cx| {
+ worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.create_entry(project_path.path, is_directory, cx)
- }))
+ })
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn_weak(|_, mut cx| async move {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
@@ -1143,19 +1145,20 @@ impl Project {
is_directory,
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1164,8 +1167,10 @@ impl Project {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1178,7 +1183,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn_weak(|_, mut cx| async move {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::CopyProjectEntry {
project_id,
@@ -1186,19 +1191,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1207,8 +1213,10 @@ impl Project {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1221,7 +1229,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn_weak(|_, mut cx| async move {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::RenameProjectEntry {
project_id,
@@ -1229,19 +1237,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1658,18 +1667,15 @@ impl Project {
pub fn open_path(
&mut self,
- path: impl Into<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
- let task = self.open_buffer(path, cx);
+ ) -> Task<Result<(Option<ProjectEntryId>, AnyModelHandle)>> {
+ let task = self.open_buffer(path.clone(), cx);
cx.spawn_weak(|_, cx| async move {
let buffer = task.await?;
- let project_entry_id = buffer
- .read_with(&cx, |buffer, cx| {
- File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
- })
- .ok_or_else(|| anyhow!("no project entry"))?;
-
+ let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+ File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+ });
let buffer: &AnyModelHandle = &buffer;
Ok((project_entry_id, buffer.clone()))
})
@@ -1984,8 +1990,10 @@ impl Project {
remote_id,
);
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
+ if let Some(entry_id) = file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
}
@@ -2440,24 +2448,25 @@ impl Project {
return None;
};
- match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
- Some(_) => {
- return None;
- }
- None => {
- let remote_id = buffer.read(cx).remote_id();
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
-
- self.local_buffer_ids_by_path.insert(
- ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path.clone(),
- },
- remote_id,
- );
+ let remote_id = buffer.read(cx).remote_id();
+ if let Some(entry_id) = file.entry_id {
+ match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+ Some(_) => {
+ return None;
+ }
+ None => {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
- }
+ };
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ },
+ remote_id,
+ );
}
_ => {}
}
@@ -5775,11 +5784,6 @@ impl Project {
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
- if !query.file_matches(Some(&ignored_abs_path))
- || snapshot.is_path_excluded(&ignored_abs_path)
- {
- continue;
- }
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
@@ -5807,6 +5811,13 @@ impl Project {
}
}
} else if !fs_metadata.is_symlink {
+ if !query.file_matches(Some(&ignored_abs_path))
+ || snapshot.is_path_excluded(
+ ignored_entry.path.to_path_buf(),
+ )
+ {
+ continue;
+ }
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
@@ -6207,10 +6218,13 @@ impl Project {
return;
}
- let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+ let new_file = if let Some(entry) = old_file
+ .entry_id
+ .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+ {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6219,7 +6233,7 @@ impl Project {
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6249,10 +6263,12 @@ impl Project {
);
}
- if new_file.entry_id != *entry_id {
+ if new_file.entry_id != Some(*entry_id) {
self.local_buffer_ids_by_entry_id.remove(entry_id);
- self.local_buffer_ids_by_entry_id
- .insert(new_file.entry_id, buffer_id);
+ if let Some(entry_id) = new_file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, buffer_id);
+ }
}
if new_file != *old_file {
@@ -6815,7 +6831,7 @@ impl Project {
})
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6839,11 +6855,10 @@ impl Project {
.as_local_mut()
.unwrap()
.rename_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })?
+ })
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6867,11 +6882,10 @@ impl Project {
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })?
+ })
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -4050,6 +4050,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
);
}
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index.txt": "index_key:index_value"
+ },
+ "node_modules": {
+ "eslint": {
+ "index.ts": "const eslint_key = 'eslint value'",
+ "package.json": r#"{ "some_key": "some value" }"#,
+ },
+ "prettier": {
+ "index.ts": "const prettier_key = 'prettier value'",
+ "package.json": r#"{ "other_key": "other value" }"#,
+ },
+ },
+ "package.json": r#"{ "main_key": "main value" }"#,
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+ let query = "key";
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+ "Only one non-ignored file should have the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([
+ ("package.json".to_string(), vec![8..11]),
+ ("target/index.txt".to_string(), vec![6..9]),
+ (
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ ),
+ ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+ ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+ ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+ ]),
+ "Unrestricted search with ignored directories should find every file with the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(
+ query,
+ false,
+ false,
+ true,
+ vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+ vec![PathMatcher::new("*.ts").unwrap()],
+ )
+ .unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([(
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ )]),
+ "With search including ignored prettier directory and excluding TS files, only one file should be found"
+ );
+}
+
#[test]
fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("**/*.js"), "");
@@ -371,15 +371,25 @@ impl SearchQuery {
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
match file_path {
Some(file_path) => {
- !self
- .files_to_exclude()
- .iter()
- .any(|exclude_glob| exclude_glob.is_match(file_path))
- && (self.files_to_include().is_empty()
+ let mut path = file_path.to_path_buf();
+ loop {
+ if self
+ .files_to_exclude()
+ .iter()
+ .any(|exclude_glob| exclude_glob.is_match(&path))
+ {
+ return false;
+ } else if self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.is_match(file_path)))
+ .any(|include_glob| include_glob.is_match(&path))
+ {
+ return true;
+ } else if !path.pop() {
+ return false;
+ }
+ }
}
None => self.files_to_include().is_empty(),
}
@@ -960,8 +960,6 @@ impl LocalWorktree {
cx.spawn(|this, cx| async move {
let text = fs.load(&abs_path).await?;
- let entry = entry.await?;
-
let mut index_task = None;
let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot());
if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -981,18 +979,43 @@ impl LocalWorktree {
None
};
- Ok((
- File {
- entry_id: entry.id,
- worktree: this,
- path: entry.path,
- mtime: entry.mtime,
- is_local: true,
- is_deleted: false,
- },
- text,
- diff_base,
- ))
+ match entry.await? {
+ Some(entry) => Ok((
+ File {
+ entry_id: Some(entry.id),
+ worktree: this,
+ path: entry.path,
+ mtime: entry.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ )),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!("Loading metadata for excluded file {abs_path:?}")
+ })?
+ .with_context(|| {
+ format!("Excluded file {abs_path:?} got removed during loading")
+ })?;
+ Ok((
+ File {
+ entry_id: None,
+ worktree: this,
+ path,
+ mtime: metadata.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ ))
+ }
+ }
})
}
@@ -1013,17 +1036,37 @@ impl LocalWorktree {
let text = buffer.as_rope().clone();
let fingerprint = text.fingerprint();
let version = buffer.version();
- let save = self.write_file(path, text, buffer.line_ending(), cx);
+ let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+ let fs = Arc::clone(&self.fs);
+ let abs_path = self.absolutize(&path);
cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?;
+ let (entry_id, mtime, path) = match entry {
+ Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!(
+ "Fetching metadata after saving the excluded buffer {abs_path:?}"
+ )
+ })?
+ .with_context(|| {
+ format!("Excluded buffer {path:?} got removed during saving")
+ })?;
+ (None, metadata.mtime, path)
+ }
+ };
+
if has_changed_file {
let new_file = Arc::new(File {
- entry_id: entry.id,
+ entry_id,
worktree: handle,
- path: entry.path,
- mtime: entry.mtime,
+ path,
+ mtime,
is_local: true,
is_deleted: false,
});
@@ -1049,13 +1092,13 @@ impl LocalWorktree {
project_id,
buffer_id,
version: serialize_version(&version),
- mtime: Some(entry.mtime.into()),
+ mtime: Some(mtime.into()),
fingerprint: serialize_fingerprint(fingerprint),
})?;
}
buffer_handle.update(&mut cx, |buffer, cx| {
- buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+ buffer.did_save(version.clone(), fingerprint, mtime, cx);
});
Ok(())
@@ -1080,7 +1123,7 @@ impl LocalWorktree {
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
@@ -1097,7 +1140,7 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
let (result, refreshes) = this.update(&mut cx, |this, cx| {
- let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+ let mut refreshes = Vec::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
@@ -1124,14 +1167,14 @@ impl LocalWorktree {
})
}
- pub fn write_file(
+ pub(crate) fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = path.into();
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx
@@ -1190,8 +1233,11 @@ impl LocalWorktree {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1201,7 +1247,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
rename.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1209,7 +1255,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), Some(old_path), cx)
})
.await
- }))
+ })
}
pub fn copy_entry(
@@ -1217,8 +1263,11 @@ impl LocalWorktree {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1233,7 +1282,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
copy.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1241,7 +1290,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), None, cx)
})
.await
- }))
+ })
}
pub fn expand_entry(
@@ -1277,7 +1326,10 @@ impl LocalWorktree {
path: Arc<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ if self.is_path_excluded(path.to_path_buf()) {
+ return Task::ready(Ok(None));
+ }
let paths = if let Some(old_path) = old_path.as_ref() {
vec![old_path.clone(), path.clone()]
} else {
@@ -1286,13 +1338,15 @@ impl LocalWorktree {
let mut refresh = self.refresh_entries_for_paths(paths);
cx.spawn_weak(move |this, mut cx| async move {
refresh.recv().await;
- this.upgrade(&cx)
+ let new_entry = this
+ .upgrade(&cx)
.ok_or_else(|| anyhow!("worktree was dropped"))?
.update(&mut cx, |this, _| {
this.entry_for_path(path)
.cloned()
.ok_or_else(|| anyhow!("failed to read path after update"))
- })
+ })?;
+ Ok(Some(new_entry))
})
}
@@ -2226,10 +2280,19 @@ impl LocalSnapshot {
paths
}
- pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
- self.file_scan_exclusions
- .iter()
- .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+ loop {
+ if self
+ .file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(&path))
+ {
+ return true;
+ }
+ if !path.pop() {
+ return false;
+ }
+ }
}
}
@@ -2458,8 +2521,7 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id);
} else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
- let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
- || snapshot.is_path_excluded(&git_dir_abs_path);
+ let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{
@@ -2666,7 +2728,7 @@ pub struct File {
pub worktree: ModelHandle<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool,
pub(crate) is_deleted: bool,
}
@@ -2735,7 +2797,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.id() as u64,
- entry_id: self.entry_id.to_proto(),
+ entry_id: self.entry_id.map(|id| id.to_proto()),
path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()),
is_deleted: self.is_deleted,
@@ -2793,7 +2855,7 @@ impl File {
worktree,
path: entry.path.clone(),
mtime: entry.mtime,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
is_local: true,
is_deleted: false,
})
@@ -2818,7 +2880,7 @@ impl File {
worktree,
path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
- entry_id: ProjectEntryId::from_proto(proto.entry_id),
+ entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
is_deleted: proto.is_deleted,
})
@@ -2836,7 +2898,7 @@ impl File {
if self.is_deleted {
None
} else {
- Some(self.entry_id)
+ self.entry_id
}
}
}
@@ -3338,16 +3400,7 @@ impl BackgroundScanner {
return false;
}
- // FS events may come for files which parent directory is excluded, need to check ignore those.
- let mut path_to_test = abs_path.clone();
- let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
- || snapshot.is_path_excluded(&relative_path);
- while !excluded_file_event && path_to_test.pop() {
- if snapshot.is_path_excluded(&path_to_test) {
- excluded_file_event = true;
- }
- }
- if excluded_file_event {
+ if snapshot.is_path_excluded(relative_path.to_path_buf()) {
if !is_git_related {
log::debug!("ignoring FS event for excluded path {relative_path:?}");
}
@@ -3531,7 +3584,7 @@ impl BackgroundScanner {
let state = self.state.lock();
let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone();
- if snapshot.is_path_excluded(&job.abs_path) {
+ if snapshot.is_path_excluded(job.path.to_path_buf()) {
log::error!("skipping excluded directory {:?}", job.path);
return Ok(());
}
@@ -3603,8 +3656,8 @@ impl BackgroundScanner {
{
let mut state = self.state.lock();
- if state.snapshot.is_path_excluded(&child_abs_path) {
- let relative_path = job.path.join(child_name);
+ let relative_path = job.path.join(child_name);
+ if state.snapshot.is_path_excluded(relative_path.clone()) {
log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path);
continue;
@@ -1052,11 +1052,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
&[
".git/HEAD",
".git/foo",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
- &["target", "node_modules"],
+ &["target"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1106,6 +1107,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
".git/HEAD",
".git/foo",
".git/new_file",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
@@ -1114,7 +1116,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
"build_output/new_file",
"test_output/new_file",
],
- &["target", "node_modules", "test_output"],
+ &["target", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1174,6 +1176,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.create_entry("a/e".as_ref(), true, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_dir());
@@ -1222,6 +1225,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1257,6 +1261,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1275,6 +1280,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1291,6 +1297,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1616,14 +1623,14 @@ fn randomly_mutate_worktree(
entry.id.0,
new_path
);
- let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+ let task = worktree.rename_entry(entry.id, new_path, cx);
cx.foreground().spawn(async move {
- task.await?;
+ task.await?.unwrap();
Ok(())
})
}
_ => {
- let task = if entry.is_dir() {
+ if entry.is_dir() {
let child_path = entry.path.join(random_filename(rng));
let is_dir = rng.gen_bool(0.3);
log::info!(
@@ -1631,15 +1638,20 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
- worktree.create_entry(child_path, is_dir, cx)
+ let task = worktree.create_entry(child_path, is_dir, cx);
+ cx.foreground().spawn(async move {
+ task.await?;
+ Ok(())
+ })
} else {
log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
- worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
- };
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ let task =
+ worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+ cx.foreground().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
}
}
}
@@ -1151,20 +1151,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
let project_path = project_path.into();
- let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+ let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+ return Task::ready(Ok(None));
+ };
if self.is_local() {
- Some(worktree.update(cx, |worktree, cx| {
+ worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.create_entry(project_path.path, is_directory, cx)
- }))
+ })
} else {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
@@ -1173,19 +1175,20 @@ impl Project {
is_directory,
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1194,8 +1197,10 @@ impl Project {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1208,7 +1213,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CopyProjectEntry {
project_id,
@@ -1216,19 +1221,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1237,8 +1243,10 @@ impl Project {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
- let worktree = self.worktree_for_entry(entry_id, cx)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1251,7 +1259,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::RenameProjectEntry {
project_id,
@@ -1259,19 +1267,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1688,17 +1697,15 @@ impl Project {
pub fn open_path(
&mut self,
- path: impl Into<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
- let task = self.open_buffer(path, cx);
- cx.spawn(move |_, mut cx| async move {
+ ) -> Task<Result<(Option<ProjectEntryId>, AnyModel)>> {
+ let task = self.open_buffer(path.clone(), cx);
+ cx.spawn(move |_, cx| async move {
let buffer = task.await?;
- let project_entry_id = buffer
- .update(&mut cx, |buffer, cx| {
- File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
- })?
- .ok_or_else(|| anyhow!("no project entry"))?;
+ let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+ File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+ })?;
let buffer: &AnyModel = &buffer;
Ok((project_entry_id, buffer.clone()))
@@ -2017,8 +2024,10 @@ impl Project {
remote_id,
);
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
+ if let Some(entry_id) = file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
}
@@ -2473,24 +2482,25 @@ impl Project {
return None;
};
- match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
- Some(_) => {
- return None;
- }
- None => {
- let remote_id = buffer.read(cx).remote_id();
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
-
- self.local_buffer_ids_by_path.insert(
- ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path.clone(),
- },
- remote_id,
- );
+ let remote_id = buffer.read(cx).remote_id();
+ if let Some(entry_id) = file.entry_id {
+ match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+ Some(_) => {
+ return None;
+ }
+ None => {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
- }
+ };
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ },
+ remote_id,
+ );
}
_ => {}
}
@@ -5844,11 +5854,6 @@ impl Project {
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
- if !query.file_matches(Some(&ignored_abs_path))
- || snapshot.is_path_excluded(&ignored_abs_path)
- {
- continue;
- }
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
@@ -5876,6 +5881,13 @@ impl Project {
}
}
} else if !fs_metadata.is_symlink {
+ if !query.file_matches(Some(&ignored_abs_path))
+ || snapshot.is_path_excluded(
+ ignored_entry.path.to_path_buf(),
+ )
+ {
+ continue;
+ }
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
@@ -6277,10 +6289,13 @@ impl Project {
return;
}
- let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+ let new_file = if let Some(entry) = old_file
+ .entry_id
+ .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+ {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6289,7 +6304,7 @@ impl Project {
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6319,10 +6334,12 @@ impl Project {
);
}
- if new_file.entry_id != *entry_id {
+ if new_file.entry_id != Some(*entry_id) {
self.local_buffer_ids_by_entry_id.remove(entry_id);
- self.local_buffer_ids_by_entry_id
- .insert(new_file.entry_id, buffer_id);
+ if let Some(entry_id) = new_file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, buffer_id);
+ }
}
if new_file != *old_file {
@@ -6889,7 +6906,7 @@ impl Project {
})?
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6913,11 +6930,10 @@ impl Project {
.as_local_mut()
.unwrap()
.rename_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })??
+ })?
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6941,11 +6957,10 @@ impl Project {
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })??
+ })?
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -4182,6 +4182,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
);
}
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index.txt": "index_key:index_value"
+ },
+ "node_modules": {
+ "eslint": {
+ "index.ts": "const eslint_key = 'eslint value'",
+ "package.json": r#"{ "some_key": "some value" }"#,
+ },
+ "prettier": {
+ "index.ts": "const prettier_key = 'prettier value'",
+ "package.json": r#"{ "other_key": "other value" }"#,
+ },
+ },
+ "package.json": r#"{ "main_key": "main value" }"#,
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+ let query = "key";
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+ "Only one non-ignored file should have the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([
+ ("package.json".to_string(), vec![8..11]),
+ ("target/index.txt".to_string(), vec![6..9]),
+ (
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ ),
+ ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+ ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+ ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+ ]),
+ "Unrestricted search with ignored directories should find every file with the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(
+ query,
+ false,
+ false,
+ true,
+ vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+ vec![PathMatcher::new("*.ts").unwrap()],
+ )
+ .unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([(
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ )]),
+ "With search including ignored prettier directory and excluding TS files, only one file should be found"
+ );
+}
+
#[test]
fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("**/*.js"), "");
@@ -371,15 +371,25 @@ impl SearchQuery {
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
match file_path {
Some(file_path) => {
- !self
- .files_to_exclude()
- .iter()
- .any(|exclude_glob| exclude_glob.is_match(file_path))
- && (self.files_to_include().is_empty()
+ let mut path = file_path.to_path_buf();
+ loop {
+ if self
+ .files_to_exclude()
+ .iter()
+ .any(|exclude_glob| exclude_glob.is_match(&path))
+ {
+ return false;
+ } else if self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.is_match(file_path)))
+ .any(|include_glob| include_glob.is_match(&path))
+ {
+ return true;
+ } else if !path.pop() {
+ return false;
+ }
+ }
}
None => self.files_to_include().is_empty(),
}
@@ -958,8 +958,6 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?;
- let entry = entry.await?;
-
let mut index_task = None;
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -982,18 +980,43 @@ impl LocalWorktree {
let worktree = this
.upgrade()
.ok_or_else(|| anyhow!("worktree was dropped"))?;
- Ok((
- File {
- entry_id: entry.id,
- worktree,
- path: entry.path,
- mtime: entry.mtime,
- is_local: true,
- is_deleted: false,
- },
- text,
- diff_base,
- ))
+ match entry.await? {
+ Some(entry) => Ok((
+ File {
+ entry_id: Some(entry.id),
+ worktree,
+ path: entry.path,
+ mtime: entry.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ )),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!("Loading metadata for excluded file {abs_path:?}")
+ })?
+ .with_context(|| {
+ format!("Excluded file {abs_path:?} got removed during loading")
+ })?;
+ Ok((
+ File {
+ entry_id: None,
+ worktree,
+ path,
+ mtime: metadata.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ ))
+ }
+ }
})
}
@@ -1013,18 +1036,38 @@ impl LocalWorktree {
let text = buffer.as_rope().clone();
let fingerprint = text.fingerprint();
let version = buffer.version();
- let save = self.write_file(path, text, buffer.line_ending(), cx);
+ let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+ let fs = Arc::clone(&self.fs);
+ let abs_path = self.absolutize(&path);
cx.spawn(move |this, mut cx| async move {
let entry = save.await?;
let this = this.upgrade().context("worktree dropped")?;
+ let (entry_id, mtime, path) = match entry {
+ Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!(
+ "Fetching metadata after saving the excluded buffer {abs_path:?}"
+ )
+ })?
+ .with_context(|| {
+ format!("Excluded buffer {path:?} got removed during saving")
+ })?;
+ (None, metadata.mtime, path)
+ }
+ };
+
if has_changed_file {
let new_file = Arc::new(File {
- entry_id: entry.id,
+ entry_id,
worktree: this,
- path: entry.path,
- mtime: entry.mtime,
+ path,
+ mtime,
is_local: true,
is_deleted: false,
});
@@ -1050,13 +1093,13 @@ impl LocalWorktree {
project_id,
buffer_id,
version: serialize_version(&version),
- mtime: Some(entry.mtime.into()),
+ mtime: Some(mtime.into()),
fingerprint: serialize_fingerprint(fingerprint),
})?;
}
buffer_handle.update(&mut cx, |buffer, cx| {
- buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+ buffer.did_save(version.clone(), fingerprint, mtime, cx);
})?;
Ok(())
@@ -1081,7 +1124,7 @@ impl LocalWorktree {
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
@@ -1098,7 +1141,7 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
let (result, refreshes) = this.update(&mut cx, |this, cx| {
- let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+ let mut refreshes = Vec::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
@@ -1125,14 +1168,14 @@ impl LocalWorktree {
})
}
- pub fn write_file(
+ pub(crate) fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = path.into();
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx
@@ -1191,8 +1234,11 @@ impl LocalWorktree {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1202,7 +1248,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
rename.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1210,7 +1256,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), Some(old_path), cx)
})?
.await
- }))
+ })
}
pub fn copy_entry(
@@ -1218,8 +1264,11 @@ impl LocalWorktree {
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Option<Task<Result<Entry>>> {
- let old_path = self.entry_for_id(entry_id)?.path.clone();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1234,7 +1283,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
copy.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1242,7 +1291,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), None, cx)
})?
.await
- }))
+ })
}
pub fn expand_entry(
@@ -1278,7 +1327,10 @@ impl LocalWorktree {
path: Arc<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ if self.is_path_excluded(path.to_path_buf()) {
+ return Task::ready(Ok(None));
+ }
let paths = if let Some(old_path) = old_path.as_ref() {
vec![old_path.clone(), path.clone()]
} else {
@@ -1287,11 +1339,12 @@ impl LocalWorktree {
let mut refresh = self.refresh_entries_for_paths(paths);
cx.spawn(move |this, mut cx| async move {
refresh.recv().await;
- this.update(&mut cx, |this, _| {
+ let new_entry = this.update(&mut cx, |this, _| {
this.entry_for_path(path)
.cloned()
.ok_or_else(|| anyhow!("failed to read path after update"))
- })?
+ })??;
+ Ok(Some(new_entry))
})
}
@@ -2222,10 +2275,19 @@ impl LocalSnapshot {
paths
}
- pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
- self.file_scan_exclusions
- .iter()
- .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+ loop {
+ if self
+ .file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(&path))
+ {
+ return true;
+ }
+ if !path.pop() {
+ return false;
+ }
+ }
}
}
@@ -2455,8 +2517,7 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id);
} else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
- let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
- || snapshot.is_path_excluded(&git_dir_abs_path);
+ let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{
@@ -2663,7 +2724,7 @@ pub struct File {
pub worktree: Model<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool,
pub(crate) is_deleted: bool,
}
@@ -2732,7 +2793,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.entity_id().as_u64(),
- entry_id: self.entry_id.to_proto(),
+ entry_id: self.entry_id.map(|id| id.to_proto()),
path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()),
is_deleted: self.is_deleted,
@@ -2790,7 +2851,7 @@ impl File {
worktree,
path: entry.path.clone(),
mtime: entry.mtime,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
is_local: true,
is_deleted: false,
})
@@ -2815,7 +2876,7 @@ impl File {
worktree,
path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
- entry_id: ProjectEntryId::from_proto(proto.entry_id),
+ entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
is_deleted: proto.is_deleted,
})
@@ -2833,7 +2894,7 @@ impl File {
if self.is_deleted {
None
} else {
- Some(self.entry_id)
+ self.entry_id
}
}
}
@@ -3329,16 +3390,7 @@ impl BackgroundScanner {
return false;
}
- // FS events may come for files which parent directory is excluded, need to check ignore those.
- let mut path_to_test = abs_path.clone();
- let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
- || snapshot.is_path_excluded(&relative_path);
- while !excluded_file_event && path_to_test.pop() {
- if snapshot.is_path_excluded(&path_to_test) {
- excluded_file_event = true;
- }
- }
- if excluded_file_event {
+ if snapshot.is_path_excluded(relative_path.to_path_buf()) {
if !is_git_related {
log::debug!("ignoring FS event for excluded path {relative_path:?}");
}
@@ -3522,7 +3574,7 @@ impl BackgroundScanner {
let state = self.state.lock();
let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone();
- if snapshot.is_path_excluded(&job.abs_path) {
+ if snapshot.is_path_excluded(job.path.to_path_buf()) {
log::error!("skipping excluded directory {:?}", job.path);
return Ok(());
}
@@ -3593,9 +3645,9 @@ impl BackgroundScanner {
}
{
+ let relative_path = job.path.join(child_name);
let mut state = self.state.lock();
- if state.snapshot.is_path_excluded(&child_abs_path) {
- let relative_path = job.path.join(child_name);
+ if state.snapshot.is_path_excluded(relative_path.clone()) {
log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path);
continue;
@@ -1055,11 +1055,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
&[
".git/HEAD",
".git/foo",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
- &["target", "node_modules"],
+ &["target"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1109,6 +1110,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
".git/HEAD",
".git/foo",
".git/new_file",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
@@ -1117,7 +1119,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
"build_output/new_file",
"test_output/new_file",
],
- &["target", "node_modules", "test_output"],
+ &["target", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1177,6 +1179,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.create_entry("a/e".as_ref(), true, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_dir());
@@ -1226,6 +1229,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1261,6 +1265,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1279,6 +1284,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1295,6 +1301,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1620,14 +1627,14 @@ fn randomly_mutate_worktree(
entry.id.0,
new_path
);
- let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+ let task = worktree.rename_entry(entry.id, new_path, cx);
cx.background_executor().spawn(async move {
- task.await?;
+ task.await?.unwrap();
Ok(())
})
}
_ => {
- let task = if entry.is_dir() {
+ if entry.is_dir() {
let child_path = entry.path.join(random_filename(rng));
let is_dir = rng.gen_bool(0.3);
log::info!(
@@ -1635,15 +1642,20 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
- worktree.create_entry(child_path, is_dir, cx)
+ let task = worktree.create_entry(child_path, is_dir, cx);
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
} else {
log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
- worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
- };
- cx.background_executor().spawn(async move {
- task.await?;
- Ok(())
- })
+ let task =
+ worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
}
}
}
@@ -621,7 +621,7 @@ impl ProjectPanel {
edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| {
project.create_entry((worktree_id, &new_path), is_dir, cx)
- })?;
+ });
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
@@ -635,7 +635,7 @@ impl ProjectPanel {
edited_entry_id = entry.id;
edit_task = self.project.update(cx, |project, cx| {
project.rename_entry(entry.id, new_path.as_path(), cx)
- })?;
+ });
};
edit_state.processing_filename = Some(filename);
@@ -648,21 +648,22 @@ impl ProjectPanel {
cx.notify();
})?;
- let new_entry = new_entry?;
- this.update(&mut cx, |this, cx| {
- if let Some(selection) = &mut this.selection {
- if selection.entry_id == edited_entry_id {
- selection.worktree_id = worktree_id;
- selection.entry_id = new_entry.id;
- this.expand_to_selection(cx);
+ if let Some(new_entry) = new_entry? {
+ this.update(&mut cx, |this, cx| {
+ if let Some(selection) = &mut this.selection {
+ if selection.entry_id == edited_entry_id {
+ selection.worktree_id = worktree_id;
+ selection.entry_id = new_entry.id;
+ this.expand_to_selection(cx);
+ }
}
- }
- this.update_visible_entries(None, cx);
- if is_new_entry && !is_dir {
- this.open_entry(new_entry.id, true, cx);
- }
- cx.notify();
- })?;
+ this.update_visible_entries(None, cx);
+ if is_new_entry && !is_dir {
+ this.open_entry(new_entry.id, true, cx);
+ }
+ cx.notify();
+ })?;
+ }
Ok(())
}))
}
@@ -935,15 +936,17 @@ impl ProjectPanel {
}
if clipboard_entry.is_cut() {
- if let Some(task) = self.project.update(cx, |project, cx| {
- project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx)
- }
- } else if let Some(task) = self.project.update(cx, |project, cx| {
- project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx)
+ self.project
+ .update(cx, |project, cx| {
+ project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
+ } else {
+ self.project
+ .update(cx, |project, cx| {
+ project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
}
}
None
@@ -1026,7 +1029,7 @@ impl ProjectPanel {
let mut new_path = destination_path.to_path_buf();
new_path.push(entry_path.path.file_name()?);
if new_path != entry_path.path.as_ref() {
- let task = project.rename_entry(entry_to_move, new_path, cx)?;
+ let task = project.rename_entry(entry_to_move, new_path, cx);
cx.foreground().spawn(task).detach_and_log_err(cx);
}
@@ -1627,9 +1630,21 @@ impl View for ProjectPanel {
}
}
- fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
+ fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
Self::reset_to_default_keymap_context(keymap);
keymap.add_identifier("menu");
+
+ if let Some(window) = cx.active_window() {
+ window.read_with(cx, |cx| {
+ let identifier = if self.filename_editor.is_focused(cx) {
+ "editing"
+ } else {
+ "not_editing"
+ };
+
+ keymap.add_identifier(identifier);
+ });
+ }
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
@@ -10,9 +10,9 @@ use anyhow::{anyhow, Result};
use gpui::{
actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
- InteractiveElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
- PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View,
- ViewContext, VisualContext as _, WeakView, WindowContext,
+ InteractiveElement, KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels,
+ Point, PromptLevel, Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle,
+ View, ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@@ -397,7 +397,6 @@ impl ProjectPanel {
menu = menu.action(
"Add Folder to Project",
Box::new(workspace::AddFolderToProject),
- cx,
);
if is_root {
menu = menu.entry(
@@ -412,35 +411,35 @@ impl ProjectPanel {
}
menu = menu
- .action("New File", Box::new(NewFile), cx)
- .action("New Folder", Box::new(NewDirectory), cx)
+ .action("New File", Box::new(NewFile))
+ .action("New Folder", Box::new(NewDirectory))
.separator()
- .action("Cut", Box::new(Cut), cx)
- .action("Copy", Box::new(Copy), cx);
+ .action("Cut", Box::new(Cut))
+ .action("Copy", Box::new(Copy));
if let Some(clipboard_entry) = self.clipboard_entry {
if clipboard_entry.worktree_id() == worktree_id {
- menu = menu.action("Paste", Box::new(Paste), cx);
+ menu = menu.action("Paste", Box::new(Paste));
}
}
menu = menu
.separator()
- .action("Copy Path", Box::new(CopyPath), cx)
- .action("Copy Relative Path", Box::new(CopyRelativePath), cx)
+ .action("Copy Path", Box::new(CopyPath))
+ .action("Copy Relative Path", Box::new(CopyRelativePath))
.separator()
- .action("Reveal in Finder", Box::new(RevealInFinder), cx);
+ .action("Reveal in Finder", Box::new(RevealInFinder));
if is_dir {
menu = menu
- .action("Open in Terminal", Box::new(OpenInTerminal), cx)
- .action("Search Inside", Box::new(NewSearchInDirectory), cx)
+ .action("Open in Terminal", Box::new(OpenInTerminal))
+ .action("Search Inside", Box::new(NewSearchInDirectory))
}
- menu = menu.separator().action("Rename", Box::new(Rename), cx);
+ menu = menu.separator().action("Rename", Box::new(Rename));
if !is_root {
- menu = menu.action("Delete", Box::new(Delete), cx);
+ menu = menu.action("Delete", Box::new(Delete));
}
menu
@@ -611,7 +610,7 @@ impl ProjectPanel {
edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| {
project.create_entry((worktree_id, &new_path), is_dir, cx)
- })?;
+ });
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
@@ -625,7 +624,7 @@ impl ProjectPanel {
edited_entry_id = entry.id;
edit_task = self.project.update(cx, |project, cx| {
project.rename_entry(entry.id, new_path.as_path(), cx)
- })?;
+ });
};
edit_state.processing_filename = Some(filename);
@@ -638,21 +637,22 @@ impl ProjectPanel {
cx.notify();
})?;
- let new_entry = new_entry?;
- this.update(&mut cx, |this, cx| {
- if let Some(selection) = &mut this.selection {
- if selection.entry_id == edited_entry_id {
- selection.worktree_id = worktree_id;
- selection.entry_id = new_entry.id;
- this.expand_to_selection(cx);
+ if let Some(new_entry) = new_entry? {
+ this.update(&mut cx, |this, cx| {
+ if let Some(selection) = &mut this.selection {
+ if selection.entry_id == edited_entry_id {
+ selection.worktree_id = worktree_id;
+ selection.entry_id = new_entry.id;
+ this.expand_to_selection(cx);
+ }
}
- }
- this.update_visible_entries(None, cx);
- if is_new_entry && !is_dir {
- this.open_entry(new_entry.id, true, cx);
- }
- cx.notify();
- })?;
+ this.update_visible_entries(None, cx);
+ if is_new_entry && !is_dir {
+ this.open_entry(new_entry.id, true, cx);
+ }
+ cx.notify();
+ })?;
+ }
Ok(())
}))
}
@@ -932,15 +932,17 @@ impl ProjectPanel {
}
if clipboard_entry.is_cut() {
- if let Some(task) = self.project.update(cx, |project, cx| {
- project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx);
- }
- } else if let Some(task) = self.project.update(cx, |project, cx| {
- project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx);
+ self.project
+ .update(cx, |project, cx| {
+ project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
+ } else {
+ self.project
+ .update(cx, |project, cx| {
+ project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
}
Some(())
@@ -1026,7 +1028,7 @@ impl ProjectPanel {
// let mut new_path = destination_path.to_path_buf();
// new_path.push(entry_path.path.file_name()?);
// if new_path != entry_path.path.as_ref() {
- // let task = project.rename_entry(entry_to_move, new_path, cx)?;
+ // let task = project.rename_entry(entry_to_move, new_path, cx);
// cx.foreground_executor().spawn(task).detach_and_log_err(cx);
// }
@@ -1420,6 +1422,22 @@ impl ProjectPanel {
// );
// })
}
+
+ fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
+ let mut dispatch_context = KeyContext::default();
+ dispatch_context.add("ProjectPanel");
+ dispatch_context.add("menu");
+
+ let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
+ "editing"
+ } else {
+ "not_editing"
+ };
+
+ dispatch_context.add(identifier);
+
+ dispatch_context
+ }
}
impl Render for ProjectPanel {
@@ -1433,7 +1451,7 @@ impl Render for ProjectPanel {
.id("project-panel")
.size_full()
.relative()
- .key_context("ProjectPanel")
+ .key_context(self.dispatch_context(cx))
.on_action(cx.listener(Self::select_next))
.on_action(cx.listener(Self::select_prev))
.on_action(cx.listener(Self::expand_selected_entry))
@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
}
message ProjectEntryResponse {
- Entry entry = 1;
+ optional Entry entry = 1;
uint64 worktree_scan_id = 2;
}
@@ -1357,7 +1357,7 @@ message User {
message File {
uint64 worktree_id = 1;
- uint64 entry_id = 2;
+ optional uint64 entry_id = 2;
string path = 3;
Timestamp mtime = 4;
bool is_deleted = 5;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
}
message ProjectEntryResponse {
- Entry entry = 1;
+ optional Entry entry = 1;
uint64 worktree_scan_id = 2;
}
@@ -1357,7 +1357,7 @@ message User {
message File {
uint64 worktree_id = 1;
- uint64 entry_id = 2;
+ optional uint64 entry_id = 2;
string path = 3;
Timestamp mtime = 4;
bool is_deleted = 5;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 64;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -0,0 +1,69 @@
+[package]
+name = "semantic_index2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/semantic_index.rs"
+doctest = false
+
+[dependencies]
+ai = { package = "ai2", path = "../ai2" }
+collections = { path = "../collections" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+project = { package = "project2", path = "../project2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+util = { path = "../util" }
+rpc = { package = "rpc2", path = "../rpc2" }
+settings = { package = "settings2", path = "../settings2" }
+anyhow.workspace = true
+postage.workspace = true
+futures.workspace = true
+ordered-float.workspace = true
+smol.workspace = true
+rusqlite.workspace = true
+log.workspace = true
+tree-sitter.workspace = true
+lazy_static.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+async-trait.workspace = true
+tiktoken-rs.workspace = true
+parking_lot.workspace = true
+rand.workspace = true
+schemars.workspace = true
+globset.workspace = true
+sha1 = "0.10.5"
+ndarray = { version = "0.15.0" }
+
+[dev-dependencies]
+ai = { package = "ai2", path = "../ai2", features = ["test-support"] }
+collections = { path = "../collections", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+project = { package = "project2", path = "../project2", features = ["test-support"] }
+rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+settings = { package = "settings2", path = "../settings2", features = ["test-support"]}
+rust-embed = { version = "8.0", features = ["include-exclude"] }
+client = { package = "client2", path = "../client2" }
+node_runtime = { path = "../node_runtime"}
+
+pretty_assertions.workspace = true
+rand.workspace = true
+unindent.workspace = true
+tempdir.workspace = true
+ctor.workspace = true
+env_logger.workspace = true
+
+tree-sitter-typescript.workspace = true
+tree-sitter-json.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-toml.workspace = true
+tree-sitter-cpp.workspace = true
+tree-sitter-elixir.workspace = true
+tree-sitter-lua.workspace = true
+tree-sitter-ruby.workspace = true
+tree-sitter-php.workspace = true
@@ -0,0 +1,20 @@
+
+# Semantic Index
+
+## Evaluation
+
+### Metrics
+
+nDCG@k:
+- "The value of NDCG is determined by comparing the relevance of the items returned by the search engine to the relevance of the item that a hypothetical "ideal" search engine would return.
+- "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?"
+
+MRR@k:
+- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list."
+
+MAP@k:
+- "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list.
+
+Resources:
+- [Evaluating recommendation metrics](https://www.shaped.ai/blog/evaluating-recommendation-systems-map-mmr-ndcg)
+- [Math Walkthrough](https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0)
@@ -0,0 +1,114 @@
+{
+ "repo": "https://github.com/AntonOsika/gpt-engineer.git",
+ "commit": "7735a6445bae3611c62f521e6464c67c957f87c2",
+ "assertions": [
+ {
+ "query": "How do I contribute to this project?",
+ "matches": [
+ ".github/CONTRIBUTING.md:1",
+ "ROADMAP.md:48"
+ ]
+ },
+ {
+ "query": "What version of the openai package is active?",
+ "matches": [
+ "pyproject.toml:14"
+ ]
+ },
+ {
+ "query": "Ask user for clarification",
+ "matches": [
+ "gpt_engineer/steps.py:69"
+ ]
+ },
+ {
+ "query": "generate tests for python code",
+ "matches": [
+ "gpt_engineer/steps.py:153"
+ ]
+ },
+ {
+ "query": "get item from database based on key",
+ "matches": [
+ "gpt_engineer/db.py:42",
+ "gpt_engineer/db.py:68"
+ ]
+ },
+ {
+ "query": "prompt user to select files",
+ "matches": [
+ "gpt_engineer/file_selector.py:171",
+ "gpt_engineer/file_selector.py:306",
+ "gpt_engineer/file_selector.py:289",
+ "gpt_engineer/file_selector.py:234"
+ ]
+ },
+ {
+ "query": "send to rudderstack",
+ "matches": [
+ "gpt_engineer/collect.py:11",
+ "gpt_engineer/collect.py:38"
+ ]
+ },
+ {
+ "query": "parse code blocks from chat messages",
+ "matches": [
+ "gpt_engineer/chat_to_files.py:10",
+ "docs/intro/chat_parsing.md:1"
+ ]
+ },
+ {
+ "query": "how do I use the docker cli?",
+ "matches": [
+ "docker/README.md:1"
+ ]
+ },
+ {
+ "query": "ask the user if the code ran successfully?",
+ "matches": [
+ "gpt_engineer/learning.py:54"
+ ]
+ },
+ {
+ "query": "how is consent granted by the user?",
+ "matches": [
+ "gpt_engineer/learning.py:107",
+ "gpt_engineer/learning.py:130",
+ "gpt_engineer/learning.py:152"
+ ]
+ },
+ {
+ "query": "what are all the different steps the agent can take?",
+ "matches": [
+ "docs/intro/steps_module.md:1",
+ "gpt_engineer/steps.py:391"
+ ]
+ },
+ {
+ "query": "ask the user for clarification?",
+ "matches": [
+ "gpt_engineer/steps.py:69"
+ ]
+ },
+ {
+ "query": "what models are available?",
+ "matches": [
+ "gpt_engineer/ai.py:315",
+ "gpt_engineer/ai.py:341",
+ "docs/open-models.md:1"
+ ]
+ },
+ {
+ "query": "what is the current focus of the project?",
+ "matches": [
+ "ROADMAP.md:11"
+ ]
+ },
+ {
+ "query": "does the agent know how to fix code?",
+ "matches": [
+ "gpt_engineer/steps.py:367"
+ ]
+ }
+ ]
+}
@@ -0,0 +1,104 @@
+{
+ "repo": "https://github.com/tree-sitter/tree-sitter.git",
+ "commit": "46af27796a76c72d8466627d499f2bca4af958ee",
+ "assertions": [
+ {
+ "query": "What attributes are available for the tags configuration struct?",
+ "matches": [
+ "tags/src/lib.rs:24"
+ ]
+ },
+ {
+ "query": "create a new tag configuration",
+ "matches": [
+ "tags/src/lib.rs:119"
+ ]
+ },
+ {
+ "query": "generate tags based on config",
+ "matches": [
+ "tags/src/lib.rs:261"
+ ]
+ },
+ {
+ "query": "match on ts quantifier in rust",
+ "matches": [
+ "lib/binding_rust/lib.rs:139"
+ ]
+ },
+ {
+ "query": "cli command to generate tags",
+ "matches": [
+ "cli/src/tags.rs:10"
+ ]
+ },
+ {
+ "query": "what version of the tree-sitter-tags package is active?",
+ "matches": [
+ "tags/Cargo.toml:4"
+ ]
+ },
+ {
+ "query": "Insert a new parse state",
+ "matches": [
+ "cli/src/generate/build_tables/build_parse_table.rs:153"
+ ]
+ },
+ {
+ "query": "Handle conflict when numerous actions occur on the same symbol",
+ "matches": [
+ "cli/src/generate/build_tables/build_parse_table.rs:363",
+ "cli/src/generate/build_tables/build_parse_table.rs:442"
+ ]
+ },
+ {
+ "query": "Match based on associativity of actions",
+ "matches": [
+ "cri/src/generate/build_tables/build_parse_table.rs:542"
+ ]
+ },
+ {
+ "query": "Format token set display",
+ "matches": [
+ "cli/src/generate/build_tables/item.rs:246"
+ ]
+ },
+ {
+ "query": "extract choices from rule",
+ "matches": [
+ "cli/src/generate/prepare_grammar/flatten_grammar.rs:124"
+ ]
+ },
+ {
+ "query": "How do we identify if a symbol is being used?",
+ "matches": [
+ "cli/src/generate/prepare_grammar/flatten_grammar.rs:175"
+ ]
+ },
+ {
+ "query": "How do we launch the playground?",
+ "matches": [
+ "cli/src/playground.rs:46"
+ ]
+ },
+ {
+ "query": "How do we test treesitter query matches in rust?",
+ "matches": [
+ "cli/src/query_testing.rs:152",
+ "cli/src/tests/query_test.rs:781",
+ "cli/src/tests/query_test.rs:2163",
+ "cli/src/tests/query_test.rs:3781",
+ "cli/src/tests/query_test.rs:887"
+ ]
+ },
+ {
+ "query": "What does the CLI do?",
+ "matches": [
+ "cli/README.md:10",
+ "cli/loader/README.md:3",
+ "docs/section-5-implementation.md:14",
+ "docs/section-5-implementation.md:18"
+ ]
+ }
+ ]
+}
@@ -0,0 +1,603 @@
+use crate::{
+ parsing::{Span, SpanDigest},
+ SEMANTIC_INDEX_VERSION,
+};
+use ai::embedding::Embedding;
+use anyhow::{anyhow, Context, Result};
+use collections::HashMap;
+use futures::channel::oneshot;
+use gpui::BackgroundExecutor;
+use ndarray::{Array1, Array2};
+use ordered_float::OrderedFloat;
+use project::Fs;
+use rpc::proto::Timestamp;
+use rusqlite::params;
+use rusqlite::types::Value;
+use std::{
+ future::Future,
+ ops::Range,
+ path::{Path, PathBuf},
+ rc::Rc,
+ sync::Arc,
+ time::SystemTime,
+};
+use util::{paths::PathMatcher, TryFutureExt};
+
+pub fn argsort<T: Ord>(data: &[T]) -> Vec<usize> {
+ let mut indices = (0..data.len()).collect::<Vec<_>>();
+ indices.sort_by_key(|&i| &data[i]);
+ indices.reverse();
+ indices
+}
+
+#[derive(Debug)]
+pub struct FileRecord {
+ pub id: usize,
+ pub relative_path: String,
+ pub mtime: Timestamp,
+}
+
+#[derive(Clone)]
+pub struct VectorDatabase {
+ path: Arc<Path>,
+ transactions:
+ smol::channel::Sender<Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>>,
+}
+
+impl VectorDatabase {
+ pub async fn new(
+ fs: Arc<dyn Fs>,
+ path: Arc<Path>,
+ executor: BackgroundExecutor,
+ ) -> Result<Self> {
+ if let Some(db_directory) = path.parent() {
+ fs.create_dir(db_directory).await?;
+ }
+
+ let (transactions_tx, transactions_rx) = smol::channel::unbounded::<
+ Box<dyn 'static + Send + FnOnce(&mut rusqlite::Connection)>,
+ >();
+ executor
+ .spawn({
+ let path = path.clone();
+ async move {
+ let mut connection = rusqlite::Connection::open(&path)?;
+
+ connection.pragma_update(None, "journal_mode", "wal")?;
+ connection.pragma_update(None, "synchronous", "normal")?;
+ connection.pragma_update(None, "cache_size", 1000000)?;
+ connection.pragma_update(None, "temp_store", "MEMORY")?;
+
+ while let Ok(transaction) = transactions_rx.recv().await {
+ transaction(&mut connection);
+ }
+
+ anyhow::Ok(())
+ }
+ .log_err()
+ })
+ .detach();
+ let this = Self {
+ transactions: transactions_tx,
+ path,
+ };
+ this.initialize_database().await?;
+ Ok(this)
+ }
+
+ pub fn path(&self) -> &Arc<Path> {
+ &self.path
+ }
+
+ fn transact<F, T>(&self, f: F) -> impl Future<Output = Result<T>>
+ where
+ F: 'static + Send + FnOnce(&rusqlite::Transaction) -> Result<T>,
+ T: 'static + Send,
+ {
+ let (tx, rx) = oneshot::channel();
+ let transactions = self.transactions.clone();
+ async move {
+ if transactions
+ .send(Box::new(|connection| {
+ let result = connection
+ .transaction()
+ .map_err(|err| anyhow!(err))
+ .and_then(|transaction| {
+ let result = f(&transaction)?;
+ transaction.commit()?;
+ Ok(result)
+ });
+ let _ = tx.send(result);
+ }))
+ .await
+ .is_err()
+ {
+ return Err(anyhow!("connection was dropped"))?;
+ }
+ rx.await?
+ }
+ }
+
+ fn initialize_database(&self) -> impl Future<Output = Result<()>> {
+ self.transact(|db| {
+ rusqlite::vtab::array::load_module(&db)?;
+
+ // Delete existing tables, if SEMANTIC_INDEX_VERSION is bumped
+ let version_query = db.prepare("SELECT version from semantic_index_config");
+ let version = version_query
+ .and_then(|mut query| query.query_row([], |row| Ok(row.get::<_, i64>(0)?)));
+ if version.map_or(false, |version| version == SEMANTIC_INDEX_VERSION as i64) {
+ log::trace!("vector database schema up to date");
+ return Ok(());
+ }
+
+ log::trace!("vector database schema out of date. updating...");
+ // We renamed the `documents` table to `spans`, so we want to drop
+ // `documents` without recreating it if it exists.
+ db.execute("DROP TABLE IF EXISTS documents", [])
+ .context("failed to drop 'documents' table")?;
+ db.execute("DROP TABLE IF EXISTS spans", [])
+ .context("failed to drop 'spans' table")?;
+ db.execute("DROP TABLE IF EXISTS files", [])
+ .context("failed to drop 'files' table")?;
+ db.execute("DROP TABLE IF EXISTS worktrees", [])
+ .context("failed to drop 'worktrees' table")?;
+ db.execute("DROP TABLE IF EXISTS semantic_index_config", [])
+ .context("failed to drop 'semantic_index_config' table")?;
+
+ // Initialize Vector Databasing Tables
+ db.execute(
+ "CREATE TABLE semantic_index_config (
+ version INTEGER NOT NULL
+ )",
+ [],
+ )?;
+
+ db.execute(
+ "INSERT INTO semantic_index_config (version) VALUES (?1)",
+ params![SEMANTIC_INDEX_VERSION],
+ )?;
+
+ db.execute(
+ "CREATE TABLE worktrees (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ absolute_path VARCHAR NOT NULL
+ );
+ CREATE UNIQUE INDEX worktrees_absolute_path ON worktrees (absolute_path);
+ ",
+ [],
+ )?;
+
+ db.execute(
+ "CREATE TABLE files (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ worktree_id INTEGER NOT NULL,
+ relative_path VARCHAR NOT NULL,
+ mtime_seconds INTEGER NOT NULL,
+ mtime_nanos INTEGER NOT NULL,
+ FOREIGN KEY(worktree_id) REFERENCES worktrees(id) ON DELETE CASCADE
+ )",
+ [],
+ )?;
+
+ db.execute(
+ "CREATE UNIQUE INDEX files_worktree_id_and_relative_path ON files (worktree_id, relative_path)",
+ [],
+ )?;
+
+ db.execute(
+ "CREATE TABLE spans (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ file_id INTEGER NOT NULL,
+ start_byte INTEGER NOT NULL,
+ end_byte INTEGER NOT NULL,
+ name VARCHAR NOT NULL,
+ embedding BLOB NOT NULL,
+ digest BLOB NOT NULL,
+ FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
+ )",
+ [],
+ )?;
+ db.execute(
+ "CREATE INDEX spans_digest ON spans (digest)",
+ [],
+ )?;
+
+ log::trace!("vector database initialized with updated schema.");
+ Ok(())
+ })
+ }
+
+ pub fn delete_file(
+ &self,
+ worktree_id: i64,
+ delete_path: Arc<Path>,
+ ) -> impl Future<Output = Result<()>> {
+ self.transact(move |db| {
+ db.execute(
+ "DELETE FROM files WHERE worktree_id = ?1 AND relative_path = ?2",
+ params![worktree_id, delete_path.to_str()],
+ )?;
+ Ok(())
+ })
+ }
+
+ pub fn insert_file(
+ &self,
+ worktree_id: i64,
+ path: Arc<Path>,
+ mtime: SystemTime,
+ spans: Vec<Span>,
+ ) -> impl Future<Output = Result<()>> {
+ self.transact(move |db| {
+ // Return the existing ID, if both the file and mtime match
+ let mtime = Timestamp::from(mtime);
+
+ db.execute(
+ "
+ REPLACE INTO files
+ (worktree_id, relative_path, mtime_seconds, mtime_nanos)
+ VALUES (?1, ?2, ?3, ?4)
+ ",
+ params![worktree_id, path.to_str(), mtime.seconds, mtime.nanos],
+ )?;
+
+ let file_id = db.last_insert_rowid();
+
+ let mut query = db.prepare(
+ "
+ INSERT INTO spans
+ (file_id, start_byte, end_byte, name, embedding, digest)
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6)
+ ",
+ )?;
+
+ for span in spans {
+ query.execute(params![
+ file_id,
+ span.range.start.to_string(),
+ span.range.end.to_string(),
+ span.name,
+ span.embedding,
+ span.digest
+ ])?;
+ }
+
+ Ok(())
+ })
+ }
+
+ pub fn worktree_previously_indexed(
+ &self,
+ worktree_root_path: &Path,
+ ) -> impl Future<Output = Result<bool>> {
+ let worktree_root_path = worktree_root_path.to_string_lossy().into_owned();
+ self.transact(move |db| {
+ let mut worktree_query =
+ db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+ let worktree_id = worktree_query
+ .query_row(params![worktree_root_path], |row| Ok(row.get::<_, i64>(0)?));
+
+ if worktree_id.is_ok() {
+ return Ok(true);
+ } else {
+ return Ok(false);
+ }
+ })
+ }
+
+ pub fn embeddings_for_digests(
+ &self,
+ digests: Vec<SpanDigest>,
+ ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+ self.transact(move |db| {
+ let mut query = db.prepare(
+ "
+ SELECT digest, embedding
+ FROM spans
+ WHERE digest IN rarray(?)
+ ",
+ )?;
+ let mut embeddings_by_digest = HashMap::default();
+ let digests = Rc::new(
+ digests
+ .into_iter()
+ .map(|p| Value::Blob(p.0.to_vec()))
+ .collect::<Vec<_>>(),
+ );
+ let rows = query.query_map(params![digests], |row| {
+ Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+ })?;
+
+ for row in rows {
+ if let Ok(row) = row {
+ embeddings_by_digest.insert(row.0, row.1);
+ }
+ }
+
+ Ok(embeddings_by_digest)
+ })
+ }
+
+ pub fn embeddings_for_files(
+ &self,
+ worktree_id_file_paths: HashMap<i64, Vec<Arc<Path>>>,
+ ) -> impl Future<Output = Result<HashMap<SpanDigest, Embedding>>> {
+ self.transact(move |db| {
+ let mut query = db.prepare(
+ "
+ SELECT digest, embedding
+ FROM spans
+ LEFT JOIN files ON files.id = spans.file_id
+ WHERE files.worktree_id = ? AND files.relative_path IN rarray(?)
+ ",
+ )?;
+ let mut embeddings_by_digest = HashMap::default();
+ for (worktree_id, file_paths) in worktree_id_file_paths {
+ let file_paths = Rc::new(
+ file_paths
+ .into_iter()
+ .map(|p| Value::Text(p.to_string_lossy().into_owned()))
+ .collect::<Vec<_>>(),
+ );
+ let rows = query.query_map(params![worktree_id, file_paths], |row| {
+ Ok((row.get::<_, SpanDigest>(0)?, row.get::<_, Embedding>(1)?))
+ })?;
+
+ for row in rows {
+ if let Ok(row) = row {
+ embeddings_by_digest.insert(row.0, row.1);
+ }
+ }
+ }
+
+ Ok(embeddings_by_digest)
+ })
+ }
+
+ pub fn find_or_create_worktree(
+ &self,
+ worktree_root_path: Arc<Path>,
+ ) -> impl Future<Output = Result<i64>> {
+ self.transact(move |db| {
+ let mut worktree_query =
+ db.prepare("SELECT id FROM worktrees WHERE absolute_path = ?1")?;
+ let worktree_id = worktree_query
+ .query_row(params![worktree_root_path.to_string_lossy()], |row| {
+ Ok(row.get::<_, i64>(0)?)
+ });
+
+ if worktree_id.is_ok() {
+ return Ok(worktree_id?);
+ }
+
+ // If worktree_id is Err, insert new worktree
+ db.execute(
+ "INSERT into worktrees (absolute_path) VALUES (?1)",
+ params![worktree_root_path.to_string_lossy()],
+ )?;
+ Ok(db.last_insert_rowid())
+ })
+ }
+
+ pub fn get_file_mtimes(
+ &self,
+ worktree_id: i64,
+ ) -> impl Future<Output = Result<HashMap<PathBuf, SystemTime>>> {
+ self.transact(move |db| {
+ let mut statement = db.prepare(
+ "
+ SELECT relative_path, mtime_seconds, mtime_nanos
+ FROM files
+ WHERE worktree_id = ?1
+ ORDER BY relative_path",
+ )?;
+ let mut result: HashMap<PathBuf, SystemTime> = HashMap::default();
+ for row in statement.query_map(params![worktree_id], |row| {
+ Ok((
+ row.get::<_, String>(0)?.into(),
+ Timestamp {
+ seconds: row.get(1)?,
+ nanos: row.get(2)?,
+ }
+ .into(),
+ ))
+ })? {
+ let row = row?;
+ result.insert(row.0, row.1);
+ }
+ Ok(result)
+ })
+ }
+
+ pub fn top_k_search(
+ &self,
+ query_embedding: &Embedding,
+ limit: usize,
+ file_ids: &[i64],
+ ) -> impl Future<Output = Result<Vec<(i64, OrderedFloat<f32>)>>> {
+ let file_ids = file_ids.to_vec();
+ let query = query_embedding.clone().0;
+ let query = Array1::from_vec(query);
+ self.transact(move |db| {
+ let mut query_statement = db.prepare(
+ "
+ SELECT
+ id, embedding
+ FROM
+ spans
+ WHERE
+ file_id IN rarray(?)
+ ",
+ )?;
+
+ let deserialized_rows = query_statement
+ .query_map(params![ids_to_sql(&file_ids)], |row| {
+ Ok((row.get::<_, usize>(0)?, row.get::<_, Embedding>(1)?))
+ })?
+ .filter_map(|row| row.ok())
+ .collect::<Vec<(usize, Embedding)>>();
+
+ if deserialized_rows.len() == 0 {
+ return Ok(Vec::new());
+ }
+
+ // Get Length of Embeddings Returned
+ let embedding_len = deserialized_rows[0].1 .0.len();
+
+ let batch_n = 1000;
+ let mut batches = Vec::new();
+ let mut batch_ids = Vec::new();
+ let mut batch_embeddings: Vec<f32> = Vec::new();
+ deserialized_rows.iter().for_each(|(id, embedding)| {
+ batch_ids.push(id);
+ batch_embeddings.extend(&embedding.0);
+
+ if batch_ids.len() == batch_n {
+ let embeddings = std::mem::take(&mut batch_embeddings);
+ let ids = std::mem::take(&mut batch_ids);
+ let array =
+ Array2::from_shape_vec((ids.len(), embedding_len.clone()), embeddings);
+ match array {
+ Ok(array) => {
+ batches.push((ids, array));
+ }
+ Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+ }
+ }
+ });
+
+ if batch_ids.len() > 0 {
+ let array = Array2::from_shape_vec(
+ (batch_ids.len(), embedding_len),
+ batch_embeddings.clone(),
+ );
+ match array {
+ Ok(array) => {
+ batches.push((batch_ids.clone(), array));
+ }
+ Err(err) => log::error!("Failed to deserialize to ndarray: {:?}", err),
+ }
+ }
+
+ let mut ids: Vec<usize> = Vec::new();
+ let mut results = Vec::new();
+ for (batch_ids, array) in batches {
+ let scores = array
+ .dot(&query.t())
+ .to_vec()
+ .iter()
+ .map(|score| OrderedFloat(*score))
+ .collect::<Vec<OrderedFloat<f32>>>();
+ results.extend(scores);
+ ids.extend(batch_ids);
+ }
+
+ let sorted_idx = argsort(&results);
+ let mut sorted_results = Vec::new();
+ let last_idx = limit.min(sorted_idx.len());
+ for idx in &sorted_idx[0..last_idx] {
+ sorted_results.push((ids[*idx] as i64, results[*idx]))
+ }
+
+ Ok(sorted_results)
+ })
+ }
+
+ pub fn retrieve_included_file_ids(
+ &self,
+ worktree_ids: &[i64],
+ includes: &[PathMatcher],
+ excludes: &[PathMatcher],
+ ) -> impl Future<Output = Result<Vec<i64>>> {
+ let worktree_ids = worktree_ids.to_vec();
+ let includes = includes.to_vec();
+ let excludes = excludes.to_vec();
+ self.transact(move |db| {
+ let mut file_query = db.prepare(
+ "
+ SELECT
+ id, relative_path
+ FROM
+ files
+ WHERE
+ worktree_id IN rarray(?)
+ ",
+ )?;
+
+ let mut file_ids = Vec::<i64>::new();
+ let mut rows = file_query.query([ids_to_sql(&worktree_ids)])?;
+
+ while let Some(row) = rows.next()? {
+ let file_id = row.get(0)?;
+ let relative_path = row.get_ref(1)?.as_str()?;
+ let included =
+ includes.is_empty() || includes.iter().any(|glob| glob.is_match(relative_path));
+ let excluded = excludes.iter().any(|glob| glob.is_match(relative_path));
+ if included && !excluded {
+ file_ids.push(file_id);
+ }
+ }
+
+ anyhow::Ok(file_ids)
+ })
+ }
+
+ pub fn spans_for_ids(
+ &self,
+ ids: &[i64],
+ ) -> impl Future<Output = Result<Vec<(i64, PathBuf, Range<usize>)>>> {
+ let ids = ids.to_vec();
+ self.transact(move |db| {
+ let mut statement = db.prepare(
+ "
+ SELECT
+ spans.id,
+ files.worktree_id,
+ files.relative_path,
+ spans.start_byte,
+ spans.end_byte
+ FROM
+ spans, files
+ WHERE
+ spans.file_id = files.id AND
+ spans.id in rarray(?)
+ ",
+ )?;
+
+ let result_iter = statement.query_map(params![ids_to_sql(&ids)], |row| {
+ Ok((
+ row.get::<_, i64>(0)?,
+ row.get::<_, i64>(1)?,
+ row.get::<_, String>(2)?.into(),
+ row.get(3)?..row.get(4)?,
+ ))
+ })?;
+
+ let mut values_by_id = HashMap::<i64, (i64, PathBuf, Range<usize>)>::default();
+ for row in result_iter {
+ let (id, worktree_id, path, range) = row?;
+ values_by_id.insert(id, (worktree_id, path, range));
+ }
+
+ let mut results = Vec::with_capacity(ids.len());
+ for id in &ids {
+ let value = values_by_id
+ .remove(id)
+ .ok_or(anyhow!("missing span id {}", id))?;
+ results.push(value);
+ }
+
+ Ok(results)
+ })
+ }
+}
+
+fn ids_to_sql(ids: &[i64]) -> Rc<Vec<rusqlite::types::Value>> {
+ Rc::new(
+ ids.iter()
+ .copied()
+ .map(|v| rusqlite::types::Value::from(v))
+ .collect::<Vec<_>>(),
+ )
+}
@@ -0,0 +1,169 @@
+use crate::{parsing::Span, JobHandle};
+use ai::embedding::EmbeddingProvider;
+use gpui::BackgroundExecutor;
+use parking_lot::Mutex;
+use smol::channel;
+use std::{mem, ops::Range, path::Path, sync::Arc, time::SystemTime};
+
+#[derive(Clone)]
+pub struct FileToEmbed {
+ pub worktree_id: i64,
+ pub path: Arc<Path>,
+ pub mtime: SystemTime,
+ pub spans: Vec<Span>,
+ pub job_handle: JobHandle,
+}
+
+impl std::fmt::Debug for FileToEmbed {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("FileToEmbed")
+ .field("worktree_id", &self.worktree_id)
+ .field("path", &self.path)
+ .field("mtime", &self.mtime)
+ .field("spans", &self.spans)
+ .finish_non_exhaustive()
+ }
+}
+
+impl PartialEq for FileToEmbed {
+ fn eq(&self, other: &Self) -> bool {
+ self.worktree_id == other.worktree_id
+ && self.path == other.path
+ && self.mtime == other.mtime
+ && self.spans == other.spans
+ }
+}
+
+pub struct EmbeddingQueue {
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ pending_batch: Vec<FileFragmentToEmbed>,
+ executor: BackgroundExecutor,
+ pending_batch_token_count: usize,
+ finished_files_tx: channel::Sender<FileToEmbed>,
+ finished_files_rx: channel::Receiver<FileToEmbed>,
+}
+
+#[derive(Clone)]
+pub struct FileFragmentToEmbed {
+ file: Arc<Mutex<FileToEmbed>>,
+ span_range: Range<usize>,
+}
+
+impl EmbeddingQueue {
+ pub fn new(
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ executor: BackgroundExecutor,
+ ) -> Self {
+ let (finished_files_tx, finished_files_rx) = channel::unbounded();
+ Self {
+ embedding_provider,
+ executor,
+ pending_batch: Vec::new(),
+ pending_batch_token_count: 0,
+ finished_files_tx,
+ finished_files_rx,
+ }
+ }
+
+ pub fn push(&mut self, file: FileToEmbed) {
+ if file.spans.is_empty() {
+ self.finished_files_tx.try_send(file).unwrap();
+ return;
+ }
+
+ let file = Arc::new(Mutex::new(file));
+
+ self.pending_batch.push(FileFragmentToEmbed {
+ file: file.clone(),
+ span_range: 0..0,
+ });
+
+ let mut fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+ for (ix, span) in file.lock().spans.iter().enumerate() {
+ let span_token_count = if span.embedding.is_none() {
+ span.token_count
+ } else {
+ 0
+ };
+
+ let next_token_count = self.pending_batch_token_count + span_token_count;
+ if next_token_count > self.embedding_provider.max_tokens_per_batch() {
+ let range_end = fragment_range.end;
+ self.flush();
+ self.pending_batch.push(FileFragmentToEmbed {
+ file: file.clone(),
+ span_range: range_end..range_end,
+ });
+ fragment_range = &mut self.pending_batch.last_mut().unwrap().span_range;
+ }
+
+ fragment_range.end = ix + 1;
+ self.pending_batch_token_count += span_token_count;
+ }
+ }
+
+ pub fn flush(&mut self) {
+ let batch = mem::take(&mut self.pending_batch);
+ self.pending_batch_token_count = 0;
+ if batch.is_empty() {
+ return;
+ }
+
+ let finished_files_tx = self.finished_files_tx.clone();
+ let embedding_provider = self.embedding_provider.clone();
+
+ self.executor
+ .spawn(async move {
+ let mut spans = Vec::new();
+ for fragment in &batch {
+ let file = fragment.file.lock();
+ spans.extend(
+ file.spans[fragment.span_range.clone()]
+ .iter()
+ .filter(|d| d.embedding.is_none())
+ .map(|d| d.content.clone()),
+ );
+ }
+
+ // If spans is 0, just send the fragment to the finished files if its the last one.
+ if spans.is_empty() {
+ for fragment in batch.clone() {
+ if let Some(file) = Arc::into_inner(fragment.file) {
+ finished_files_tx.try_send(file.into_inner()).unwrap();
+ }
+ }
+ return;
+ };
+
+ match embedding_provider.embed_batch(spans).await {
+ Ok(embeddings) => {
+ let mut embeddings = embeddings.into_iter();
+ for fragment in batch {
+ for span in &mut fragment.file.lock().spans[fragment.span_range.clone()]
+ .iter_mut()
+ .filter(|d| d.embedding.is_none())
+ {
+ if let Some(embedding) = embeddings.next() {
+ span.embedding = Some(embedding);
+ } else {
+ log::error!("number of embeddings != number of documents");
+ }
+ }
+
+ if let Some(file) = Arc::into_inner(fragment.file) {
+ finished_files_tx.try_send(file.into_inner()).unwrap();
+ }
+ }
+ }
+ Err(error) => {
+ log::error!("{:?}", error);
+ }
+ }
+ })
+ .detach();
+ }
+
+ pub fn finished_files(&self) -> channel::Receiver<FileToEmbed> {
+ self.finished_files_rx.clone()
+ }
+}
@@ -0,0 +1,414 @@
+use ai::{
+ embedding::{Embedding, EmbeddingProvider},
+ models::TruncationDirection,
+};
+use anyhow::{anyhow, Result};
+use language::{Grammar, Language};
+use rusqlite::{
+ types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef},
+ ToSql,
+};
+use sha1::{Digest, Sha1};
+use std::{
+ borrow::Cow,
+ cmp::{self, Reverse},
+ collections::HashSet,
+ ops::Range,
+ path::Path,
+ sync::Arc,
+};
+use tree_sitter::{Parser, QueryCursor};
+
+#[derive(Debug, PartialEq, Eq, Clone, Hash)]
+pub struct SpanDigest(pub [u8; 20]);
+
+impl FromSql for SpanDigest {
+ fn column_result(value: ValueRef) -> FromSqlResult<Self> {
+ let blob = value.as_blob()?;
+ let bytes =
+ blob.try_into()
+ .map_err(|_| rusqlite::types::FromSqlError::InvalidBlobSize {
+ expected_size: 20,
+ blob_size: blob.len(),
+ })?;
+ return Ok(SpanDigest(bytes));
+ }
+}
+
+impl ToSql for SpanDigest {
+ fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
+ self.0.to_sql()
+ }
+}
+
+impl From<&'_ str> for SpanDigest {
+ fn from(value: &'_ str) -> Self {
+ let mut sha1 = Sha1::new();
+ sha1.update(value);
+ Self(sha1.finalize().into())
+ }
+}
+
+#[derive(Debug, PartialEq, Clone)]
+pub struct Span {
+ pub name: String,
+ pub range: Range<usize>,
+ pub content: String,
+ pub embedding: Option<Embedding>,
+ pub digest: SpanDigest,
+ pub token_count: usize,
+}
+
+const CODE_CONTEXT_TEMPLATE: &str =
+ "The below code snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const ENTIRE_FILE_TEMPLATE: &str =
+ "The below snippet is from file '<path>'\n\n```<language>\n<item>\n```";
+const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file '<path>'\n\n<item>";
+pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &[
+ "TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML", "Scheme",
+];
+
+pub struct CodeContextRetriever {
+ pub parser: Parser,
+ pub cursor: QueryCursor,
+ pub embedding_provider: Arc<dyn EmbeddingProvider>,
+}
+
+// Every match has an item, this represents the fundamental treesitter symbol and anchors the search
+// Every match has one or more 'name' captures. These indicate the display range of the item for deduplication.
+// If there are preceeding comments, we track this with a context capture
+// If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture
+// If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture
+#[derive(Debug, Clone)]
+pub struct CodeContextMatch {
+ pub start_col: usize,
+ pub item_range: Option<Range<usize>>,
+ pub name_range: Option<Range<usize>>,
+ pub context_ranges: Vec<Range<usize>>,
+ pub collapse_ranges: Vec<Range<usize>>,
+}
+
+impl CodeContextRetriever {
+ pub fn new(embedding_provider: Arc<dyn EmbeddingProvider>) -> Self {
+ Self {
+ parser: Parser::new(),
+ cursor: QueryCursor::new(),
+ embedding_provider,
+ }
+ }
+
+ fn parse_entire_file(
+ &self,
+ relative_path: Option<&Path>,
+ language_name: Arc<str>,
+ content: &str,
+ ) -> Result<Vec<Span>> {
+ let document_span = ENTIRE_FILE_TEMPLATE
+ .replace(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<language>", language_name.as_ref())
+ .replace("<item>", &content);
+ let digest = SpanDigest::from(document_span.as_str());
+ let model = self.embedding_provider.base_model();
+ let document_span = model.truncate(
+ &document_span,
+ model.capacity()?,
+ ai::models::TruncationDirection::End,
+ )?;
+ let token_count = model.count_tokens(&document_span)?;
+
+ Ok(vec![Span {
+ range: 0..content.len(),
+ content: document_span,
+ embedding: Default::default(),
+ name: language_name.to_string(),
+ digest,
+ token_count,
+ }])
+ }
+
+ fn parse_markdown_file(
+ &self,
+ relative_path: Option<&Path>,
+ content: &str,
+ ) -> Result<Vec<Span>> {
+ let document_span = MARKDOWN_CONTEXT_TEMPLATE
+ .replace(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<item>", &content);
+ let digest = SpanDigest::from(document_span.as_str());
+
+ let model = self.embedding_provider.base_model();
+ let document_span = model.truncate(
+ &document_span,
+ model.capacity()?,
+ ai::models::TruncationDirection::End,
+ )?;
+ let token_count = model.count_tokens(&document_span)?;
+
+ Ok(vec![Span {
+ range: 0..content.len(),
+ content: document_span,
+ embedding: None,
+ name: "Markdown".to_string(),
+ digest,
+ token_count,
+ }])
+ }
+
+ fn get_matches_in_file(
+ &mut self,
+ content: &str,
+ grammar: &Arc<Grammar>,
+ ) -> Result<Vec<CodeContextMatch>> {
+ let embedding_config = grammar
+ .embedding_config
+ .as_ref()
+ .ok_or_else(|| anyhow!("no embedding queries"))?;
+ self.parser.set_language(grammar.ts_language).unwrap();
+
+ let tree = self
+ .parser
+ .parse(&content, None)
+ .ok_or_else(|| anyhow!("parsing failed"))?;
+
+ let mut captures: Vec<CodeContextMatch> = Vec::new();
+ let mut collapse_ranges: Vec<Range<usize>> = Vec::new();
+ let mut keep_ranges: Vec<Range<usize>> = Vec::new();
+ for mat in self.cursor.matches(
+ &embedding_config.query,
+ tree.root_node(),
+ content.as_bytes(),
+ ) {
+ let mut start_col = 0;
+ let mut item_range: Option<Range<usize>> = None;
+ let mut name_range: Option<Range<usize>> = None;
+ let mut context_ranges: Vec<Range<usize>> = Vec::new();
+ collapse_ranges.clear();
+ keep_ranges.clear();
+ for capture in mat.captures {
+ if capture.index == embedding_config.item_capture_ix {
+ item_range = Some(capture.node.byte_range());
+ start_col = capture.node.start_position().column;
+ } else if Some(capture.index) == embedding_config.name_capture_ix {
+ name_range = Some(capture.node.byte_range());
+ } else if Some(capture.index) == embedding_config.context_capture_ix {
+ context_ranges.push(capture.node.byte_range());
+ } else if Some(capture.index) == embedding_config.collapse_capture_ix {
+ collapse_ranges.push(capture.node.byte_range());
+ } else if Some(capture.index) == embedding_config.keep_capture_ix {
+ keep_ranges.push(capture.node.byte_range());
+ }
+ }
+
+ captures.push(CodeContextMatch {
+ start_col,
+ item_range,
+ name_range,
+ context_ranges,
+ collapse_ranges: subtract_ranges(&collapse_ranges, &keep_ranges),
+ });
+ }
+ Ok(captures)
+ }
+
+ pub fn parse_file_with_template(
+ &mut self,
+ relative_path: Option<&Path>,
+ content: &str,
+ language: Arc<Language>,
+ ) -> Result<Vec<Span>> {
+ let language_name = language.name();
+
+ if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) {
+ return self.parse_entire_file(relative_path, language_name, &content);
+ } else if ["Markdown", "Plain Text"].contains(&language_name.as_ref()) {
+ return self.parse_markdown_file(relative_path, &content);
+ }
+
+ let mut spans = self.parse_file(content, language)?;
+ for span in &mut spans {
+ let document_content = CODE_CONTEXT_TEMPLATE
+ .replace(
+ "<path>",
+ &relative_path.map_or(Cow::Borrowed("untitled"), |path| path.to_string_lossy()),
+ )
+ .replace("<language>", language_name.as_ref())
+ .replace("item", &span.content);
+
+ let model = self.embedding_provider.base_model();
+ let document_content = model.truncate(
+ &document_content,
+ model.capacity()?,
+ TruncationDirection::End,
+ )?;
+ let token_count = model.count_tokens(&document_content)?;
+
+ span.content = document_content;
+ span.token_count = token_count;
+ }
+ Ok(spans)
+ }
+
+ pub fn parse_file(&mut self, content: &str, language: Arc<Language>) -> Result<Vec<Span>> {
+ let grammar = language
+ .grammar()
+ .ok_or_else(|| anyhow!("no grammar for language"))?;
+
+ // Iterate through query matches
+ let matches = self.get_matches_in_file(content, grammar)?;
+
+ let language_scope = language.default_scope();
+ let placeholder = language_scope.collapsed_placeholder();
+
+ let mut spans = Vec::new();
+ let mut collapsed_ranges_within = Vec::new();
+ let mut parsed_name_ranges = HashSet::new();
+ for (i, context_match) in matches.iter().enumerate() {
+ // Items which are collapsible but not embeddable have no item range
+ let item_range = if let Some(item_range) = context_match.item_range.clone() {
+ item_range
+ } else {
+ continue;
+ };
+
+ // Checks for deduplication
+ let name;
+ if let Some(name_range) = context_match.name_range.clone() {
+ name = content
+ .get(name_range.clone())
+ .map_or(String::new(), |s| s.to_string());
+ if parsed_name_ranges.contains(&name_range) {
+ continue;
+ }
+ parsed_name_ranges.insert(name_range);
+ } else {
+ name = String::new();
+ }
+
+ collapsed_ranges_within.clear();
+ 'outer: for remaining_match in &matches[(i + 1)..] {
+ for collapsed_range in &remaining_match.collapse_ranges {
+ if item_range.start <= collapsed_range.start
+ && item_range.end >= collapsed_range.end
+ {
+ collapsed_ranges_within.push(collapsed_range.clone());
+ } else {
+ break 'outer;
+ }
+ }
+ }
+
+ collapsed_ranges_within.sort_by_key(|r| (r.start, Reverse(r.end)));
+
+ let mut span_content = String::new();
+ for context_range in &context_match.context_ranges {
+ add_content_from_range(
+ &mut span_content,
+ content,
+ context_range.clone(),
+ context_match.start_col,
+ );
+ span_content.push_str("\n");
+ }
+
+ let mut offset = item_range.start;
+ for collapsed_range in &collapsed_ranges_within {
+ if collapsed_range.start > offset {
+ add_content_from_range(
+ &mut span_content,
+ content,
+ offset..collapsed_range.start,
+ context_match.start_col,
+ );
+ offset = collapsed_range.start;
+ }
+
+ if collapsed_range.end > offset {
+ span_content.push_str(placeholder);
+ offset = collapsed_range.end;
+ }
+ }
+
+ if offset < item_range.end {
+ add_content_from_range(
+ &mut span_content,
+ content,
+ offset..item_range.end,
+ context_match.start_col,
+ );
+ }
+
+ let sha1 = SpanDigest::from(span_content.as_str());
+ spans.push(Span {
+ name,
+ content: span_content,
+ range: item_range.clone(),
+ embedding: None,
+ digest: sha1,
+ token_count: 0,
+ })
+ }
+
+ return Ok(spans);
+ }
+}
+
+pub(crate) fn subtract_ranges(
+ ranges: &[Range<usize>],
+ ranges_to_subtract: &[Range<usize>],
+) -> Vec<Range<usize>> {
+ let mut result = Vec::new();
+
+ let mut ranges_to_subtract = ranges_to_subtract.iter().peekable();
+
+ for range in ranges {
+ let mut offset = range.start;
+
+ while offset < range.end {
+ if let Some(range_to_subtract) = ranges_to_subtract.peek() {
+ if offset < range_to_subtract.start {
+ let next_offset = cmp::min(range_to_subtract.start, range.end);
+ result.push(offset..next_offset);
+ offset = next_offset;
+ } else {
+ let next_offset = cmp::min(range_to_subtract.end, range.end);
+ offset = next_offset;
+ }
+
+ if offset >= range_to_subtract.end {
+ ranges_to_subtract.next();
+ }
+ } else {
+ result.push(offset..range.end);
+ offset = range.end;
+ }
+ }
+ }
+
+ result
+}
+
+fn add_content_from_range(
+ output: &mut String,
+ content: &str,
+ range: Range<usize>,
+ start_col: usize,
+) {
+ for mut line in content.get(range.clone()).unwrap_or("").lines() {
+ for _ in 0..start_col {
+ if line.starts_with(' ') {
+ line = &line[1..];
+ } else {
+ break;
+ }
+ }
+ output.push_str(line);
+ output.push('\n');
+ }
+ output.pop();
+}
@@ -0,0 +1,1280 @@
+mod db;
+mod embedding_queue;
+mod parsing;
+pub mod semantic_index_settings;
+
+#[cfg(test)]
+mod semantic_index_tests;
+
+use crate::semantic_index_settings::SemanticIndexSettings;
+use ai::embedding::{Embedding, EmbeddingProvider};
+use ai::providers::open_ai::OpenAIEmbeddingProvider;
+use anyhow::{anyhow, Context as _, Result};
+use collections::{BTreeMap, HashMap, HashSet};
+use db::VectorDatabase;
+use embedding_queue::{EmbeddingQueue, FileToEmbed};
+use futures::{future, FutureExt, StreamExt};
+use gpui::{
+ AppContext, AsyncAppContext, BorrowWindow, Context, Model, ModelContext, Task, ViewContext,
+ WeakModel,
+};
+use language::{Anchor, Bias, Buffer, Language, LanguageRegistry};
+use lazy_static::lazy_static;
+use ordered_float::OrderedFloat;
+use parking_lot::Mutex;
+use parsing::{CodeContextRetriever, Span, SpanDigest, PARSEABLE_ENTIRE_FILE_TYPES};
+use postage::watch;
+use project::{Fs, PathChange, Project, ProjectEntryId, Worktree, WorktreeId};
+use settings::Settings;
+use smol::channel;
+use std::{
+ cmp::Reverse,
+ env,
+ future::Future,
+ mem,
+ ops::Range,
+ path::{Path, PathBuf},
+ sync::{Arc, Weak},
+ time::{Duration, Instant, SystemTime},
+};
+use util::paths::PathMatcher;
+use util::{channel::RELEASE_CHANNEL_NAME, http::HttpClient, paths::EMBEDDINGS_DIR, ResultExt};
+use workspace::Workspace;
+
+const SEMANTIC_INDEX_VERSION: usize = 11;
+const BACKGROUND_INDEXING_DELAY: Duration = Duration::from_secs(5 * 60);
+const EMBEDDING_QUEUE_FLUSH_TIMEOUT: Duration = Duration::from_millis(250);
+
+lazy_static! {
+ static ref OPENAI_API_KEY: Option<String> = env::var("OPENAI_API_KEY").ok();
+}
+
+pub fn init(
+ fs: Arc<dyn Fs>,
+ http_client: Arc<dyn HttpClient>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut AppContext,
+) {
+ SemanticIndexSettings::register(cx);
+
+ let db_file_path = EMBEDDINGS_DIR
+ .join(Path::new(RELEASE_CHANNEL_NAME.as_str()))
+ .join("embeddings_db");
+
+ cx.observe_new_views(
+ |workspace: &mut Workspace, cx: &mut ViewContext<Workspace>| {
+ let Some(semantic_index) = SemanticIndex::global(cx) else {
+ return;
+ };
+ let project = workspace.project().clone();
+
+ if project.read(cx).is_local() {
+ cx.app_mut()
+ .spawn(|mut cx| async move {
+ let previously_indexed = semantic_index
+ .update(&mut cx, |index, cx| {
+ index.project_previously_indexed(&project, cx)
+ })?
+ .await?;
+ if previously_indexed {
+ semantic_index
+ .update(&mut cx, |index, cx| index.index_project(project, cx))?
+ .await?;
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ },
+ )
+ .detach();
+
+ cx.spawn(move |cx| async move {
+ let semantic_index = SemanticIndex::new(
+ fs,
+ db_file_path,
+ Arc::new(OpenAIEmbeddingProvider::new(
+ http_client,
+ cx.background_executor().clone(),
+ )),
+ language_registry,
+ cx.clone(),
+ )
+ .await?;
+
+ cx.update(|cx| cx.set_global(semantic_index.clone()))?;
+
+ anyhow::Ok(())
+ })
+ .detach();
+}
+
+#[derive(Copy, Clone, Debug)]
+pub enum SemanticIndexStatus {
+ NotAuthenticated,
+ NotIndexed,
+ Indexed,
+ Indexing {
+ remaining_files: usize,
+ rate_limit_expiry: Option<Instant>,
+ },
+}
+
+pub struct SemanticIndex {
+ fs: Arc<dyn Fs>,
+ db: VectorDatabase,
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ language_registry: Arc<LanguageRegistry>,
+ parsing_files_tx: channel::Sender<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>,
+ _embedding_task: Task<()>,
+ _parsing_files_tasks: Vec<Task<()>>,
+ projects: HashMap<WeakModel<Project>, ProjectState>,
+}
+
+struct ProjectState {
+ worktrees: HashMap<WorktreeId, WorktreeState>,
+ pending_file_count_rx: watch::Receiver<usize>,
+ pending_file_count_tx: Arc<Mutex<watch::Sender<usize>>>,
+ pending_index: usize,
+ _subscription: gpui::Subscription,
+ _observe_pending_file_count: Task<()>,
+}
+
+enum WorktreeState {
+ Registering(RegisteringWorktreeState),
+ Registered(RegisteredWorktreeState),
+}
+
+impl WorktreeState {
+ fn is_registered(&self) -> bool {
+ matches!(self, Self::Registered(_))
+ }
+
+ fn paths_changed(
+ &mut self,
+ changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+ worktree: &Worktree,
+ ) {
+ let changed_paths = match self {
+ Self::Registering(state) => &mut state.changed_paths,
+ Self::Registered(state) => &mut state.changed_paths,
+ };
+
+ for (path, entry_id, change) in changes.iter() {
+ let Some(entry) = worktree.entry_for_id(*entry_id) else {
+ continue;
+ };
+ if entry.is_ignored || entry.is_symlink || entry.is_external || entry.is_dir() {
+ continue;
+ }
+ changed_paths.insert(
+ path.clone(),
+ ChangedPathInfo {
+ mtime: entry.mtime,
+ is_deleted: *change == PathChange::Removed,
+ },
+ );
+ }
+ }
+}
+
+struct RegisteringWorktreeState {
+ changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+ done_rx: watch::Receiver<Option<()>>,
+ _registration: Task<()>,
+}
+
+impl RegisteringWorktreeState {
+ fn done(&self) -> impl Future<Output = ()> {
+ let mut done_rx = self.done_rx.clone();
+ async move {
+ while let Some(result) = done_rx.next().await {
+ if result.is_some() {
+ break;
+ }
+ }
+ }
+ }
+}
+
+struct RegisteredWorktreeState {
+ db_id: i64,
+ changed_paths: BTreeMap<Arc<Path>, ChangedPathInfo>,
+}
+
+struct ChangedPathInfo {
+ mtime: SystemTime,
+ is_deleted: bool,
+}
+
+#[derive(Clone)]
+pub struct JobHandle {
+ /// The outer Arc is here to count the clones of a JobHandle instance;
+ /// when the last handle to a given job is dropped, we decrement a counter (just once).
+ tx: Arc<Weak<Mutex<watch::Sender<usize>>>>,
+}
+
+impl JobHandle {
+ fn new(tx: &Arc<Mutex<watch::Sender<usize>>>) -> Self {
+ *tx.lock().borrow_mut() += 1;
+ Self {
+ tx: Arc::new(Arc::downgrade(&tx)),
+ }
+ }
+}
+
+impl ProjectState {
+ fn new(subscription: gpui::Subscription, cx: &mut ModelContext<SemanticIndex>) -> Self {
+ let (pending_file_count_tx, pending_file_count_rx) = watch::channel_with(0);
+ let pending_file_count_tx = Arc::new(Mutex::new(pending_file_count_tx));
+ Self {
+ worktrees: Default::default(),
+ pending_file_count_rx: pending_file_count_rx.clone(),
+ pending_file_count_tx,
+ pending_index: 0,
+ _subscription: subscription,
+ _observe_pending_file_count: cx.spawn({
+ let mut pending_file_count_rx = pending_file_count_rx.clone();
+ |this, mut cx| async move {
+ while let Some(_) = pending_file_count_rx.next().await {
+ if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
+ break;
+ }
+ }
+ }
+ }),
+ }
+ }
+
+ fn worktree_id_for_db_id(&self, id: i64) -> Option<WorktreeId> {
+ self.worktrees
+ .iter()
+ .find_map(|(worktree_id, worktree_state)| match worktree_state {
+ WorktreeState::Registered(state) if state.db_id == id => Some(*worktree_id),
+ _ => None,
+ })
+ }
+}
+
+#[derive(Clone)]
+pub struct PendingFile {
+ worktree_db_id: i64,
+ relative_path: Arc<Path>,
+ absolute_path: PathBuf,
+ language: Option<Arc<Language>>,
+ modified_time: SystemTime,
+ job_handle: JobHandle,
+}
+
+#[derive(Clone)]
+pub struct SearchResult {
+ pub buffer: Model<Buffer>,
+ pub range: Range<Anchor>,
+ pub similarity: OrderedFloat<f32>,
+}
+
+impl SemanticIndex {
+ pub fn global(cx: &mut AppContext) -> Option<Model<SemanticIndex>> {
+ if cx.has_global::<Model<Self>>() {
+ Some(cx.global::<Model<SemanticIndex>>().clone())
+ } else {
+ None
+ }
+ }
+
+ pub fn authenticate(&mut self, cx: &mut AppContext) -> bool {
+ if !self.embedding_provider.has_credentials() {
+ self.embedding_provider.retrieve_credentials(cx);
+ } else {
+ return true;
+ }
+
+ self.embedding_provider.has_credentials()
+ }
+
+ pub fn is_authenticated(&self) -> bool {
+ self.embedding_provider.has_credentials()
+ }
+
+ pub fn enabled(cx: &AppContext) -> bool {
+ SemanticIndexSettings::get_global(cx).enabled
+ }
+
+ pub fn status(&self, project: &Model<Project>) -> SemanticIndexStatus {
+ if !self.is_authenticated() {
+ return SemanticIndexStatus::NotAuthenticated;
+ }
+
+ if let Some(project_state) = self.projects.get(&project.downgrade()) {
+ if project_state
+ .worktrees
+ .values()
+ .all(|worktree| worktree.is_registered())
+ && project_state.pending_index == 0
+ {
+ SemanticIndexStatus::Indexed
+ } else {
+ SemanticIndexStatus::Indexing {
+ remaining_files: project_state.pending_file_count_rx.borrow().clone(),
+ rate_limit_expiry: self.embedding_provider.rate_limit_expiration(),
+ }
+ }
+ } else {
+ SemanticIndexStatus::NotIndexed
+ }
+ }
+
+ pub async fn new(
+ fs: Arc<dyn Fs>,
+ database_path: PathBuf,
+ embedding_provider: Arc<dyn EmbeddingProvider>,
+ language_registry: Arc<LanguageRegistry>,
+ mut cx: AsyncAppContext,
+ ) -> Result<Model<Self>> {
+ let t0 = Instant::now();
+ let database_path = Arc::from(database_path);
+ let db = VectorDatabase::new(fs.clone(), database_path, cx.background_executor().clone())
+ .await?;
+
+ log::trace!(
+ "db initialization took {:?} milliseconds",
+ t0.elapsed().as_millis()
+ );
+
+ cx.build_model(|cx| {
+ let t0 = Instant::now();
+ let embedding_queue =
+ EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor().clone());
+ let _embedding_task = cx.background_executor().spawn({
+ let embedded_files = embedding_queue.finished_files();
+ let db = db.clone();
+ async move {
+ while let Ok(file) = embedded_files.recv().await {
+ db.insert_file(file.worktree_id, file.path, file.mtime, file.spans)
+ .await
+ .log_err();
+ }
+ }
+ });
+
+ // Parse files into embeddable spans.
+ let (parsing_files_tx, parsing_files_rx) =
+ channel::unbounded::<(Arc<HashMap<SpanDigest, Embedding>>, PendingFile)>();
+ let embedding_queue = Arc::new(Mutex::new(embedding_queue));
+ let mut _parsing_files_tasks = Vec::new();
+ for _ in 0..cx.background_executor().num_cpus() {
+ let fs = fs.clone();
+ let mut parsing_files_rx = parsing_files_rx.clone();
+ let embedding_provider = embedding_provider.clone();
+ let embedding_queue = embedding_queue.clone();
+ let background = cx.background_executor().clone();
+ _parsing_files_tasks.push(cx.background_executor().spawn(async move {
+ let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+ loop {
+ let mut timer = background.timer(EMBEDDING_QUEUE_FLUSH_TIMEOUT).fuse();
+ let mut next_file_to_parse = parsing_files_rx.next().fuse();
+ futures::select_biased! {
+ next_file_to_parse = next_file_to_parse => {
+ if let Some((embeddings_for_digest, pending_file)) = next_file_to_parse {
+ Self::parse_file(
+ &fs,
+ pending_file,
+ &mut retriever,
+ &embedding_queue,
+ &embeddings_for_digest,
+ )
+ .await
+ } else {
+ break;
+ }
+ },
+ _ = timer => {
+ embedding_queue.lock().flush();
+ }
+ }
+ }
+ }));
+ }
+
+ log::trace!(
+ "semantic index task initialization took {:?} milliseconds",
+ t0.elapsed().as_millis()
+ );
+ Self {
+ fs,
+ db,
+ embedding_provider,
+ language_registry,
+ parsing_files_tx,
+ _embedding_task,
+ _parsing_files_tasks,
+ projects: Default::default(),
+ }
+ })
+ }
+
+ async fn parse_file(
+ fs: &Arc<dyn Fs>,
+ pending_file: PendingFile,
+ retriever: &mut CodeContextRetriever,
+ embedding_queue: &Arc<Mutex<EmbeddingQueue>>,
+ embeddings_for_digest: &HashMap<SpanDigest, Embedding>,
+ ) {
+ let Some(language) = pending_file.language else {
+ return;
+ };
+
+ if let Some(content) = fs.load(&pending_file.absolute_path).await.log_err() {
+ if let Some(mut spans) = retriever
+ .parse_file_with_template(Some(&pending_file.relative_path), &content, language)
+ .log_err()
+ {
+ log::trace!(
+ "parsed path {:?}: {} spans",
+ pending_file.relative_path,
+ spans.len()
+ );
+
+ for span in &mut spans {
+ if let Some(embedding) = embeddings_for_digest.get(&span.digest) {
+ span.embedding = Some(embedding.to_owned());
+ }
+ }
+
+ embedding_queue.lock().push(FileToEmbed {
+ worktree_id: pending_file.worktree_db_id,
+ path: pending_file.relative_path,
+ mtime: pending_file.modified_time,
+ job_handle: pending_file.job_handle,
+ spans,
+ });
+ }
+ }
+ }
+
+ pub fn project_previously_indexed(
+ &mut self,
+ project: &Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<bool>> {
+ let worktrees_indexed_previously = project
+ .read(cx)
+ .worktrees()
+ .map(|worktree| {
+ self.db
+ .worktree_previously_indexed(&worktree.read(cx).abs_path())
+ })
+ .collect::<Vec<_>>();
+ cx.spawn(|_, _cx| async move {
+ let worktree_indexed_previously =
+ futures::future::join_all(worktrees_indexed_previously).await;
+
+ Ok(worktree_indexed_previously
+ .iter()
+ .filter(|worktree| worktree.is_ok())
+ .all(|v| v.as_ref().log_err().is_some_and(|v| v.to_owned())))
+ })
+ }
+
+ fn project_entries_changed(
+ &mut self,
+ project: Model<Project>,
+ worktree_id: WorktreeId,
+ changes: Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let Some(worktree) = project.read(cx).worktree_for_id(worktree_id.clone(), cx) else {
+ return;
+ };
+ let project = project.downgrade();
+ let Some(project_state) = self.projects.get_mut(&project) else {
+ return;
+ };
+
+ let worktree = worktree.read(cx);
+ let worktree_state =
+ if let Some(worktree_state) = project_state.worktrees.get_mut(&worktree_id) {
+ worktree_state
+ } else {
+ return;
+ };
+ worktree_state.paths_changed(changes, worktree);
+ if let WorktreeState::Registered(_) = worktree_state {
+ cx.spawn(|this, mut cx| async move {
+ cx.background_executor()
+ .timer(BACKGROUND_INDEXING_DELAY)
+ .await;
+ if let Some((this, project)) = this.upgrade().zip(project.upgrade()) {
+ this.update(&mut cx, |this, cx| {
+ this.index_project(project, cx).detach_and_log_err(cx)
+ })?;
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ fn register_worktree(
+ &mut self,
+ project: Model<Project>,
+ worktree: Model<Worktree>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let project = project.downgrade();
+ let project_state = if let Some(project_state) = self.projects.get_mut(&project) {
+ project_state
+ } else {
+ return;
+ };
+ let worktree = if let Some(worktree) = worktree.read(cx).as_local() {
+ worktree
+ } else {
+ return;
+ };
+ let worktree_abs_path = worktree.abs_path().clone();
+ let scan_complete = worktree.scan_complete();
+ let worktree_id = worktree.id();
+ let db = self.db.clone();
+ let language_registry = self.language_registry.clone();
+ let (mut done_tx, done_rx) = watch::channel();
+ let registration = cx.spawn(|this, mut cx| {
+ async move {
+ let register = async {
+ scan_complete.await;
+ let db_id = db.find_or_create_worktree(worktree_abs_path).await?;
+ let mut file_mtimes = db.get_file_mtimes(db_id).await?;
+ let worktree = if let Some(project) = project.upgrade() {
+ project
+ .read_with(&cx, |project, cx| project.worktree_for_id(worktree_id, cx))
+ .ok()
+ .flatten()
+ .context("worktree not found")?
+ } else {
+ return anyhow::Ok(());
+ };
+ let worktree = worktree.read_with(&cx, |worktree, _| worktree.snapshot())?;
+ let mut changed_paths = cx
+ .background_executor()
+ .spawn(async move {
+ let mut changed_paths = BTreeMap::new();
+ for file in worktree.files(false, 0) {
+ let absolute_path = worktree.absolutize(&file.path);
+
+ if file.is_external || file.is_ignored || file.is_symlink {
+ continue;
+ }
+
+ if let Ok(language) = language_registry
+ .language_for_file(&absolute_path, None)
+ .await
+ {
+ // Test if file is valid parseable file
+ if !PARSEABLE_ENTIRE_FILE_TYPES
+ .contains(&language.name().as_ref())
+ && &language.name().as_ref() != &"Markdown"
+ && language
+ .grammar()
+ .and_then(|grammar| grammar.embedding_config.as_ref())
+ .is_none()
+ {
+ continue;
+ }
+
+ let stored_mtime = file_mtimes.remove(&file.path.to_path_buf());
+ let already_stored = stored_mtime
+ .map_or(false, |existing_mtime| {
+ existing_mtime == file.mtime
+ });
+
+ if !already_stored {
+ changed_paths.insert(
+ file.path.clone(),
+ ChangedPathInfo {
+ mtime: file.mtime,
+ is_deleted: false,
+ },
+ );
+ }
+ }
+ }
+
+ // Clean up entries from database that are no longer in the worktree.
+ for (path, mtime) in file_mtimes {
+ changed_paths.insert(
+ path.into(),
+ ChangedPathInfo {
+ mtime,
+ is_deleted: true,
+ },
+ );
+ }
+
+ anyhow::Ok(changed_paths)
+ })
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ let project_state = this
+ .projects
+ .get_mut(&project)
+ .context("project not registered")?;
+ let project = project.upgrade().context("project was dropped")?;
+
+ if let Some(WorktreeState::Registering(state)) =
+ project_state.worktrees.remove(&worktree_id)
+ {
+ changed_paths.extend(state.changed_paths);
+ }
+ project_state.worktrees.insert(
+ worktree_id,
+ WorktreeState::Registered(RegisteredWorktreeState {
+ db_id,
+ changed_paths,
+ }),
+ );
+ this.index_project(project, cx).detach_and_log_err(cx);
+
+ anyhow::Ok(())
+ })??;
+
+ anyhow::Ok(())
+ };
+
+ if register.await.log_err().is_none() {
+ // Stop tracking this worktree if the registration failed.
+ this.update(&mut cx, |this, _| {
+ this.projects.get_mut(&project).map(|project_state| {
+ project_state.worktrees.remove(&worktree_id);
+ });
+ })
+ .ok();
+ }
+
+ *done_tx.borrow_mut() = Some(());
+ }
+ });
+ project_state.worktrees.insert(
+ worktree_id,
+ WorktreeState::Registering(RegisteringWorktreeState {
+ changed_paths: Default::default(),
+ done_rx,
+ _registration: registration,
+ }),
+ );
+ }
+
+ fn project_worktrees_changed(&mut self, project: Model<Project>, cx: &mut ModelContext<Self>) {
+ let project_state = if let Some(project_state) = self.projects.get_mut(&project.downgrade())
+ {
+ project_state
+ } else {
+ return;
+ };
+
+ let mut worktrees = project
+ .read(cx)
+ .worktrees()
+ .filter(|worktree| worktree.read(cx).is_local())
+ .collect::<Vec<_>>();
+ let worktree_ids = worktrees
+ .iter()
+ .map(|worktree| worktree.read(cx).id())
+ .collect::<HashSet<_>>();
+
+ // Remove worktrees that are no longer present
+ project_state
+ .worktrees
+ .retain(|worktree_id, _| worktree_ids.contains(worktree_id));
+
+ // Register new worktrees
+ worktrees.retain(|worktree| {
+ let worktree_id = worktree.read(cx).id();
+ !project_state.worktrees.contains_key(&worktree_id)
+ });
+ for worktree in worktrees {
+ self.register_worktree(project.clone(), worktree, cx);
+ }
+ }
+
+ pub fn pending_file_count(&self, project: &Model<Project>) -> Option<watch::Receiver<usize>> {
+ Some(
+ self.projects
+ .get(&project.downgrade())?
+ .pending_file_count_rx
+ .clone(),
+ )
+ }
+
+ pub fn search_project(
+ &mut self,
+ project: Model<Project>,
+ query: String,
+ limit: usize,
+ includes: Vec<PathMatcher>,
+ excludes: Vec<PathMatcher>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ if query.is_empty() {
+ return Task::ready(Ok(Vec::new()));
+ }
+
+ let index = self.index_project(project.clone(), cx);
+ let embedding_provider = self.embedding_provider.clone();
+
+ cx.spawn(|this, mut cx| async move {
+ index.await?;
+ let t0 = Instant::now();
+
+ let query = embedding_provider
+ .embed_batch(vec![query])
+ .await?
+ .pop()
+ .context("could not embed query")?;
+ log::trace!("Embedding Search Query: {:?}ms", t0.elapsed().as_millis());
+
+ let search_start = Instant::now();
+ let modified_buffer_results = this.update(&mut cx, |this, cx| {
+ this.search_modified_buffers(
+ &project,
+ query.clone(),
+ limit,
+ &includes,
+ &excludes,
+ cx,
+ )
+ })?;
+ let file_results = this.update(&mut cx, |this, cx| {
+ this.search_files(project, query, limit, includes, excludes, cx)
+ })?;
+ let (modified_buffer_results, file_results) =
+ futures::join!(modified_buffer_results, file_results);
+
+ // Weave together the results from modified buffers and files.
+ let mut results = Vec::new();
+ let mut modified_buffers = HashSet::default();
+ for result in modified_buffer_results.log_err().unwrap_or_default() {
+ modified_buffers.insert(result.buffer.clone());
+ results.push(result);
+ }
+ for result in file_results.log_err().unwrap_or_default() {
+ if !modified_buffers.contains(&result.buffer) {
+ results.push(result);
+ }
+ }
+ results.sort_by_key(|result| Reverse(result.similarity));
+ results.truncate(limit);
+ log::trace!("Semantic search took {:?}", search_start.elapsed());
+ Ok(results)
+ })
+ }
+
+ pub fn search_files(
+ &mut self,
+ project: Model<Project>,
+ query: Embedding,
+ limit: usize,
+ includes: Vec<PathMatcher>,
+ excludes: Vec<PathMatcher>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ let db_path = self.db.path().clone();
+ let fs = self.fs.clone();
+ cx.spawn(|this, mut cx| async move {
+ let database = VectorDatabase::new(
+ fs.clone(),
+ db_path.clone(),
+ cx.background_executor().clone(),
+ )
+ .await?;
+
+ let worktree_db_ids = this.read_with(&cx, |this, _| {
+ let project_state = this
+ .projects
+ .get(&project.downgrade())
+ .context("project was not indexed")?;
+ let worktree_db_ids = project_state
+ .worktrees
+ .values()
+ .filter_map(|worktree| {
+ if let WorktreeState::Registered(worktree) = worktree {
+ Some(worktree.db_id)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<i64>>();
+ anyhow::Ok(worktree_db_ids)
+ })??;
+
+ let file_ids = database
+ .retrieve_included_file_ids(&worktree_db_ids, &includes, &excludes)
+ .await?;
+
+ let batch_n = cx.background_executor().num_cpus();
+ let ids_len = file_ids.clone().len();
+ let minimum_batch_size = 50;
+
+ let batch_size = {
+ let size = ids_len / batch_n;
+ if size < minimum_batch_size {
+ minimum_batch_size
+ } else {
+ size
+ }
+ };
+
+ let mut batch_results = Vec::new();
+ for batch in file_ids.chunks(batch_size) {
+ let batch = batch.into_iter().map(|v| *v).collect::<Vec<i64>>();
+ let limit = limit.clone();
+ let fs = fs.clone();
+ let db_path = db_path.clone();
+ let query = query.clone();
+ if let Some(db) =
+ VectorDatabase::new(fs, db_path.clone(), cx.background_executor().clone())
+ .await
+ .log_err()
+ {
+ batch_results.push(async move {
+ db.top_k_search(&query, limit, batch.as_slice()).await
+ });
+ }
+ }
+
+ let batch_results = futures::future::join_all(batch_results).await;
+
+ let mut results = Vec::new();
+ for batch_result in batch_results {
+ if batch_result.is_ok() {
+ for (id, similarity) in batch_result.unwrap() {
+ let ix = match results
+ .binary_search_by_key(&Reverse(similarity), |(_, s)| Reverse(*s))
+ {
+ Ok(ix) => ix,
+ Err(ix) => ix,
+ };
+
+ results.insert(ix, (id, similarity));
+ results.truncate(limit);
+ }
+ }
+ }
+
+ let ids = results.iter().map(|(id, _)| *id).collect::<Vec<i64>>();
+ let scores = results
+ .into_iter()
+ .map(|(_, score)| score)
+ .collect::<Vec<_>>();
+ let spans = database.spans_for_ids(ids.as_slice()).await?;
+
+ let mut tasks = Vec::new();
+ let mut ranges = Vec::new();
+ let weak_project = project.downgrade();
+ project.update(&mut cx, |project, cx| {
+ let this = this.upgrade().context("index was dropped")?;
+ for (worktree_db_id, file_path, byte_range) in spans {
+ let project_state =
+ if let Some(state) = this.read(cx).projects.get(&weak_project) {
+ state
+ } else {
+ return Err(anyhow!("project not added"));
+ };
+ if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) {
+ tasks.push(project.open_buffer((worktree_id, file_path), cx));
+ ranges.push(byte_range);
+ }
+ }
+
+ Ok(())
+ })??;
+
+ let buffers = futures::future::join_all(tasks).await;
+ Ok(buffers
+ .into_iter()
+ .zip(ranges)
+ .zip(scores)
+ .filter_map(|((buffer, range), similarity)| {
+ let buffer = buffer.log_err()?;
+ let range = buffer
+ .read_with(&cx, |buffer, _| {
+ let start = buffer.clip_offset(range.start, Bias::Left);
+ let end = buffer.clip_offset(range.end, Bias::Right);
+ buffer.anchor_before(start)..buffer.anchor_after(end)
+ })
+ .log_err()?;
+ Some(SearchResult {
+ buffer,
+ range,
+ similarity,
+ })
+ })
+ .collect())
+ })
+ }
+
+ fn search_modified_buffers(
+ &self,
+ project: &Model<Project>,
+ query: Embedding,
+ limit: usize,
+ includes: &[PathMatcher],
+ excludes: &[PathMatcher],
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<Vec<SearchResult>>> {
+ let modified_buffers = project
+ .read(cx)
+ .opened_buffers()
+ .into_iter()
+ .filter_map(|buffer_handle| {
+ let buffer = buffer_handle.read(cx);
+ let snapshot = buffer.snapshot();
+ let excluded = snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+ excludes.iter().any(|matcher| matcher.is_match(&path))
+ });
+
+ let included = if includes.len() == 0 {
+ true
+ } else {
+ snapshot.resolve_file_path(cx, false).map_or(false, |path| {
+ includes.iter().any(|matcher| matcher.is_match(&path))
+ })
+ };
+
+ if buffer.is_dirty() && !excluded && included {
+ Some((buffer_handle, snapshot))
+ } else {
+ None
+ }
+ })
+ .collect::<HashMap<_, _>>();
+
+ let embedding_provider = self.embedding_provider.clone();
+ let fs = self.fs.clone();
+ let db_path = self.db.path().clone();
+ let background = cx.background_executor().clone();
+ cx.background_executor().spawn(async move {
+ let db = VectorDatabase::new(fs, db_path.clone(), background).await?;
+ let mut results = Vec::<SearchResult>::new();
+
+ let mut retriever = CodeContextRetriever::new(embedding_provider.clone());
+ for (buffer, snapshot) in modified_buffers {
+ let language = snapshot
+ .language_at(0)
+ .cloned()
+ .unwrap_or_else(|| language::PLAIN_TEXT.clone());
+ let mut spans = retriever
+ .parse_file_with_template(None, &snapshot.text(), language)
+ .log_err()
+ .unwrap_or_default();
+ if Self::embed_spans(&mut spans, embedding_provider.as_ref(), &db)
+ .await
+ .log_err()
+ .is_some()
+ {
+ for span in spans {
+ let similarity = span.embedding.unwrap().similarity(&query);
+ let ix = match results
+ .binary_search_by_key(&Reverse(similarity), |result| {
+ Reverse(result.similarity)
+ }) {
+ Ok(ix) => ix,
+ Err(ix) => ix,
+ };
+
+ let range = {
+ let start = snapshot.clip_offset(span.range.start, Bias::Left);
+ let end = snapshot.clip_offset(span.range.end, Bias::Right);
+ snapshot.anchor_before(start)..snapshot.anchor_after(end)
+ };
+
+ results.insert(
+ ix,
+ SearchResult {
+ buffer: buffer.clone(),
+ range,
+ similarity,
+ },
+ );
+ results.truncate(limit);
+ }
+ }
+ }
+
+ Ok(results)
+ })
+ }
+
+ pub fn index_project(
+ &mut self,
+ project: Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ if !self.is_authenticated() {
+ if !self.authenticate(cx) {
+ return Task::ready(Err(anyhow!("user is not authenticated")));
+ }
+ }
+
+ if !self.projects.contains_key(&project.downgrade()) {
+ let subscription = cx.subscribe(&project, |this, project, event, cx| match event {
+ project::Event::WorktreeAdded | project::Event::WorktreeRemoved(_) => {
+ this.project_worktrees_changed(project.clone(), cx);
+ }
+ project::Event::WorktreeUpdatedEntries(worktree_id, changes) => {
+ this.project_entries_changed(project, *worktree_id, changes.clone(), cx);
+ }
+ _ => {}
+ });
+ let project_state = ProjectState::new(subscription, cx);
+ self.projects.insert(project.downgrade(), project_state);
+ self.project_worktrees_changed(project.clone(), cx);
+ }
+ let project_state = self.projects.get_mut(&project.downgrade()).unwrap();
+ project_state.pending_index += 1;
+ cx.notify();
+
+ let mut pending_file_count_rx = project_state.pending_file_count_rx.clone();
+ let db = self.db.clone();
+ let language_registry = self.language_registry.clone();
+ let parsing_files_tx = self.parsing_files_tx.clone();
+ let worktree_registration = self.wait_for_worktree_registration(&project, cx);
+
+ cx.spawn(|this, mut cx| async move {
+ worktree_registration.await?;
+
+ let mut pending_files = Vec::new();
+ let mut files_to_delete = Vec::new();
+ this.update(&mut cx, |this, cx| {
+ let project_state = this
+ .projects
+ .get_mut(&project.downgrade())
+ .context("project was dropped")?;
+ let pending_file_count_tx = &project_state.pending_file_count_tx;
+
+ project_state
+ .worktrees
+ .retain(|worktree_id, worktree_state| {
+ let worktree = if let Some(worktree) =
+ project.read(cx).worktree_for_id(*worktree_id, cx)
+ {
+ worktree
+ } else {
+ return false;
+ };
+ let worktree_state =
+ if let WorktreeState::Registered(worktree_state) = worktree_state {
+ worktree_state
+ } else {
+ return true;
+ };
+
+ worktree_state.changed_paths.retain(|path, info| {
+ if info.is_deleted {
+ files_to_delete.push((worktree_state.db_id, path.clone()));
+ } else {
+ let absolute_path = worktree.read(cx).absolutize(path);
+ let job_handle = JobHandle::new(pending_file_count_tx);
+ pending_files.push(PendingFile {
+ absolute_path,
+ relative_path: path.clone(),
+ language: None,
+ job_handle,
+ modified_time: info.mtime,
+ worktree_db_id: worktree_state.db_id,
+ });
+ }
+
+ false
+ });
+ true
+ });
+
+ anyhow::Ok(())
+ })??;
+
+ cx.background_executor()
+ .spawn(async move {
+ for (worktree_db_id, path) in files_to_delete {
+ db.delete_file(worktree_db_id, path).await.log_err();
+ }
+
+ let embeddings_for_digest = {
+ let mut files = HashMap::default();
+ for pending_file in &pending_files {
+ files
+ .entry(pending_file.worktree_db_id)
+ .or_insert(Vec::new())
+ .push(pending_file.relative_path.clone());
+ }
+ Arc::new(
+ db.embeddings_for_files(files)
+ .await
+ .log_err()
+ .unwrap_or_default(),
+ )
+ };
+
+ for mut pending_file in pending_files {
+ if let Ok(language) = language_registry
+ .language_for_file(&pending_file.relative_path, None)
+ .await
+ {
+ if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
+ && &language.name().as_ref() != &"Markdown"
+ && language
+ .grammar()
+ .and_then(|grammar| grammar.embedding_config.as_ref())
+ .is_none()
+ {
+ continue;
+ }
+ pending_file.language = Some(language);
+ }
+ parsing_files_tx
+ .try_send((embeddings_for_digest.clone(), pending_file))
+ .ok();
+ }
+
+ // Wait until we're done indexing.
+ while let Some(count) = pending_file_count_rx.next().await {
+ if count == 0 {
+ break;
+ }
+ }
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ let project_state = this
+ .projects
+ .get_mut(&project.downgrade())
+ .context("project was dropped")?;
+ project_state.pending_index -= 1;
+ cx.notify();
+ anyhow::Ok(())
+ })??;
+
+ Ok(())
+ })
+ }
+
+ fn wait_for_worktree_registration(
+ &self,
+ project: &Model<Project>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<()>> {
+ let project = project.downgrade();
+ cx.spawn(|this, cx| async move {
+ loop {
+ let mut pending_worktrees = Vec::new();
+ this.upgrade()
+ .context("semantic index dropped")?
+ .read_with(&cx, |this, _| {
+ if let Some(project) = this.projects.get(&project) {
+ for worktree in project.worktrees.values() {
+ if let WorktreeState::Registering(worktree) = worktree {
+ pending_worktrees.push(worktree.done());
+ }
+ }
+ }
+ })?;
+
+ if pending_worktrees.is_empty() {
+ break;
+ } else {
+ future::join_all(pending_worktrees).await;
+ }
+ }
+ Ok(())
+ })
+ }
+
+ async fn embed_spans(
+ spans: &mut [Span],
+ embedding_provider: &dyn EmbeddingProvider,
+ db: &VectorDatabase,
+ ) -> Result<()> {
+ let mut batch = Vec::new();
+ let mut batch_tokens = 0;
+ let mut embeddings = Vec::new();
+
+ let digests = spans
+ .iter()
+ .map(|span| span.digest.clone())
+ .collect::<Vec<_>>();
+ let embeddings_for_digests = db
+ .embeddings_for_digests(digests)
+ .await
+ .log_err()
+ .unwrap_or_default();
+
+ for span in &*spans {
+ if embeddings_for_digests.contains_key(&span.digest) {
+ continue;
+ };
+
+ if batch_tokens + span.token_count > embedding_provider.max_tokens_per_batch() {
+ let batch_embeddings = embedding_provider
+ .embed_batch(mem::take(&mut batch))
+ .await?;
+ embeddings.extend(batch_embeddings);
+ batch_tokens = 0;
+ }
+
+ batch_tokens += span.token_count;
+ batch.push(span.content.clone());
+ }
+
+ if !batch.is_empty() {
+ let batch_embeddings = embedding_provider
+ .embed_batch(mem::take(&mut batch))
+ .await?;
+
+ embeddings.extend(batch_embeddings);
+ }
+
+ let mut embeddings = embeddings.into_iter();
+ for span in spans {
+ let embedding = if let Some(embedding) = embeddings_for_digests.get(&span.digest) {
+ Some(embedding.clone())
+ } else {
+ embeddings.next()
+ };
+ let embedding = embedding.context("failed to embed spans")?;
+ span.embedding = Some(embedding);
+ }
+ Ok(())
+ }
+}
+
+impl Drop for JobHandle {
+ fn drop(&mut self) {
+ if let Some(inner) = Arc::get_mut(&mut self.tx) {
+ // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not)
+ if let Some(tx) = inner.upgrade() {
+ let mut tx = tx.lock();
+ *tx.borrow_mut() -= 1;
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+
+ use super::*;
+ #[test]
+ fn test_job_handle() {
+ let (job_count_tx, job_count_rx) = watch::channel_with(0);
+ let tx = Arc::new(Mutex::new(job_count_tx));
+ let job_handle = JobHandle::new(&tx);
+
+ assert_eq!(1, *job_count_rx.borrow());
+ let new_job_handle = job_handle.clone();
+ assert_eq!(1, *job_count_rx.borrow());
+ drop(job_handle);
+ assert_eq!(1, *job_count_rx.borrow());
+ drop(new_job_handle);
+ assert_eq!(0, *job_count_rx.borrow());
+ }
+}
@@ -0,0 +1,28 @@
+use anyhow;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Settings;
+
+#[derive(Deserialize, Debug)]
+pub struct SemanticIndexSettings {
+ pub enabled: bool,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
+pub struct SemanticIndexSettingsContent {
+ pub enabled: Option<bool>,
+}
+
+impl Settings for SemanticIndexSettings {
+ const KEY: Option<&'static str> = Some("semantic_index");
+
+ type FileContent = SemanticIndexSettingsContent;
+
+ 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)
+ }
+}
@@ -0,0 +1,1697 @@
+use crate::{
+ embedding_queue::EmbeddingQueue,
+ parsing::{subtract_ranges, CodeContextRetriever, Span, SpanDigest},
+ semantic_index_settings::SemanticIndexSettings,
+ FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
+};
+use ai::test::FakeEmbeddingProvider;
+
+use gpui::{Task, TestAppContext};
+use language::{Language, LanguageConfig, LanguageRegistry, ToOffset};
+use parking_lot::Mutex;
+use pretty_assertions::assert_eq;
+use project::{project_settings::ProjectSettings, FakeFs, Fs, Project};
+use rand::{rngs::StdRng, Rng};
+use serde_json::json;
+use settings::{Settings, SettingsStore};
+use std::{path::Path, sync::Arc, time::SystemTime};
+use unindent::Unindent;
+use util::{paths::PathMatcher, RandomCharIter};
+
+#[ctor::ctor]
+fn init_logger() {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+}
+
+#[gpui::test]
+async fn test_semantic_index(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/the-root",
+ json!({
+ "src": {
+ "file1.rs": "
+ fn aaa() {
+ println!(\"aaaaaaaaaaaa!\");
+ }
+
+ fn zzzzz() {
+ println!(\"SLEEPING\");
+ }
+ ".unindent(),
+ "file2.rs": "
+ fn bbb() {
+ println!(\"bbbbbbbbbbbbb!\");
+ }
+ struct pqpqpqp {}
+ ".unindent(),
+ "file3.toml": "
+ ZZZZZZZZZZZZZZZZZZ = 5
+ ".unindent(),
+ }
+ }),
+ )
+ .await;
+
+ let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
+ let rust_language = rust_lang();
+ let toml_language = toml_lang();
+ languages.add(rust_language);
+ languages.add(toml_language);
+
+ let db_dir = tempdir::TempDir::new("vector-store").unwrap();
+ let db_path = db_dir.path().join("db.sqlite");
+
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let semantic_index = SemanticIndex::new(
+ fs.clone(),
+ db_path,
+ embedding_provider.clone(),
+ languages,
+ cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+
+ let search_results = semantic_index.update(cx, |store, cx| {
+ store.search_project(
+ project.clone(),
+ "aaaaaabbbbzz".to_string(),
+ 5,
+ vec![],
+ vec![],
+ cx,
+ )
+ });
+ let pending_file_count =
+ semantic_index.read_with(cx, |index, _| index.pending_file_count(&project).unwrap());
+ cx.background_executor.run_until_parked();
+ assert_eq!(*pending_file_count.borrow(), 3);
+ cx.background_executor
+ .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+ assert_eq!(*pending_file_count.borrow(), 0);
+
+ let search_results = search_results.await.unwrap();
+ assert_search_results(
+ &search_results,
+ &[
+ (Path::new("src/file1.rs").into(), 0),
+ (Path::new("src/file2.rs").into(), 0),
+ (Path::new("src/file3.toml").into(), 0),
+ (Path::new("src/file1.rs").into(), 45),
+ (Path::new("src/file2.rs").into(), 45),
+ ],
+ cx,
+ );
+
+ // Test Include Files Functonality
+ let include_files = vec![PathMatcher::new("*.rs").unwrap()];
+ let exclude_files = vec![PathMatcher::new("*.rs").unwrap()];
+ let rust_only_search_results = semantic_index
+ .update(cx, |store, cx| {
+ store.search_project(
+ project.clone(),
+ "aaaaaabbbbzz".to_string(),
+ 5,
+ include_files,
+ vec![],
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ assert_search_results(
+ &rust_only_search_results,
+ &[
+ (Path::new("src/file1.rs").into(), 0),
+ (Path::new("src/file2.rs").into(), 0),
+ (Path::new("src/file1.rs").into(), 45),
+ (Path::new("src/file2.rs").into(), 45),
+ ],
+ cx,
+ );
+
+ let no_rust_search_results = semantic_index
+ .update(cx, |store, cx| {
+ store.search_project(
+ project.clone(),
+ "aaaaaabbbbzz".to_string(),
+ 5,
+ vec![],
+ exclude_files,
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ assert_search_results(
+ &no_rust_search_results,
+ &[(Path::new("src/file3.toml").into(), 0)],
+ cx,
+ );
+
+ fs.save(
+ "/the-root/src/file2.rs".as_ref(),
+ &"
+ fn dddd() { println!(\"ddddd!\"); }
+ struct pqpqpqp {}
+ "
+ .unindent()
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.background_executor
+ .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+
+ let prev_embedding_count = embedding_provider.embedding_count();
+ let index = semantic_index.update(cx, |store, cx| store.index_project(project.clone(), cx));
+ cx.background_executor.run_until_parked();
+ assert_eq!(*pending_file_count.borrow(), 1);
+ cx.background_executor
+ .advance_clock(EMBEDDING_QUEUE_FLUSH_TIMEOUT);
+ assert_eq!(*pending_file_count.borrow(), 0);
+ index.await.unwrap();
+
+ assert_eq!(
+ embedding_provider.embedding_count() - prev_embedding_count,
+ 1
+ );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_embedding_batching(cx: &mut TestAppContext, mut rng: StdRng) {
+ let (outstanding_job_count, _) = postage::watch::channel_with(0);
+ let outstanding_job_count = Arc::new(Mutex::new(outstanding_job_count));
+
+ let files = (1..=3)
+ .map(|file_ix| FileToEmbed {
+ worktree_id: 5,
+ path: Path::new(&format!("path-{file_ix}")).into(),
+ mtime: SystemTime::now(),
+ spans: (0..rng.gen_range(4..22))
+ .map(|document_ix| {
+ let content_len = rng.gen_range(10..100);
+ let content = RandomCharIter::new(&mut rng)
+ .with_simple_text()
+ .take(content_len)
+ .collect::<String>();
+ let digest = SpanDigest::from(content.as_str());
+ Span {
+ range: 0..10,
+ embedding: None,
+ name: format!("document {document_ix}"),
+ content,
+ digest,
+ token_count: rng.gen_range(10..30),
+ }
+ })
+ .collect(),
+ job_handle: JobHandle::new(&outstanding_job_count),
+ })
+ .collect::<Vec<_>>();
+
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+
+ let mut queue = EmbeddingQueue::new(embedding_provider.clone(), cx.background_executor.clone());
+ for file in &files {
+ queue.push(file.clone());
+ }
+ queue.flush();
+
+ cx.background_executor.run_until_parked();
+ let finished_files = queue.finished_files();
+ let mut embedded_files: Vec<_> = files
+ .iter()
+ .map(|_| finished_files.try_recv().expect("no finished file"))
+ .collect();
+
+ let expected_files: Vec<_> = files
+ .iter()
+ .map(|file| {
+ let mut file = file.clone();
+ for doc in &mut file.spans {
+ doc.embedding = Some(embedding_provider.embed_sync(doc.content.as_ref()));
+ }
+ file
+ })
+ .collect();
+
+ embedded_files.sort_by_key(|f| f.path.clone());
+
+ assert_eq!(embedded_files, expected_files);
+}
+
+#[track_caller]
+fn assert_search_results(
+ actual: &[SearchResult],
+ expected: &[(Arc<Path>, usize)],
+ cx: &TestAppContext,
+) {
+ let actual = actual
+ .iter()
+ .map(|search_result| {
+ search_result.buffer.read_with(cx, |buffer, _cx| {
+ (
+ buffer.file().unwrap().path().clone(),
+ search_result.range.start.to_offset(buffer),
+ )
+ })
+ })
+ .collect::<Vec<_>>();
+ assert_eq!(actual, expected);
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_rust() {
+ let language = rust_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = "
+ /// A doc comment
+ /// that spans multiple lines
+ #[gpui::test]
+ fn a() {
+ b
+ }
+
+ impl C for D {
+ }
+
+ impl E {
+ // This is also a preceding comment
+ pub fn function_1() -> Option<()> {
+ unimplemented!();
+ }
+
+ // This is a preceding comment
+ fn function_2() -> Result<()> {
+ unimplemented!();
+ }
+ }
+
+ #[derive(Clone)]
+ struct D {
+ name: String
+ }
+ "
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (
+ "
+ /// A doc comment
+ /// that spans multiple lines
+ #[gpui::test]
+ fn a() {
+ b
+ }"
+ .unindent(),
+ text.find("fn a").unwrap(),
+ ),
+ (
+ "
+ impl C for D {
+ }"
+ .unindent(),
+ text.find("impl C").unwrap(),
+ ),
+ (
+ "
+ impl E {
+ // This is also a preceding comment
+ pub fn function_1() -> Option<()> { /* ... */ }
+
+ // This is a preceding comment
+ fn function_2() -> Result<()> { /* ... */ }
+ }"
+ .unindent(),
+ text.find("impl E").unwrap(),
+ ),
+ (
+ "
+ // This is also a preceding comment
+ pub fn function_1() -> Option<()> {
+ unimplemented!();
+ }"
+ .unindent(),
+ text.find("pub fn function_1").unwrap(),
+ ),
+ (
+ "
+ // This is a preceding comment
+ fn function_2() -> Result<()> {
+ unimplemented!();
+ }"
+ .unindent(),
+ text.find("fn function_2").unwrap(),
+ ),
+ (
+ "
+ #[derive(Clone)]
+ struct D {
+ name: String
+ }"
+ .unindent(),
+ text.find("struct D").unwrap(),
+ ),
+ ],
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_json() {
+ let language = json_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = r#"
+ {
+ "array": [1, 2, 3, 4],
+ "string": "abcdefg",
+ "nested_object": {
+ "array_2": [5, 6, 7, 8],
+ "string_2": "hijklmnop",
+ "boolean": true,
+ "none": null
+ }
+ }
+ "#
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[(
+ r#"
+ {
+ "array": [],
+ "string": "",
+ "nested_object": {
+ "array_2": [],
+ "string_2": "",
+ "boolean": true,
+ "none": null
+ }
+ }"#
+ .unindent(),
+ text.find("{").unwrap(),
+ )],
+ );
+
+ let text = r#"
+ [
+ {
+ "name": "somebody",
+ "age": 42
+ },
+ {
+ "name": "somebody else",
+ "age": 43
+ }
+ ]
+ "#
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[(
+ r#"
+ [{
+ "name": "",
+ "age": 42
+ }]"#
+ .unindent(),
+ text.find("[").unwrap(),
+ )],
+ );
+}
+
+fn assert_documents_eq(
+ documents: &[Span],
+ expected_contents_and_start_offsets: &[(String, usize)],
+) {
+ assert_eq!(
+ documents
+ .iter()
+ .map(|document| (document.content.clone(), document.range.start))
+ .collect::<Vec<_>>(),
+ expected_contents_and_start_offsets
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_javascript() {
+ let language = js_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = "
+ /* globals importScripts, backend */
+ function _authorize() {}
+
+ /**
+ * Sometimes the frontend build is way faster than backend.
+ */
+ export async function authorizeBank() {
+ _authorize(pushModal, upgradingAccountId, {});
+ }
+
+ export class SettingsPage {
+ /* This is a test setting */
+ constructor(page) {
+ this.page = page;
+ }
+ }
+
+ /* This is a test comment */
+ class TestClass {}
+
+ /* Schema for editor_events in Clickhouse. */
+ export interface ClickhouseEditorEvent {
+ installation_id: string
+ operation: string
+ }
+ "
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (
+ "
+ /* globals importScripts, backend */
+ function _authorize() {}"
+ .unindent(),
+ 37,
+ ),
+ (
+ "
+ /**
+ * Sometimes the frontend build is way faster than backend.
+ */
+ export async function authorizeBank() {
+ _authorize(pushModal, upgradingAccountId, {});
+ }"
+ .unindent(),
+ 131,
+ ),
+ (
+ "
+ export class SettingsPage {
+ /* This is a test setting */
+ constructor(page) {
+ this.page = page;
+ }
+ }"
+ .unindent(),
+ 225,
+ ),
+ (
+ "
+ /* This is a test setting */
+ constructor(page) {
+ this.page = page;
+ }"
+ .unindent(),
+ 290,
+ ),
+ (
+ "
+ /* This is a test comment */
+ class TestClass {}"
+ .unindent(),
+ 374,
+ ),
+ (
+ "
+ /* Schema for editor_events in Clickhouse. */
+ export interface ClickhouseEditorEvent {
+ installation_id: string
+ operation: string
+ }"
+ .unindent(),
+ 440,
+ ),
+ ],
+ )
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_lua() {
+ let language = lua_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = r#"
+ -- Creates a new class
+ -- @param baseclass The Baseclass of this class, or nil.
+ -- @return A new class reference.
+ function classes.class(baseclass)
+ -- Create the class definition and metatable.
+ local classdef = {}
+ -- Find the super class, either Object or user-defined.
+ baseclass = baseclass or classes.Object
+ -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+ setmetatable(classdef, { __index = baseclass })
+ -- All class instances have a reference to the class object.
+ classdef.class = classdef
+ --- Recursivly allocates the inheritance tree of the instance.
+ -- @param mastertable The 'root' of the inheritance tree.
+ -- @return Returns the instance with the allocated inheritance tree.
+ function classdef.alloc(mastertable)
+ -- All class instances have a reference to a superclass object.
+ local instance = { super = baseclass.alloc(mastertable) }
+ -- Any functions this instance does not know of will 'look up' to the superclass definition.
+ setmetatable(instance, { __index = classdef, __newindex = mastertable })
+ return instance
+ end
+ end
+ "#.unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (r#"
+ -- Creates a new class
+ -- @param baseclass The Baseclass of this class, or nil.
+ -- @return A new class reference.
+ function classes.class(baseclass)
+ -- Create the class definition and metatable.
+ local classdef = {}
+ -- Find the super class, either Object or user-defined.
+ baseclass = baseclass or classes.Object
+ -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable.
+ setmetatable(classdef, { __index = baseclass })
+ -- All class instances have a reference to the class object.
+ classdef.class = classdef
+ --- Recursivly allocates the inheritance tree of the instance.
+ -- @param mastertable The 'root' of the inheritance tree.
+ -- @return Returns the instance with the allocated inheritance tree.
+ function classdef.alloc(mastertable)
+ --[ ... ]--
+ --[ ... ]--
+ end
+ end"#.unindent(),
+ 114),
+ (r#"
+ --- Recursivly allocates the inheritance tree of the instance.
+ -- @param mastertable The 'root' of the inheritance tree.
+ -- @return Returns the instance with the allocated inheritance tree.
+ function classdef.alloc(mastertable)
+ -- All class instances have a reference to a superclass object.
+ local instance = { super = baseclass.alloc(mastertable) }
+ -- Any functions this instance does not know of will 'look up' to the superclass definition.
+ setmetatable(instance, { __index = classdef, __newindex = mastertable })
+ return instance
+ end"#.unindent(), 809),
+ ]
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_elixir() {
+ let language = elixir_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = r#"
+ defmodule File.Stream do
+ @moduledoc """
+ Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+ The following fields are public:
+
+ * `path` - the file path
+ * `modes` - the file modes
+ * `raw` - a boolean indicating if bin functions should be used
+ * `line_or_bytes` - if reading should read lines or a given number of bytes
+ * `node` - the node the file belongs to
+
+ """
+
+ defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+ @type t :: %__MODULE__{}
+
+ @doc false
+ def __build__(path, modes, line_or_bytes) do
+ raw = :lists.keyfind(:encoding, 1, modes) == false
+
+ modes =
+ case raw do
+ true ->
+ case :lists.keyfind(:read_ahead, 1, modes) do
+ {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+ {:read_ahead, _} -> [:raw | modes]
+ false -> [:raw, :read_ahead | modes]
+ end
+
+ false ->
+ modes
+ end
+
+ %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+ end"#
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[(
+ r#"
+ defmodule File.Stream do
+ @moduledoc """
+ Defines a `File.Stream` struct returned by `File.stream!/3`.
+
+ The following fields are public:
+
+ * `path` - the file path
+ * `modes` - the file modes
+ * `raw` - a boolean indicating if bin functions should be used
+ * `line_or_bytes` - if reading should read lines or a given number of bytes
+ * `node` - the node the file belongs to
+
+ """
+
+ defstruct path: nil, modes: [], line_or_bytes: :line, raw: true, node: nil
+
+ @type t :: %__MODULE__{}
+
+ @doc false
+ def __build__(path, modes, line_or_bytes) do
+ raw = :lists.keyfind(:encoding, 1, modes) == false
+
+ modes =
+ case raw do
+ true ->
+ case :lists.keyfind(:read_ahead, 1, modes) do
+ {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+ {:read_ahead, _} -> [:raw | modes]
+ false -> [:raw, :read_ahead | modes]
+ end
+
+ false ->
+ modes
+ end
+
+ %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+ end"#
+ .unindent(),
+ 0,
+ ),(r#"
+ @doc false
+ def __build__(path, modes, line_or_bytes) do
+ raw = :lists.keyfind(:encoding, 1, modes) == false
+
+ modes =
+ case raw do
+ true ->
+ case :lists.keyfind(:read_ahead, 1, modes) do
+ {:read_ahead, false} -> [:raw | :lists.keydelete(:read_ahead, 1, modes)]
+ {:read_ahead, _} -> [:raw | modes]
+ false -> [:raw, :read_ahead | modes]
+ end
+
+ false ->
+ modes
+ end
+
+ %File.Stream{path: path, modes: modes, raw: raw, line_or_bytes: line_or_bytes, node: node()}
+
+ end"#.unindent(), 574)],
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_cpp() {
+ let language = cpp_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = "
+ /**
+ * @brief Main function
+ * @returns 0 on exit
+ */
+ int main() { return 0; }
+
+ /**
+ * This is a test comment
+ */
+ class MyClass { // The class
+ public: // Access specifier
+ int myNum; // Attribute (int variable)
+ string myString; // Attribute (string variable)
+ };
+
+ // This is a test comment
+ enum Color { red, green, blue };
+
+ /** This is a preceding block comment
+ * This is the second line
+ */
+ struct { // Structure declaration
+ int myNum; // Member (int variable)
+ string myString; // Member (string variable)
+ } myStructure;
+
+ /**
+ * @brief Matrix class.
+ */
+ template <typename T,
+ typename = typename std::enable_if<
+ std::is_integral<T>::value || std::is_floating_point<T>::value,
+ bool>::type>
+ class Matrix2 {
+ std::vector<std::vector<T>> _mat;
+
+ public:
+ /**
+ * @brief Constructor
+ * @tparam Integer ensuring integers are being evaluated and not other
+ * data types.
+ * @param size denoting the size of Matrix as size x size
+ */
+ template <typename Integer,
+ typename = typename std::enable_if<std::is_integral<Integer>::value,
+ Integer>::type>
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(size, 0));
+ }
+ }
+ }"
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (
+ "
+ /**
+ * @brief Main function
+ * @returns 0 on exit
+ */
+ int main() { return 0; }"
+ .unindent(),
+ 54,
+ ),
+ (
+ "
+ /**
+ * This is a test comment
+ */
+ class MyClass { // The class
+ public: // Access specifier
+ int myNum; // Attribute (int variable)
+ string myString; // Attribute (string variable)
+ }"
+ .unindent(),
+ 112,
+ ),
+ (
+ "
+ // This is a test comment
+ enum Color { red, green, blue }"
+ .unindent(),
+ 322,
+ ),
+ (
+ "
+ /** This is a preceding block comment
+ * This is the second line
+ */
+ struct { // Structure declaration
+ int myNum; // Member (int variable)
+ string myString; // Member (string variable)
+ } myStructure;"
+ .unindent(),
+ 425,
+ ),
+ (
+ "
+ /**
+ * @brief Matrix class.
+ */
+ template <typename T,
+ typename = typename std::enable_if<
+ std::is_integral<T>::value || std::is_floating_point<T>::value,
+ bool>::type>
+ class Matrix2 {
+ std::vector<std::vector<T>> _mat;
+
+ public:
+ /**
+ * @brief Constructor
+ * @tparam Integer ensuring integers are being evaluated and not other
+ * data types.
+ * @param size denoting the size of Matrix as size x size
+ */
+ template <typename Integer,
+ typename = typename std::enable_if<std::is_integral<Integer>::value,
+ Integer>::type>
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(size, 0));
+ }
+ }
+ }"
+ .unindent(),
+ 612,
+ ),
+ (
+ "
+ explicit Matrix(const Integer size) {
+ for (size_t i = 0; i < size; ++i) {
+ _mat.emplace_back(std::vector<T>(size, 0));
+ }
+ }"
+ .unindent(),
+ 1226,
+ ),
+ ],
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_ruby() {
+ let language = ruby_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = r#"
+ # This concern is inspired by "sudo mode" on GitHub. It
+ # is a way to re-authenticate a user before allowing them
+ # to see or perform an action.
+ #
+ # Add `before_action :require_challenge!` to actions you
+ # want to protect.
+ #
+ # The user will be shown a page to enter the challenge (which
+ # is either the password, or just the username when no
+ # password exists). Upon passing, there is a grace period
+ # during which no challenge will be asked from the user.
+ #
+ # Accessing challenge-protected resources during the grace
+ # period will refresh the grace period.
+ module ChallengableConcern
+ extend ActiveSupport::Concern
+
+ CHALLENGE_TIMEOUT = 1.hour.freeze
+
+ def require_challenge!
+ return if skip_challenge?
+
+ if challenge_passed_recently?
+ session[:challenge_passed_at] = Time.now.utc
+ return
+ end
+
+ @challenge = Form::Challenge.new(return_to: request.url)
+
+ if params.key?(:form_challenge)
+ if challenge_passed?
+ session[:challenge_passed_at] = Time.now.utc
+ else
+ flash.now[:alert] = I18n.t('challenge.invalid_password')
+ render_challenge
+ end
+ else
+ render_challenge
+ end
+ end
+
+ def challenge_passed?
+ current_user.valid_password?(challenge_params[:current_password])
+ end
+ end
+
+ class Animal
+ include Comparable
+
+ attr_reader :legs
+
+ def initialize(name, legs)
+ @name, @legs = name, legs
+ end
+
+ def <=>(other)
+ legs <=> other.legs
+ end
+ end
+
+ # Singleton method for car object
+ def car.wheels
+ puts "There are four wheels"
+ end"#
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (
+ r#"
+ # This concern is inspired by "sudo mode" on GitHub. It
+ # is a way to re-authenticate a user before allowing them
+ # to see or perform an action.
+ #
+ # Add `before_action :require_challenge!` to actions you
+ # want to protect.
+ #
+ # The user will be shown a page to enter the challenge (which
+ # is either the password, or just the username when no
+ # password exists). Upon passing, there is a grace period
+ # during which no challenge will be asked from the user.
+ #
+ # Accessing challenge-protected resources during the grace
+ # period will refresh the grace period.
+ module ChallengableConcern
+ extend ActiveSupport::Concern
+
+ CHALLENGE_TIMEOUT = 1.hour.freeze
+
+ def require_challenge!
+ # ...
+ end
+
+ def challenge_passed?
+ # ...
+ end
+ end"#
+ .unindent(),
+ 558,
+ ),
+ (
+ r#"
+ def require_challenge!
+ return if skip_challenge?
+
+ if challenge_passed_recently?
+ session[:challenge_passed_at] = Time.now.utc
+ return
+ end
+
+ @challenge = Form::Challenge.new(return_to: request.url)
+
+ if params.key?(:form_challenge)
+ if challenge_passed?
+ session[:challenge_passed_at] = Time.now.utc
+ else
+ flash.now[:alert] = I18n.t('challenge.invalid_password')
+ render_challenge
+ end
+ else
+ render_challenge
+ end
+ end"#
+ .unindent(),
+ 663,
+ ),
+ (
+ r#"
+ def challenge_passed?
+ current_user.valid_password?(challenge_params[:current_password])
+ end"#
+ .unindent(),
+ 1254,
+ ),
+ (
+ r#"
+ class Animal
+ include Comparable
+
+ attr_reader :legs
+
+ def initialize(name, legs)
+ # ...
+ end
+
+ def <=>(other)
+ # ...
+ end
+ end"#
+ .unindent(),
+ 1363,
+ ),
+ (
+ r#"
+ def initialize(name, legs)
+ @name, @legs = name, legs
+ end"#
+ .unindent(),
+ 1427,
+ ),
+ (
+ r#"
+ def <=>(other)
+ legs <=> other.legs
+ end"#
+ .unindent(),
+ 1501,
+ ),
+ (
+ r#"
+ # Singleton method for car object
+ def car.wheels
+ puts "There are four wheels"
+ end"#
+ .unindent(),
+ 1591,
+ ),
+ ],
+ );
+}
+
+#[gpui::test]
+async fn test_code_context_retrieval_php() {
+ let language = php_lang();
+ let embedding_provider = Arc::new(FakeEmbeddingProvider::default());
+ let mut retriever = CodeContextRetriever::new(embedding_provider);
+
+ let text = r#"
+ <?php
+
+ namespace LevelUp\Experience\Concerns;
+
+ /*
+ This is a multiple-lines comment block
+ that spans over multiple
+ lines
+ */
+ function functionName() {
+ echo "Hello world!";
+ }
+
+ trait HasAchievements
+ {
+ /**
+ * @throws \Exception
+ */
+ public function grantAchievement(Achievement $achievement, $progress = null): void
+ {
+ if ($progress > 100) {
+ throw new Exception(message: 'Progress cannot be greater than 100');
+ }
+
+ if ($this->achievements()->find($achievement->id)) {
+ throw new Exception(message: 'User already has this Achievement');
+ }
+
+ $this->achievements()->attach($achievement, [
+ 'progress' => $progress ?? null,
+ ]);
+
+ $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+ }
+
+ public function achievements(): BelongsToMany
+ {
+ return $this->belongsToMany(related: Achievement::class)
+ ->withPivot(columns: 'progress')
+ ->where('is_secret', false)
+ ->using(AchievementUser::class);
+ }
+ }
+
+ interface Multiplier
+ {
+ public function qualifies(array $data): bool;
+
+ public function setMultiplier(): int;
+ }
+
+ enum AuditType: string
+ {
+ case Add = 'add';
+ case Remove = 'remove';
+ case Reset = 'reset';
+ case LevelUp = 'level_up';
+ }
+
+ ?>"#
+ .unindent();
+
+ let documents = retriever.parse_file(&text, language.clone()).unwrap();
+
+ assert_documents_eq(
+ &documents,
+ &[
+ (
+ r#"
+ /*
+ This is a multiple-lines comment block
+ that spans over multiple
+ lines
+ */
+ function functionName() {
+ echo "Hello world!";
+ }"#
+ .unindent(),
+ 123,
+ ),
+ (
+ r#"
+ trait HasAchievements
+ {
+ /**
+ * @throws \Exception
+ */
+ public function grantAchievement(Achievement $achievement, $progress = null): void
+ {/* ... */}
+
+ public function achievements(): BelongsToMany
+ {/* ... */}
+ }"#
+ .unindent(),
+ 177,
+ ),
+ (r#"
+ /**
+ * @throws \Exception
+ */
+ public function grantAchievement(Achievement $achievement, $progress = null): void
+ {
+ if ($progress > 100) {
+ throw new Exception(message: 'Progress cannot be greater than 100');
+ }
+
+ if ($this->achievements()->find($achievement->id)) {
+ throw new Exception(message: 'User already has this Achievement');
+ }
+
+ $this->achievements()->attach($achievement, [
+ 'progress' => $progress ?? null,
+ ]);
+
+ $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this)));
+ }"#.unindent(), 245),
+ (r#"
+ public function achievements(): BelongsToMany
+ {
+ return $this->belongsToMany(related: Achievement::class)
+ ->withPivot(columns: 'progress')
+ ->where('is_secret', false)
+ ->using(AchievementUser::class);
+ }"#.unindent(), 902),
+ (r#"
+ interface Multiplier
+ {
+ public function qualifies(array $data): bool;
+
+ public function setMultiplier(): int;
+ }"#.unindent(),
+ 1146),
+ (r#"
+ enum AuditType: string
+ {
+ case Add = 'add';
+ case Remove = 'remove';
+ case Reset = 'reset';
+ case LevelUp = 'level_up';
+ }"#.unindent(), 1265)
+ ],
+ );
+}
+
+fn js_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Javascript".into(),
+ path_suffixes: vec!["js".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_tsx()),
+ )
+ .with_embedding_query(
+ &r#"
+
+ (
+ (comment)* @context
+ .
+ [
+ (export_statement
+ (function_declaration
+ "async"? @name
+ "function" @name
+ name: (_) @name))
+ (function_declaration
+ "async"? @name
+ "function" @name
+ name: (_) @name)
+ ] @item
+ )
+
+ (
+ (comment)* @context
+ .
+ [
+ (export_statement
+ (class_declaration
+ "class" @name
+ name: (_) @name))
+ (class_declaration
+ "class" @name
+ name: (_) @name)
+ ] @item
+ )
+
+ (
+ (comment)* @context
+ .
+ [
+ (export_statement
+ (interface_declaration
+ "interface" @name
+ name: (_) @name))
+ (interface_declaration
+ "interface" @name
+ name: (_) @name)
+ ] @item
+ )
+
+ (
+ (comment)* @context
+ .
+ [
+ (export_statement
+ (enum_declaration
+ "enum" @name
+ name: (_) @name))
+ (enum_declaration
+ "enum" @name
+ name: (_) @name)
+ ] @item
+ )
+
+ (
+ (comment)* @context
+ .
+ (method_definition
+ [
+ "get"
+ "set"
+ "async"
+ "*"
+ "static"
+ ]* @name
+ name: (_) @name) @item
+ )
+
+ "#
+ .unindent(),
+ )
+ .unwrap(),
+ )
+}
+
+fn rust_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".into()],
+ collapsed_placeholder: " /* ... */ ".to_string(),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ [(line_comment) (attribute_item)]* @context
+ .
+ [
+ (struct_item
+ name: (_) @name)
+
+ (enum_item
+ name: (_) @name)
+
+ (impl_item
+ trait: (_)? @name
+ "for"? @name
+ type: (_) @name)
+
+ (trait_item
+ name: (_) @name)
+
+ (function_item
+ name: (_) @name
+ body: (block
+ "{" @keep
+ "}" @keep) @collapse)
+
+ (macro_definition
+ name: (_) @name)
+ ] @item
+ )
+
+ (attribute_item) @collapse
+ (use_declaration) @collapse
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn json_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "JSON".into(),
+ path_suffixes: vec!["json".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_json::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (document) @item
+
+ (array
+ "[" @keep
+ .
+ (object)? @keep
+ "]" @keep) @collapse
+
+ (pair value: (string
+ "\"" @keep
+ "\"" @keep) @collapse)
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn toml_lang() -> Arc<Language> {
+ Arc::new(Language::new(
+ LanguageConfig {
+ name: "TOML".into(),
+ path_suffixes: vec!["toml".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_toml::language()),
+ ))
+}
+
+fn cpp_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "CPP".into(),
+ path_suffixes: vec!["cpp".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_cpp::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ (comment)* @context
+ .
+ (function_definition
+ (type_qualifier)? @name
+ type: (_)? @name
+ declarator: [
+ (function_declarator
+ declarator: (_) @name)
+ (pointer_declarator
+ "*" @name
+ declarator: (function_declarator
+ declarator: (_) @name))
+ (pointer_declarator
+ "*" @name
+ declarator: (pointer_declarator
+ "*" @name
+ declarator: (function_declarator
+ declarator: (_) @name)))
+ (reference_declarator
+ ["&" "&&"] @name
+ (function_declarator
+ declarator: (_) @name))
+ ]
+ (type_qualifier)? @name) @item
+ )
+
+ (
+ (comment)* @context
+ .
+ (template_declaration
+ (class_specifier
+ "class" @name
+ name: (_) @name)
+ ) @item
+ )
+
+ (
+ (comment)* @context
+ .
+ (class_specifier
+ "class" @name
+ name: (_) @name) @item
+ )
+
+ (
+ (comment)* @context
+ .
+ (enum_specifier
+ "enum" @name
+ name: (_) @name) @item
+ )
+
+ (
+ (comment)* @context
+ .
+ (declaration
+ type: (struct_specifier
+ "struct" @name)
+ declarator: (_) @name) @item
+ )
+
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn lua_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Lua".into(),
+ path_suffixes: vec!["lua".into()],
+ collapsed_placeholder: "--[ ... ]--".to_string(),
+ ..Default::default()
+ },
+ Some(tree_sitter_lua::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ (comment)* @context
+ .
+ (function_declaration
+ "function" @name
+ name: (_) @name
+ (comment)* @collapse
+ body: (block) @collapse
+ ) @item
+ )
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn php_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "PHP".into(),
+ path_suffixes: vec!["php".into()],
+ collapsed_placeholder: "/* ... */".into(),
+ ..Default::default()
+ },
+ Some(tree_sitter_php::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ (comment)* @context
+ .
+ [
+ (function_definition
+ "function" @name
+ name: (_) @name
+ body: (_
+ "{" @keep
+ "}" @keep) @collapse
+ )
+
+ (trait_declaration
+ "trait" @name
+ name: (_) @name)
+
+ (method_declaration
+ "function" @name
+ name: (_) @name
+ body: (_
+ "{" @keep
+ "}" @keep) @collapse
+ )
+
+ (interface_declaration
+ "interface" @name
+ name: (_) @name
+ )
+
+ (enum_declaration
+ "enum" @name
+ name: (_) @name
+ )
+
+ ] @item
+ )
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn ruby_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Ruby".into(),
+ path_suffixes: vec!["rb".into()],
+ collapsed_placeholder: "# ...".to_string(),
+ ..Default::default()
+ },
+ Some(tree_sitter_ruby::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ (comment)* @context
+ .
+ [
+ (module
+ "module" @name
+ name: (_) @name)
+ (method
+ "def" @name
+ name: (_) @name
+ body: (body_statement) @collapse)
+ (class
+ "class" @name
+ name: (_) @name)
+ (singleton_method
+ "def" @name
+ object: (_) @name
+ "." @name
+ name: (_) @name
+ body: (body_statement) @collapse)
+ ] @item
+ )
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+fn elixir_lang() -> Arc<Language> {
+ Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Elixir".into(),
+ path_suffixes: vec!["rs".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_elixir::language()),
+ )
+ .with_embedding_query(
+ r#"
+ (
+ (unary_operator
+ operator: "@"
+ operand: (call
+ target: (identifier) @unary
+ (#match? @unary "^(doc)$"))
+ ) @context
+ .
+ (call
+ target: (identifier) @name
+ (arguments
+ [
+ (identifier) @name
+ (call
+ target: (identifier) @name)
+ (binary_operator
+ left: (call
+ target: (identifier) @name)
+ operator: "when")
+ ])
+ (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item
+ )
+
+ (call
+ target: (identifier) @name
+ (arguments (alias) @name)
+ (#any-match? @name "^(defmodule|defprotocol)$")) @item
+ "#,
+ )
+ .unwrap(),
+ )
+}
+
+#[gpui::test]
+fn test_subtract_ranges() {
+ // collapsed_ranges: Vec<Range<usize>>, keep_ranges: Vec<Range<usize>>
+
+ assert_eq!(
+ subtract_ranges(&[0..5, 10..21], &[0..1, 4..5]),
+ vec![1..4, 10..21]
+ );
+
+ assert_eq!(subtract_ranges(&[0..5], &[1..2]), &[0..1, 2..5]);
+}
+
+fn init_test(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ SemanticIndexSettings::register(cx);
+ ProjectSettings::register(cx);
+ });
+}
@@ -1,3 +1,5 @@
+mod auto_height_editor;
+mod cursor;
mod focus;
mod kitchen_sink;
mod picker;
@@ -5,6 +7,8 @@ mod scroll;
mod text;
mod z_index;
+pub use auto_height_editor::*;
+pub use cursor::*;
pub use focus::*;
pub use kitchen_sink::*;
pub use picker::*;
@@ -0,0 +1,34 @@
+use editor::Editor;
+use gpui::{
+ div, white, Div, KeyBinding, ParentElement, Render, Styled, View, ViewContext, VisualContext,
+ WindowContext,
+};
+
+pub struct AutoHeightEditorStory {
+ editor: View<Editor>,
+}
+
+impl AutoHeightEditorStory {
+ pub fn new(cx: &mut WindowContext) -> View<Self> {
+ cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]);
+ cx.build_view(|cx| Self {
+ editor: cx.build_view(|cx| {
+ let mut editor = Editor::auto_height(3, cx);
+ editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
+ editor
+ }),
+ })
+ }
+}
+
+impl Render for AutoHeightEditorStory {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ div()
+ .size_full()
+ .bg(white())
+ .text_sm()
+ .child(div().w_32().bg(gpui::black()).child(self.editor.clone()))
+ }
+}
@@ -0,0 +1,111 @@
+use gpui::{Div, Render, Stateful};
+use story::Story;
+use ui::prelude::*;
+
+pub struct CursorStory;
+
+impl Render for CursorStory {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ let all_cursors: [(&str, Box<dyn Fn(Stateful<Div>) -> Stateful<Div>>); 19] = [
+ (
+ "cursor_default",
+ Box::new(|el: Stateful<Div>| el.cursor_default()),
+ ),
+ (
+ "cursor_pointer",
+ Box::new(|el: Stateful<Div>| el.cursor_pointer()),
+ ),
+ (
+ "cursor_text",
+ Box::new(|el: Stateful<Div>| el.cursor_text()),
+ ),
+ (
+ "cursor_move",
+ Box::new(|el: Stateful<Div>| el.cursor_move()),
+ ),
+ (
+ "cursor_not_allowed",
+ Box::new(|el: Stateful<Div>| el.cursor_not_allowed()),
+ ),
+ (
+ "cursor_context_menu",
+ Box::new(|el: Stateful<Div>| el.cursor_context_menu()),
+ ),
+ (
+ "cursor_crosshair",
+ Box::new(|el: Stateful<Div>| el.cursor_crosshair()),
+ ),
+ (
+ "cursor_vertical_text",
+ Box::new(|el: Stateful<Div>| el.cursor_vertical_text()),
+ ),
+ (
+ "cursor_alias",
+ Box::new(|el: Stateful<Div>| el.cursor_alias()),
+ ),
+ (
+ "cursor_copy",
+ Box::new(|el: Stateful<Div>| el.cursor_copy()),
+ ),
+ (
+ "cursor_no_drop",
+ Box::new(|el: Stateful<Div>| el.cursor_no_drop()),
+ ),
+ (
+ "cursor_grab",
+ Box::new(|el: Stateful<Div>| el.cursor_grab()),
+ ),
+ (
+ "cursor_grabbing",
+ Box::new(|el: Stateful<Div>| el.cursor_grabbing()),
+ ),
+ (
+ "cursor_col_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_col_resize()),
+ ),
+ (
+ "cursor_row_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_row_resize()),
+ ),
+ (
+ "cursor_n_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_n_resize()),
+ ),
+ (
+ "cursor_e_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_e_resize()),
+ ),
+ (
+ "cursor_s_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_s_resize()),
+ ),
+ (
+ "cursor_w_resize",
+ Box::new(|el: Stateful<Div>| el.cursor_w_resize()),
+ ),
+ ];
+
+ Story::container()
+ .flex()
+ .gap_1()
+ .child(Story::title("cursor"))
+ .children(all_cursors.map(|(name, apply_cursor)| {
+ div().gap_1().flex().text_color(gpui::white()).child(
+ div()
+ .flex()
+ .items_center()
+ .justify_center()
+ .id(name)
+ .map(apply_cursor)
+ .w_64()
+ .h_8()
+ .bg(gpui::red())
+ .active(|style| style.bg(gpui::green()))
+ .text_sm()
+ .child(Story::label(name)),
+ )
+ }))
+ }
+}
@@ -1,6 +1,6 @@
use gpui::{
- blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
- TextRun, View, VisualContext, WindowContext,
+ blue, div, green, red, white, Div, HighlightStyle, InteractiveText, ParentElement, Render,
+ Styled, StyledText, View, VisualContext, WindowContext,
};
use ui::v_stack;
@@ -59,13 +59,11 @@ impl Render for TextStory {
))).child(
InteractiveText::new(
"interactive",
- StyledText::new("Hello world, how is it going?").with_runs(vec![
- cx.text_style().to_run(6),
- TextRun {
+ StyledText::new("Hello world, how is it going?").with_highlights(&cx.text_style(), [
+ (6..11, HighlightStyle {
background_color: Some(green()),
- ..cx.text_style().to_run(5)
- },
- cx.text_style().to_run(18),
+ ..Default::default()
+ }),
]),
)
.on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| {
@@ -12,10 +12,12 @@ use ui::prelude::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum ComponentStory {
+ AutoHeightEditor,
Avatar,
Button,
Checkbox,
ContextMenu,
+ Cursor,
Disclosure,
Focus,
Icon,
@@ -23,6 +25,7 @@ pub enum ComponentStory {
Keybinding,
Label,
List,
+ ListHeader,
ListItem,
Scroll,
Text,
@@ -33,10 +36,12 @@ pub enum ComponentStory {
impl ComponentStory {
pub fn story(&self, cx: &mut WindowContext) -> AnyView {
match self {
+ Self::AutoHeightEditor => AutoHeightEditorStory::new(cx).into(),
Self::Avatar => cx.build_view(|_| ui::AvatarStory).into(),
Self::Button => cx.build_view(|_| ui::ButtonStory).into(),
Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(),
Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(),
+ Self::Cursor => cx.build_view(|_| crate::stories::CursorStory).into(),
Self::Disclosure => cx.build_view(|_| ui::DisclosureStory).into(),
Self::Focus => FocusStory::view(cx).into(),
Self::Icon => cx.build_view(|_| ui::IconStory).into(),
@@ -44,6 +49,7 @@ impl ComponentStory {
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
Self::Label => cx.build_view(|_| ui::LabelStory).into(),
Self::List => cx.build_view(|_| ui::ListStory).into(),
+ Self::ListHeader => cx.build_view(|_| ui::ListHeaderStory).into(),
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(),
@@ -1132,6 +1132,7 @@ mod tests {
})
})
.await
+ .unwrap()
.unwrap();
(wt, entry)
@@ -299,11 +299,8 @@ impl TerminalView {
cx: &mut ViewContext<Self>,
) {
self.context_menu = Some(ContextMenu::build(cx, |menu, cx| {
- menu.action("Clear", Box::new(Clear), cx).action(
- "Close",
- Box::new(CloseActiveItem { save_intent: None }),
- cx,
- )
+ menu.action("Clear", Box::new(Clear))
+ .action("Close", Box::new(CloseActiveItem { save_intent: None }))
}));
dbg!(&position);
// todo!()
@@ -1173,6 +1170,7 @@ mod tests {
})
})
.await
+ .unwrap()
.unwrap();
(wt, entry)
@@ -52,13 +52,13 @@ pub(crate) fn one_dark() -> Theme {
element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
- element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+ element_disabled: SystemColors::default().transparent,
drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
ghost_element_background: SystemColors::default().transparent,
ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
- ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
+ ghost_element_disabled: SystemColors::default().transparent,
text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0),
text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0),
text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0),
@@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{
- actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View,
- ViewContext, VisualContext, WeakView,
+ actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString,
+ View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use settings::{update_settings_file, SettingsStore};
use std::sync::Arc;
use theme::{Theme, ThemeRegistry, ThemeSettings};
-use ui::{prelude::*, ListItem};
+use ui::{prelude::*, v_stack, ListItem};
use util::ResultExt;
use workspace::{ui::HighlightedLabel, Workspace};
@@ -65,10 +65,10 @@ impl FocusableView for ThemeSelector {
}
impl Render for ThemeSelector {
- type Element = View<Picker<ThemeSelectorDelegate>>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
- self.picker.clone()
+ v_stack().min_w_96().child(self.picker.clone())
}
}
@@ -98,7 +98,7 @@ impl ThemeSelectorDelegate {
let original_theme = cx.theme().clone();
let staff_mode = cx.is_staff();
- let registry = cx.global::<Arc<ThemeRegistry>>();
+ let registry = cx.global::<ThemeRegistry>();
let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
//todo!(theme sorting)
// theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
@@ -126,7 +126,7 @@ impl ThemeSelectorDelegate {
fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
if let Some(mat) = self.matches.get(self.selected_index) {
- let registry = cx.global::<Arc<ThemeRegistry>>();
+ let registry = cx.global::<ThemeRegistry>();
match registry.get(&mat.string) {
Ok(theme) => {
Self::set_theme(theme, cx);
@@ -9,6 +9,8 @@ mod keybinding;
mod label;
mod list;
mod popover;
+mod popover_menu;
+mod right_click_menu;
mod stack;
mod tooltip;
@@ -26,6 +28,8 @@ pub use keybinding::*;
pub use label::*;
pub use list::*;
pub use popover::*;
+pub use popover_menu::*;
+pub use right_click_menu::*;
pub use stack::*;
pub use tooltip::*;
@@ -1,4 +1,5 @@
mod button;
+pub(self) mod button_icon;
mod button_like;
mod icon_button;
@@ -1,13 +1,22 @@
-use gpui::AnyView;
+use gpui::{AnyView, DefiniteLength};
use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Label, LineHeightStyle};
+use crate::{
+ ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
+};
+
+use super::button_icon::ButtonIcon;
#[derive(IntoElement)]
pub struct Button {
base: ButtonLike,
label: SharedString,
label_color: Option<Color>,
+ selected_label: Option<SharedString>,
+ icon: Option<Icon>,
+ icon_size: Option<IconSize>,
+ icon_color: Option<Color>,
+ selected_icon: Option<Icon>,
}
impl Button {
@@ -16,6 +25,11 @@ impl Button {
base: ButtonLike::new(id),
label: label.into(),
label_color: None,
+ selected_label: None,
+ icon: None,
+ icon_size: None,
+ icon_color: None,
+ selected_icon: None,
}
}
@@ -23,6 +37,31 @@ impl Button {
self.label_color = label_color.into();
self
}
+
+ pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
+ self.selected_label = label.into().map(Into::into);
+ self
+ }
+
+ pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ self.icon = icon.into();
+ self
+ }
+
+ pub fn icon_size(mut self, icon_size: impl Into<Option<IconSize>>) -> Self {
+ self.icon_size = icon_size.into();
+ self
+ }
+
+ pub fn icon_color(mut self, icon_color: impl Into<Option<Color>>) -> Self {
+ self.icon_color = icon_color.into();
+ self
+ }
+
+ pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ self.selected_icon = icon.into();
+ self
+ }
}
impl Selectable for Button {
@@ -49,6 +88,18 @@ impl Clickable for Button {
}
}
+impl FixedWidth for Button {
+ fn width(mut self, width: DefiniteLength) -> Self {
+ self.base = self.base.width(width);
+ self
+ }
+
+ fn full_width(mut self) -> Self {
+ self.base = self.base.full_width();
+ self
+ }
+}
+
impl ButtonCommon for Button {
fn id(&self) -> &ElementId {
self.base.id()
@@ -74,18 +125,35 @@ impl RenderOnce for Button {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
- let label_color = if self.base.disabled {
+ let is_disabled = self.base.disabled;
+ let is_selected = self.base.selected;
+
+ let label = self
+ .selected_label
+ .filter(|_| is_selected)
+ .unwrap_or(self.label);
+
+ let label_color = if is_disabled {
Color::Disabled
- } else if self.base.selected {
+ } else if is_selected {
Color::Selected
} else {
- Color::Default
+ self.label_color.unwrap_or_default()
};
- self.base.child(
- Label::new(self.label)
- .color(label_color)
- .line_height_style(LineHeightStyle::UILabel),
- )
+ self.base
+ .children(self.icon.map(|icon| {
+ ButtonIcon::new(icon)
+ .disabled(is_disabled)
+ .selected(is_selected)
+ .selected_icon(self.selected_icon)
+ .size(self.icon_size)
+ .color(self.icon_color)
+ }))
+ .child(
+ Label::new(label)
+ .color(label_color)
+ .line_height_style(LineHeightStyle::UILabel),
+ )
}
}
@@ -0,0 +1,84 @@
+use crate::{prelude::*, Icon, IconElement, IconSize};
+
+/// An icon that appears within a button.
+///
+/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button),
+/// or as a standalone icon, like in [`IconButton`](crate::IconButton).
+#[derive(IntoElement)]
+pub(super) struct ButtonIcon {
+ icon: Icon,
+ size: IconSize,
+ color: Color,
+ disabled: bool,
+ selected: bool,
+ selected_icon: Option<Icon>,
+}
+
+impl ButtonIcon {
+ pub fn new(icon: Icon) -> Self {
+ Self {
+ icon,
+ size: IconSize::default(),
+ color: Color::default(),
+ disabled: false,
+ selected: false,
+ selected_icon: None,
+ }
+ }
+
+ pub fn size(mut self, size: impl Into<Option<IconSize>>) -> Self {
+ if let Some(size) = size.into() {
+ self.size = size;
+ }
+
+ self
+ }
+
+ pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
+ if let Some(color) = color.into() {
+ self.color = color;
+ }
+
+ self
+ }
+
+ pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ self.selected_icon = icon.into();
+ self
+ }
+}
+
+impl Disableable for ButtonIcon {
+ fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
+}
+
+impl Selectable for ButtonIcon {
+ fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+}
+
+impl RenderOnce for ButtonIcon {
+ type Rendered = IconElement;
+
+ fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+ let icon = self
+ .selected_icon
+ .filter(|_| self.selected)
+ .unwrap_or(self.icon);
+
+ let icon_color = if self.disabled {
+ Color::Disabled
+ } else if self.selected {
+ Color::Selected
+ } else {
+ self.color
+ };
+
+ IconElement::new(icon).size(self.size).color(icon_color)
+ }
+}
@@ -1,3 +1,4 @@
+use gpui::{relative, DefiniteLength};
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
use smallvec::SmallVec;
@@ -5,18 +6,50 @@ use crate::h_stack;
use crate::prelude::*;
pub trait ButtonCommon: Clickable + Disableable {
+ /// A unique element ID to identify the button.
fn id(&self) -> &ElementId;
+
+ /// The visual style of the button.
+ ///
+ /// Mosty commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
+ /// for an emphasized button.
fn style(self, style: ButtonStyle) -> Self;
+
+ /// The size of the button.
+ ///
+ /// Most buttons will use the default size.
+ ///
+ /// [`ButtonSize`] can also be used to help build non-button elements
+ /// that are consistently sized with buttons.
fn size(self, size: ButtonSize) -> Self;
+
+ /// The tooltip that shows when a user hovers over the button.
+ ///
+ /// Nearly all interactable elements should have a tooltip. Some example
+ /// exceptions might a scroll bar, or a slider.
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum ButtonStyle {
- #[default]
+ /// A filled button with a solid background color. Provides emphasis versus
+ /// the more common subtle button.
Filled,
- // Tinted,
+
+ /// 🚧 Under construction 🚧
+ ///
+ /// Used to emphasize a button in some way, like a selected state, or a semantic
+ /// coloring like an error or success button.
+ Tinted,
+
+ /// The default button style, used for most buttons. Has a transparent background,
+ /// but has a background color to indicate states like hover and active.
+ #[default]
Subtle,
+
+ /// Used for buttons that only change forground color on hover and active states.
+ ///
+ /// TODO: Better docs for this.
Transparent,
}
@@ -40,6 +73,12 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
+ ButtonStyle::Tinted => ButtonLikeStyles {
+ background: gpui::red(),
+ border_color: gpui::red(),
+ label_color: gpui::red(),
+ icon_color: gpui::red(),
+ },
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_background,
border_color: transparent_black(),
@@ -63,6 +102,12 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
+ ButtonStyle::Tinted => ButtonLikeStyles {
+ background: gpui::red(),
+ border_color: gpui::red(),
+ label_color: gpui::red(),
+ icon_color: gpui::red(),
+ },
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_hover,
border_color: transparent_black(),
@@ -88,6 +133,12 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
+ ButtonStyle::Tinted => ButtonLikeStyles {
+ background: gpui::red(),
+ border_color: gpui::red(),
+ label_color: gpui::red(),
+ icon_color: gpui::red(),
+ },
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_active,
border_color: transparent_black(),
@@ -114,6 +165,12 @@ impl ButtonStyle {
label_color: Color::Default.color(cx),
icon_color: Color::Default.color(cx),
},
+ ButtonStyle::Tinted => ButtonLikeStyles {
+ background: gpui::red(),
+ border_color: gpui::red(),
+ label_color: gpui::red(),
+ icon_color: gpui::red(),
+ },
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_background,
border_color: cx.theme().colors().border_focused,
@@ -137,6 +194,12 @@ impl ButtonStyle {
label_color: Color::Disabled.color(cx),
icon_color: Color::Disabled.color(cx),
},
+ ButtonStyle::Tinted => ButtonLikeStyles {
+ background: gpui::red(),
+ border_color: gpui::red(),
+ label_color: gpui::red(),
+ icon_color: gpui::red(),
+ },
ButtonStyle::Subtle => ButtonLikeStyles {
background: cx.theme().colors().ghost_element_disabled,
border_color: cx.theme().colors().border_disabled,
@@ -153,6 +216,8 @@ impl ButtonStyle {
}
}
+/// ButtonSize can also be used to help build non-button elements
+/// that are consistently sized with buttons.
#[derive(Default, PartialEq, Clone, Copy)]
pub enum ButtonSize {
#[default]
@@ -171,12 +236,18 @@ impl ButtonSize {
}
}
+/// A button-like element that can be used to create a custom button when
+/// prebuilt buttons are not sufficient. Use this sparingly, as it is
+/// unconstrained and may make the UI feel less consistent.
+///
+/// This is also used to build the prebuilt buttons.
#[derive(IntoElement)]
pub struct ButtonLike {
id: ElementId,
pub(super) style: ButtonStyle,
pub(super) disabled: bool,
pub(super) selected: bool,
+ pub(super) width: Option<DefiniteLength>,
size: ButtonSize,
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -190,6 +261,7 @@ impl ButtonLike {
style: ButtonStyle::default(),
disabled: false,
selected: false,
+ width: None,
size: ButtonSize::Default,
tooltip: None,
children: SmallVec::new(),
@@ -219,6 +291,18 @@ impl Clickable for ButtonLike {
}
}
+impl FixedWidth for ButtonLike {
+ fn width(mut self, width: DefiniteLength) -> Self {
+ self.width = Some(width);
+ self
+ }
+
+ fn full_width(mut self) -> Self {
+ self.width = Some(relative(1.));
+ self
+ }
+}
+
impl ButtonCommon for ButtonLike {
fn id(&self) -> &ElementId {
&self.id
@@ -252,14 +336,19 @@ impl RenderOnce for ButtonLike {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack()
.id(self.id.clone())
+ .group("")
+ .flex_none()
.h(self.size.height())
+ .when_some(self.width, |this, width| this.w(width))
.rounded_md()
- .cursor_pointer()
.gap_1()
.px_1()
.bg(self.style.enabled(cx).background)
- .hover(|hover| hover.bg(self.style.hovered(cx).background))
- .active(|active| active.bg(self.style.active(cx).background))
+ .when(!self.disabled, |this| {
+ this.cursor_pointer()
+ .hover(|hover| hover.bg(self.style.hovered(cx).background))
+ .active(|active| active.bg(self.style.active(cx).background))
+ })
.when_some(
self.on_click.filter(|_| !self.disabled),
|this, on_click| {
@@ -270,7 +359,11 @@ impl RenderOnce for ButtonLike {
},
)
.when_some(self.tooltip, |this, tooltip| {
- this.tooltip(move |cx| tooltip(cx))
+ if !self.selected {
+ this.tooltip(move |cx| tooltip(cx))
+ } else {
+ this
+ }
})
.children(self.children)
}
@@ -1,7 +1,9 @@
-use gpui::{Action, AnyView};
+use gpui::{Action, AnyView, DefiniteLength};
use crate::prelude::*;
-use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconElement, IconSize};
+use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize};
+
+use super::button_icon::ButtonIcon;
#[derive(IntoElement)]
pub struct IconButton {
@@ -9,6 +11,7 @@ pub struct IconButton {
icon: Icon,
icon_size: IconSize,
icon_color: Color,
+ selected_icon: Option<Icon>,
}
impl IconButton {
@@ -18,6 +21,7 @@ impl IconButton {
icon,
icon_size: IconSize::default(),
icon_color: Color::Default,
+ selected_icon: None,
}
}
@@ -31,6 +35,11 @@ impl IconButton {
self
}
+ pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
+ self.selected_icon = icon.into();
+ self
+ }
+
pub fn action(self, action: Box<dyn Action>) -> Self {
self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone()))
}
@@ -60,6 +69,18 @@ impl Clickable for IconButton {
}
}
+impl FixedWidth for IconButton {
+ fn width(mut self, width: DefiniteLength) -> Self {
+ self.base = self.base.width(width);
+ self
+ }
+
+ fn full_width(mut self) -> Self {
+ self.base = self.base.full_width();
+ self
+ }
+}
+
impl ButtonCommon for IconButton {
fn id(&self) -> &ElementId {
self.base.id()
@@ -85,18 +106,16 @@ impl RenderOnce for IconButton {
type Rendered = ButtonLike;
fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
- let icon_color = if self.base.disabled {
- Color::Disabled
- } else if self.base.selected {
- Color::Selected
- } else {
- self.icon_color
- };
+ let is_disabled = self.base.disabled;
+ let is_selected = self.base.selected;
self.base.child(
- IconElement::new(self.icon)
+ ButtonIcon::new(self.icon)
+ .disabled(is_disabled)
+ .selected(is_selected)
+ .selected_icon(self.selected_icon)
.size(self.icon_size)
- .color(icon_color),
+ .color(self.icon_color),
)
}
}
@@ -1,21 +1,22 @@
use crate::{
- h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
+ h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
+ ListSeparator, ListSubHeader,
};
use gpui::{
- overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DismissEvent, DispatchPhase,
- Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId, ManagedView, MouseButton,
- MouseDownEvent, Pixels, Point, Render, View, VisualContext,
+ px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+ IntoElement, Render, View, VisualContext,
};
use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev};
-use std::{cell::RefCell, rc::Rc};
+use std::{rc::Rc, time::Duration};
pub enum ContextMenuItem {
Separator,
Header(SharedString),
Entry {
label: SharedString,
+ icon: Option<Icon>,
handler: Rc<dyn Fn(&mut WindowContext)>,
- key_binding: Option<KeyBinding>,
+ action: Option<Box<dyn Action>>,
},
}
@@ -23,6 +24,7 @@ pub struct ContextMenu {
items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
selected_index: Option<usize>,
+ delayed: bool,
}
impl FocusableView for ContextMenu {
@@ -45,6 +47,7 @@ impl ContextMenu {
items: Default::default(),
focus_handle: cx.focus_handle(),
selected_index: None,
+ delayed: false,
},
cx,
)
@@ -69,21 +72,28 @@ impl ContextMenu {
self.items.push(ContextMenuItem::Entry {
label: label.into(),
handler: Rc::new(on_click),
- key_binding: None,
+ icon: None,
+ action: None,
});
self
}
- pub fn action(
- mut self,
- label: impl Into<SharedString>,
- action: Box<dyn Action>,
- cx: &mut WindowContext,
- ) -> Self {
+ pub fn action(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
+ self.items.push(ContextMenuItem::Entry {
+ label: label.into(),
+ action: Some(action.boxed_clone()),
+ handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+ icon: None,
+ });
+ self
+ }
+
+ pub fn link(mut self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
self.items.push(ContextMenuItem::Entry {
label: label.into(),
- key_binding: KeyBinding::for_action(&*action, cx),
+ action: Some(action.boxed_clone()),
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+ icon: Some(Icon::Link),
});
self
}
@@ -143,6 +153,37 @@ impl ContextMenu {
self.select_last(&Default::default(), cx);
}
}
+
+ pub fn on_action_dispatch(&mut self, dispatched: &Box<dyn Action>, cx: &mut ViewContext<Self>) {
+ if let Some(ix) = self.items.iter().position(|item| {
+ if let ContextMenuItem::Entry {
+ action: Some(action),
+ ..
+ } = item
+ {
+ action.partial_eq(&**dispatched)
+ } else {
+ false
+ }
+ }) {
+ self.selected_index = Some(ix);
+ self.delayed = true;
+ cx.notify();
+ let action = dispatched.boxed_clone();
+ cx.spawn(|this, mut cx| async move {
+ cx.background_executor()
+ .timer(Duration::from_millis(50))
+ .await;
+ this.update(&mut cx, |this, cx| {
+ cx.dispatch_action(action);
+ this.cancel(&Default::default(), cx)
+ })
+ })
+ .detach_and_log_err(cx);
+ } else {
+ cx.propagate()
+ }
+ }
}
impl ContextMenuItem {
@@ -167,6 +208,22 @@ impl Render for ContextMenu {
.on_action(cx.listener(ContextMenu::select_prev))
.on_action(cx.listener(ContextMenu::confirm))
.on_action(cx.listener(ContextMenu::cancel))
+ .when(!self.delayed, |mut el| {
+ for item in self.items.iter() {
+ if let ContextMenuItem::Entry {
+ action: Some(action),
+ ..
+ } = item
+ {
+ el = el.on_boxed_action(
+ action,
+ cx.listener(ContextMenu::on_action_dispatch),
+ );
+ }
+ }
+ el
+ })
+ .on_blur(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx)))
.flex_none()
.child(
List::new().children(self.items.iter().enumerate().map(
@@ -176,28 +233,38 @@ impl Render for ContextMenu {
ListSubHeader::new(header.clone()).into_any_element()
}
ContextMenuItem::Entry {
- label: entry,
- handler: callback,
- key_binding,
+ label,
+ handler,
+ icon,
+ action,
} => {
- let callback = callback.clone();
+ let handler = handler.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
- ListItem::new(entry.clone())
+ let label_element = if let Some(icon) = icon {
+ h_stack()
+ .gap_1()
+ .child(Label::new(label.clone()))
+ .child(IconElement::new(*icon))
+ .into_any_element()
+ } else {
+ Label::new(label.clone()).into_any_element()
+ };
+
+ ListItem::new(label.clone())
.child(
h_stack()
.w_full()
.justify_between()
- .child(Label::new(entry.clone()))
- .children(
- key_binding
- .clone()
- .map(|binding| div().ml_1().child(binding)),
- ),
+ .child(label_element)
+ .children(action.as_ref().and_then(|action| {
+ KeyBinding::for_action(&**action, cx)
+ .map(|binding| div().ml_1().child(binding))
+ })),
)
.selected(Some(ix) == self.selected_index)
.on_click(move |event, cx| {
- callback(cx);
+ handler(cx);
dismiss(event, cx)
})
.into_any_element()
@@ -208,174 +275,3 @@ impl Render for ContextMenu {
)
}
}
-
-pub struct MenuHandle<M: ManagedView> {
- id: ElementId,
- child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
- menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
- anchor: Option<AnchorCorner>,
- attach: Option<AnchorCorner>,
-}
-
-impl<M: ManagedView> MenuHandle<M> {
- pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
- self.menu_builder = Some(Rc::new(f));
- self
- }
-
- pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
- self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
- self
- }
-
- /// anchor defines which corner of the menu to anchor to the attachment point
- /// (by default the cursor position, but see attach)
- pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
- self.anchor = Some(anchor);
- self
- }
-
- /// attach defines which corner of the handle to attach the menu's anchor to
- pub fn attach(mut self, attach: AnchorCorner) -> Self {
- self.attach = Some(attach);
- self
- }
-}
-
-pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
- MenuHandle {
- id: id.into(),
- child_builder: None,
- menu_builder: None,
- anchor: None,
- attach: None,
- }
-}
-
-pub struct MenuHandleState<M> {
- menu: Rc<RefCell<Option<View<M>>>>,
- position: Rc<RefCell<Point<Pixels>>>,
- child_layout_id: Option<LayoutId>,
- child_element: Option<AnyElement>,
- menu_element: Option<AnyElement>,
-}
-
-impl<M: ManagedView> Element for MenuHandle<M> {
- type State = MenuHandleState<M>;
-
- fn layout(
- &mut self,
- element_state: Option<Self::State>,
- cx: &mut WindowContext,
- ) -> (gpui::LayoutId, Self::State) {
- let (menu, position) = if let Some(element_state) = element_state {
- (element_state.menu, element_state.position)
- } else {
- (Rc::default(), Rc::default())
- };
-
- let mut menu_layout_id = None;
-
- let menu_element = menu.borrow_mut().as_mut().map(|menu| {
- let mut overlay = overlay().snap_to_window();
- if let Some(anchor) = self.anchor {
- overlay = overlay.anchor(anchor);
- }
- overlay = overlay.position(*position.borrow());
-
- let mut element = overlay.child(menu.clone()).into_any();
- menu_layout_id = Some(element.layout(cx));
- element
- });
-
- let mut child_element = self
- .child_builder
- .take()
- .map(|child_builder| (child_builder)(menu.borrow().is_some()));
-
- let child_layout_id = child_element
- .as_mut()
- .map(|child_element| child_element.layout(cx));
-
- let layout_id = cx.request_layout(
- &gpui::Style::default(),
- menu_layout_id.into_iter().chain(child_layout_id),
- );
-
- (
- layout_id,
- MenuHandleState {
- menu,
- position,
- child_element,
- child_layout_id,
- menu_element,
- },
- )
- }
-
- fn paint(
- self,
- bounds: Bounds<gpui::Pixels>,
- element_state: &mut Self::State,
- cx: &mut WindowContext,
- ) {
- if let Some(child) = element_state.child_element.take() {
- child.paint(cx);
- }
-
- if let Some(menu) = element_state.menu_element.take() {
- menu.paint(cx);
- return;
- }
-
- let Some(builder) = self.menu_builder else {
- return;
- };
- let menu = element_state.menu.clone();
- let position = element_state.position.clone();
- let attach = self.attach.clone();
- let child_layout_id = element_state.child_layout_id.clone();
-
- cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
- if phase == DispatchPhase::Bubble
- && event.button == MouseButton::Right
- && bounds.contains_point(&event.position)
- {
- cx.stop_propagation();
- cx.prevent_default();
-
- let new_menu = (builder)(cx);
- let menu2 = menu.clone();
- cx.subscribe(&new_menu, move |_modal, _: &DismissEvent, cx| {
- *menu2.borrow_mut() = None;
- cx.notify();
- })
- .detach();
- cx.focus_view(&new_menu);
- *menu.borrow_mut() = Some(new_menu);
-
- *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
- attach
- .unwrap()
- .corner(cx.layout_bounds(child_layout_id.unwrap()))
- } else {
- cx.mouse_position()
- };
- cx.notify();
- }
- });
- }
-}
-
-impl<M: ManagedView> IntoElement for MenuHandle<M> {
- type Element = Self;
-
- fn element_id(&self) -> Option<gpui::ElementId> {
- Some(self.id.clone())
- }
-
- fn into_element(self) -> Self::Element {
- self
- }
-}
@@ -27,6 +27,7 @@ pub enum Icon {
Bolt,
CaseSensitive,
Check,
+ Copy,
ChevronDown,
ChevronLeft,
ChevronRight,
@@ -54,6 +55,7 @@ pub enum Icon {
FolderX,
Hash,
InlayHint,
+ Link,
MagicWand,
MagnifyingGlass,
MailOpen,
@@ -99,6 +101,7 @@ impl Icon {
Icon::Bolt => "icons/bolt.svg",
Icon::CaseSensitive => "icons/case_insensitive.svg",
Icon::Check => "icons/check.svg",
+ Icon::Copy => "icons/copy.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
Icon::ChevronRight => "icons/chevron_right.svg",
@@ -126,6 +129,7 @@ impl Icon {
Icon::FolderX => "icons/stop_sharing.svg",
Icon::Hash => "icons/hash.svg",
Icon::InlayHint => "icons/inlay_hint.svg",
+ Icon::Link => "icons/link.svg",
Icon::MagicWand => "icons/magic-wand.svg",
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
Icon::MailOpen => "icons/mail-open.svg",
@@ -1,5 +1,5 @@
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
-use gpui::{relative, rems, Action, Div, IntoElement, Keystroke};
+use gpui::{relative, rems, Action, Div, FocusHandle, IntoElement, Keystroke};
#[derive(IntoElement, Clone)]
pub struct KeyBinding {
@@ -49,12 +49,21 @@ impl RenderOnce for KeyBinding {
impl KeyBinding {
pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
- // todo! this last is arbitrary, we want to prefer users key bindings over defaults,
- // and vim over normal (in vim mode), etc.
let key_binding = cx.bindings_for_action(action).last().cloned()?;
Some(Self::new(key_binding))
}
+ // like for_action(), but lets you specify the context from which keybindings
+ // are matched.
+ pub fn for_action_in(
+ action: &dyn Action,
+ focus: &FocusHandle,
+ cx: &mut WindowContext,
+ ) -> Option<Self> {
+ let key_binding = cx.bindings_for_action_in(action, focus).last().cloned()?;
+ Some(Self::new(key_binding))
+ }
+
fn icon_for_key(keystroke: &Keystroke) -> Option<Icon> {
let mut icon: Option<Icon> = None;
@@ -1,6 +1,8 @@
+use std::ops::Range;
+
use crate::prelude::*;
use crate::styled_ext::StyledExt;
-use gpui::{relative, Div, IntoElement, StyledText, TextRun, WindowContext};
+use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@@ -99,38 +101,32 @@ impl RenderOnce for HighlightedLabel {
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let highlight_color = cx.theme().colors().text_accent;
- let mut text_style = cx.text_style().clone();
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
-
- let mut runs: Vec<TextRun> = Vec::new();
-
- for (char_ix, char) in self.label.char_indices() {
- let mut color = self.color.color(cx);
-
- if let Some(highlight_ix) = highlight_indices.peek() {
- if char_ix == *highlight_ix {
- color = highlight_color;
- highlight_indices.next();
+ let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
+
+ while let Some(start_ix) = highlight_indices.next() {
+ let mut end_ix = start_ix;
+
+ loop {
+ end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
+ if let Some(&next_ix) = highlight_indices.peek() {
+ if next_ix == end_ix {
+ end_ix = next_ix;
+ highlight_indices.next();
+ continue;
+ }
}
+ break;
}
- let last_run = runs.last_mut();
- let start_new_run = if let Some(last_run) = last_run {
- if color == last_run.color {
- last_run.len += char.len_utf8();
- false
- } else {
- true
- }
- } else {
- true
- };
-
- if start_new_run {
- text_style.color = color;
- runs.push(text_style.to_run(char.len_utf8()))
- }
+ highlights.push((
+ start_ix..end_ix,
+ HighlightStyle {
+ color: Some(highlight_color),
+ ..Default::default()
+ },
+ ));
}
div()
@@ -150,7 +146,7 @@ impl RenderOnce for HighlightedLabel {
LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(),
})
- .child(StyledText::new(self.label).with_runs(runs))
+ .child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights))
}
}
@@ -1,73 +1,11 @@
+mod list;
mod list_header;
mod list_item;
mod list_separator;
mod list_sub_header;
-use gpui::{AnyElement, Div};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-use crate::{v_stack, Label};
-
+pub use list::*;
pub use list_header::*;
pub use list_item::*;
pub use list_separator::*;
pub use list_sub_header::*;
-
-#[derive(IntoElement)]
-pub struct List {
- /// Message to display when the list is empty
- /// Defaults to "No items"
- empty_message: SharedString,
- header: Option<ListHeader>,
- toggle: Option<bool>,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl List {
- pub fn new() -> Self {
- Self {
- empty_message: "No items".into(),
- header: None,
- toggle: None,
- children: SmallVec::new(),
- }
- }
-
- pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
- self.empty_message = empty_message.into();
- self
- }
-
- pub fn header(mut self, header: ListHeader) -> Self {
- self.header = Some(header);
- self
- }
-
- pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
- self.toggle = toggle.into();
- self
- }
-}
-
-impl ParentElement for List {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
- &mut self.children
- }
-}
-
-impl RenderOnce for List {
- type Rendered = Div;
-
- fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
- v_stack()
- .w_full()
- .py_1()
- .children(self.header.map(|header| header))
- .map(|this| match (self.children.is_empty(), self.toggle) {
- (false, _) => this.children(self.children),
- (true, Some(false)) => this,
- (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
- })
- }
-}
@@ -0,0 +1,60 @@
+use gpui::{AnyElement, Div};
+use smallvec::SmallVec;
+
+use crate::{prelude::*, v_stack, Label, ListHeader};
+
+#[derive(IntoElement)]
+pub struct List {
+ /// Message to display when the list is empty
+ /// Defaults to "No items"
+ empty_message: SharedString,
+ header: Option<ListHeader>,
+ toggle: Option<bool>,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+impl List {
+ pub fn new() -> Self {
+ Self {
+ empty_message: "No items".into(),
+ header: None,
+ toggle: None,
+ children: SmallVec::new(),
+ }
+ }
+
+ pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
+ self.empty_message = empty_message.into();
+ self
+ }
+
+ pub fn header(mut self, header: impl Into<Option<ListHeader>>) -> Self {
+ self.header = header.into();
+ self
+ }
+
+ pub fn toggle(mut self, toggle: impl Into<Option<bool>>) -> Self {
+ self.toggle = toggle.into();
+ self
+ }
+}
+
+impl ParentElement for List {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+impl RenderOnce for List {
+ type Rendered = Div;
+
+ fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
+ v_stack().w_full().py_1().children(self.header).map(|this| {
+ match (self.children.is_empty(), self.toggle) {
+ (false, _) => this.children(self.children),
+ (true, Some(false)) => this,
+ (true, _) => this.child(Label::new(self.empty_message.clone()).color(Color::Muted)),
+ }
+ })
+ }
+}
@@ -1,22 +1,16 @@
use std::rc::Rc;
-use gpui::{ClickEvent, Div};
+use gpui::{AnyElement, ClickEvent, Div};
+use smallvec::SmallVec;
use crate::prelude::*;
-use crate::{h_stack, Disclosure, Icon, IconButton, IconElement, IconSize, Label};
-
-pub enum ListHeaderMeta {
- Tools(Vec<IconButton>),
- // TODO: This should be a button
- Button(Label),
- Text(Label),
-}
+use crate::{h_stack, Disclosure, Icon, IconElement, IconSize, Label};
#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
- meta: Option<ListHeaderMeta>,
+ meta: SmallVec<[AnyElement; 2]>,
toggle: Option<bool>,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
inset: bool,
@@ -28,7 +22,7 @@ impl ListHeader {
Self {
label: label.into(),
left_icon: None,
- meta: None,
+ meta: SmallVec::new(),
inset: false,
toggle: None,
on_toggle: None,
@@ -49,17 +43,13 @@ impl ListHeader {
self
}
- pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
- self.left_icon = left_icon;
+ pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
+ self.left_icon = left_icon.into();
self
}
- pub fn right_button(self, button: IconButton) -> Self {
- self.meta(Some(ListHeaderMeta::Tools(vec![button])))
- }
-
- pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
- self.meta = meta;
+ pub fn meta(mut self, meta: impl IntoElement) -> Self {
+ self.meta.push(meta.into_any_element());
self
}
}
@@ -75,18 +65,6 @@ impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- let meta = match self.meta {
- Some(ListHeaderMeta::Tools(icons)) => div().child(
- h_stack()
- .gap_2()
- .items_center()
- .children(icons.into_iter().map(|i| i.icon_color(Color::Muted))),
- ),
- Some(ListHeaderMeta::Button(label)) => div().child(label),
- Some(ListHeaderMeta::Text(label)) => div().child(label),
- None => div(),
- };
-
h_stack().w_full().relative().child(
div()
.h_5()
@@ -120,7 +98,7 @@ impl RenderOnce for ListHeader {
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
),
)
- .child(meta),
+ .child(h_stack().gap_2().items_center().children(self.meta)),
)
}
}
@@ -1,7 +1,8 @@
use std::rc::Rc;
use gpui::{
- px, AnyElement, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels, Stateful,
+ px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
+ Stateful,
};
use smallvec::SmallVec;
@@ -21,6 +22,7 @@ pub struct ListItem {
inset: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
@@ -38,6 +40,7 @@ impl ListItem {
on_click: None,
on_secondary_mouse_down: None,
on_toggle: None,
+ tooltip: None,
children: SmallVec::new(),
}
}
@@ -55,6 +58,11 @@ impl ListItem {
self
}
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Box::new(tooltip));
+ self
+ }
+
pub fn inset(mut self, inset: bool) -> Self {
self.inset = inset;
self
@@ -149,6 +157,7 @@ impl RenderOnce for ListItem {
(on_mouse_down)(event, cx)
})
})
+ .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
.child(
div()
.when(self.inset, |this| this.px_2())
@@ -0,0 +1,231 @@
+use std::{cell::RefCell, rc::Rc};
+
+use gpui::{
+ overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase,
+ Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent,
+ ParentElement, Pixels, Point, View, VisualContext, WindowContext,
+};
+
+use crate::{Clickable, Selectable};
+
+pub trait PopoverTrigger: IntoElement + Clickable + Selectable + 'static {}
+
+impl<T: IntoElement + Clickable + Selectable + 'static> PopoverTrigger for T {}
+
+pub struct PopoverMenu<M: ManagedView> {
+ id: ElementId,
+ child_builder: Option<
+ Box<
+ dyn FnOnce(
+ Rc<RefCell<Option<View<M>>>>,
+ Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+ ) -> AnyElement
+ + 'static,
+ >,
+ >,
+ menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+ anchor: AnchorCorner,
+ attach: Option<AnchorCorner>,
+ offset: Option<Point<Pixels>>,
+}
+
+impl<M: ManagedView> PopoverMenu<M> {
+ pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
+ self.menu_builder = Some(Rc::new(f));
+ self
+ }
+
+ pub fn trigger<T: PopoverTrigger>(mut self, t: T) -> Self {
+ self.child_builder = Some(Box::new(|menu, builder| {
+ let open = menu.borrow().is_some();
+ t.selected(open)
+ .when_some(builder, |el, builder| {
+ el.on_click({
+ move |_, cx| {
+ let new_menu = (builder)(cx);
+ let menu2 = menu.clone();
+ let previous_focus_handle = cx.focused();
+
+ cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
+ if modal.focus_handle(cx).contains_focused(cx) {
+ if previous_focus_handle.is_some() {
+ cx.focus(&previous_focus_handle.as_ref().unwrap())
+ }
+ }
+ *menu2.borrow_mut() = None;
+ cx.notify();
+ })
+ .detach();
+ cx.focus_view(&new_menu);
+ *menu.borrow_mut() = Some(new_menu);
+ }
+ })
+ })
+ .into_any_element()
+ }));
+ self
+ }
+
+ /// anchor defines which corner of the menu to anchor to the attachment point
+ /// (by default the cursor position, but see attach)
+ pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+ self.anchor = anchor;
+ self
+ }
+
+ /// attach defines which corner of the handle to attach the menu's anchor to
+ pub fn attach(mut self, attach: AnchorCorner) -> Self {
+ self.attach = Some(attach);
+ self
+ }
+
+ /// offset offsets the position of the content by that many pixels.
+ pub fn offset(mut self, offset: Point<Pixels>) -> Self {
+ self.offset = Some(offset);
+ self
+ }
+
+ fn resolved_attach(&self) -> AnchorCorner {
+ self.attach.unwrap_or_else(|| match self.anchor {
+ AnchorCorner::TopLeft => AnchorCorner::BottomLeft,
+ AnchorCorner::TopRight => AnchorCorner::BottomRight,
+ AnchorCorner::BottomLeft => AnchorCorner::TopLeft,
+ AnchorCorner::BottomRight => AnchorCorner::TopRight,
+ })
+ }
+
+ fn resolved_offset(&self, cx: &WindowContext) -> Point<Pixels> {
+ self.offset.unwrap_or_else(|| {
+ // Default offset = 4px padding + 1px border
+ let offset = rems(5. / 16.) * cx.rem_size();
+ match self.anchor {
+ AnchorCorner::TopRight | AnchorCorner::BottomRight => point(offset, px(0.)),
+ AnchorCorner::TopLeft | AnchorCorner::BottomLeft => point(-offset, px(0.)),
+ }
+ })
+ }
+}
+
+pub fn popover_menu<M: ManagedView>(id: impl Into<ElementId>) -> PopoverMenu<M> {
+ PopoverMenu {
+ id: id.into(),
+ child_builder: None,
+ menu_builder: None,
+ anchor: AnchorCorner::TopLeft,
+ attach: None,
+ offset: None,
+ }
+}
+
+pub struct PopoverMenuState<M> {
+ child_layout_id: Option<LayoutId>,
+ child_element: Option<AnyElement>,
+ child_bounds: Option<Bounds<Pixels>>,
+ menu_element: Option<AnyElement>,
+ menu: Rc<RefCell<Option<View<M>>>>,
+}
+
+impl<M: ManagedView> Element for PopoverMenu<M> {
+ type State = PopoverMenuState<M>;
+
+ fn layout(
+ &mut self,
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
+ let mut menu_layout_id = None;
+
+ let (menu, child_bounds) = if let Some(element_state) = element_state {
+ (element_state.menu, element_state.child_bounds)
+ } else {
+ (Rc::default(), None)
+ };
+
+ let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+ let mut overlay = overlay().snap_to_window().anchor(self.anchor);
+
+ if let Some(child_bounds) = child_bounds {
+ overlay = overlay.position(
+ self.resolved_attach().corner(child_bounds) + self.resolved_offset(cx),
+ );
+ }
+
+ let mut element = overlay.child(menu.clone()).into_any();
+ menu_layout_id = Some(element.layout(cx));
+ element
+ });
+
+ let mut child_element = self
+ .child_builder
+ .take()
+ .map(|child_builder| (child_builder)(menu.clone(), self.menu_builder.clone()));
+
+ let child_layout_id = child_element
+ .as_mut()
+ .map(|child_element| child_element.layout(cx));
+
+ let layout_id = cx.request_layout(
+ &gpui::Style::default(),
+ menu_layout_id.into_iter().chain(child_layout_id),
+ );
+
+ (
+ layout_id,
+ PopoverMenuState {
+ menu,
+ child_element,
+ child_layout_id,
+ menu_element,
+ child_bounds,
+ },
+ )
+ }
+
+ fn paint(
+ self,
+ _: Bounds<gpui::Pixels>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
+ ) {
+ if let Some(child) = element_state.child_element.take() {
+ child.paint(cx);
+ }
+
+ if let Some(child_layout_id) = element_state.child_layout_id.take() {
+ element_state.child_bounds = Some(cx.layout_bounds(child_layout_id));
+ }
+
+ if let Some(menu) = element_state.menu_element.take() {
+ menu.paint(cx);
+
+ if let Some(child_bounds) = element_state.child_bounds {
+ let interactive_bounds = InteractiveBounds {
+ bounds: child_bounds,
+ stacking_order: cx.stacking_order().clone(),
+ };
+
+ // Mouse-downing outside the menu dismisses it, so we don't
+ // want a click on the toggle to re-open it.
+ cx.on_mouse_event(move |e: &MouseDownEvent, phase, cx| {
+ if phase == DispatchPhase::Bubble
+ && interactive_bounds.visibly_contains(&e.position, cx)
+ {
+ cx.stop_propagation()
+ }
+ })
+ }
+ }
+ }
+}
+
+impl<M: ManagedView> IntoElement for PopoverMenu<M> {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ Some(self.id.clone())
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
@@ -0,0 +1,185 @@
+use std::{cell::RefCell, rc::Rc};
+
+use gpui::{
+ overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId,
+ IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
+ View, VisualContext, WindowContext,
+};
+
+pub struct RightClickMenu<M: ManagedView> {
+ id: ElementId,
+ child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
+ menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
+ anchor: Option<AnchorCorner>,
+ attach: Option<AnchorCorner>,
+}
+
+impl<M: ManagedView> RightClickMenu<M> {
+ pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
+ self.menu_builder = Some(Rc::new(f));
+ self
+ }
+
+ pub fn trigger<E: IntoElement + 'static>(mut self, e: E) -> Self {
+ self.child_builder = Some(Box::new(move |_| e.into_any_element()));
+ self
+ }
+
+ /// anchor defines which corner of the menu to anchor to the attachment point
+ /// (by default the cursor position, but see attach)
+ pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+ self.anchor = Some(anchor);
+ self
+ }
+
+ /// attach defines which corner of the handle to attach the menu's anchor to
+ pub fn attach(mut self, attach: AnchorCorner) -> Self {
+ self.attach = Some(attach);
+ self
+ }
+}
+
+pub fn right_click_menu<M: ManagedView>(id: impl Into<ElementId>) -> RightClickMenu<M> {
+ RightClickMenu {
+ id: id.into(),
+ child_builder: None,
+ menu_builder: None,
+ anchor: None,
+ attach: None,
+ }
+}
+
+pub struct MenuHandleState<M> {
+ menu: Rc<RefCell<Option<View<M>>>>,
+ position: Rc<RefCell<Point<Pixels>>>,
+ child_layout_id: Option<LayoutId>,
+ child_element: Option<AnyElement>,
+ menu_element: Option<AnyElement>,
+}
+
+impl<M: ManagedView> Element for RightClickMenu<M> {
+ type State = MenuHandleState<M>;
+
+ fn layout(
+ &mut self,
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
+ let (menu, position) = if let Some(element_state) = element_state {
+ (element_state.menu, element_state.position)
+ } else {
+ (Rc::default(), Rc::default())
+ };
+
+ let mut menu_layout_id = None;
+
+ let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+ let mut overlay = overlay().snap_to_window();
+ if let Some(anchor) = self.anchor {
+ overlay = overlay.anchor(anchor);
+ }
+ overlay = overlay.position(*position.borrow());
+
+ let mut element = overlay.child(menu.clone()).into_any();
+ menu_layout_id = Some(element.layout(cx));
+ element
+ });
+
+ let mut child_element = self
+ .child_builder
+ .take()
+ .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+ let child_layout_id = child_element
+ .as_mut()
+ .map(|child_element| child_element.layout(cx));
+
+ let layout_id = cx.request_layout(
+ &gpui::Style::default(),
+ menu_layout_id.into_iter().chain(child_layout_id),
+ );
+
+ (
+ layout_id,
+ MenuHandleState {
+ menu,
+ position,
+ child_element,
+ child_layout_id,
+ menu_element,
+ },
+ )
+ }
+
+ fn paint(
+ self,
+ bounds: Bounds<gpui::Pixels>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
+ ) {
+ if let Some(child) = element_state.child_element.take() {
+ child.paint(cx);
+ }
+
+ if let Some(menu) = element_state.menu_element.take() {
+ menu.paint(cx);
+ return;
+ }
+
+ let Some(builder) = self.menu_builder else {
+ return;
+ };
+ let menu = element_state.menu.clone();
+ let position = element_state.position.clone();
+ let attach = self.attach.clone();
+ let child_layout_id = element_state.child_layout_id.clone();
+
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
+ if phase == DispatchPhase::Bubble
+ && event.button == MouseButton::Right
+ && bounds.contains_point(&event.position)
+ {
+ cx.stop_propagation();
+ cx.prevent_default();
+
+ let new_menu = (builder)(cx);
+ let menu2 = menu.clone();
+ let previous_focus_handle = cx.focused();
+
+ cx.subscribe(&new_menu, move |modal, _: &DismissEvent, cx| {
+ if modal.focus_handle(cx).contains_focused(cx) {
+ if previous_focus_handle.is_some() {
+ cx.focus(&previous_focus_handle.as_ref().unwrap())
+ }
+ }
+ *menu2.borrow_mut() = None;
+ cx.notify();
+ })
+ .detach();
+ cx.focus_view(&new_menu);
+ *menu.borrow_mut() = Some(new_menu);
+
+ *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+ attach
+ .unwrap()
+ .corner(cx.layout_bounds(child_layout_id.unwrap()))
+ } else {
+ cx.mouse_position()
+ };
+ cx.notify();
+ }
+ });
+ }
+}
+
+impl<M: ManagedView> IntoElement for RightClickMenu<M> {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ Some(self.id.clone())
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
@@ -8,6 +8,7 @@ mod icon_button;
mod keybinding;
mod label;
mod list;
+mod list_header;
mod list_item;
pub use avatar::*;
@@ -20,4 +21,5 @@ pub use icon_button::*;
pub use keybinding::*;
pub use label::*;
pub use list::*;
+pub use list_header::*;
pub use list_item::*;
@@ -1,7 +1,7 @@
use gpui::{Div, Render};
use story::Story;
-use crate::prelude::*;
+use crate::{prelude::*, Icon};
use crate::{Button, ButtonStyle};
pub struct ButtonStory;
@@ -14,6 +14,24 @@ impl Render for ButtonStory {
.child(Story::title_for::<Button>())
.child(Story::label("Default"))
.child(Button::new("default_filled", "Click me"))
+ .child(Story::label("Selected"))
+ .child(Button::new("selected_filled", "Click me").selected(true))
+ .child(Story::label("Selected with `selected_label`"))
+ .child(
+ Button::new("selected_label_filled", "Click me")
+ .selected(true)
+ .selected_label("I have been selected"),
+ )
+ .child(Story::label("With `label_color`"))
+ .child(Button::new("filled_with_label_color", "Click me").color(Color::Created))
+ .child(Story::label("With `icon`"))
+ .child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit))
+ .child(Story::label("Selected with `icon`"))
+ .child(
+ Button::new("filled_and_selected_with_icon", "Click me")
+ .selected(true)
+ .icon(Icon::FileGit),
+ )
.child(Story::label("Default (Subtle)"))
.child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle))
.child(Story::label("Default (Transparent)"))
@@ -2,7 +2,7 @@ use gpui::{actions, Action, AnchorCorner, Div, Render, View};
use story::Story;
use crate::prelude::*;
-use crate::{menu_handle, ContextMenu, Label};
+use crate::{right_click_menu, ContextMenu, Label};
actions!(PrintCurrentDate, PrintBestFood);
@@ -45,25 +45,13 @@ impl Render for ContextMenuStory {
.flex_col()
.justify_between()
.child(
- menu_handle("test2")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- })
+ right_click_menu("test2")
+ .trigger(Label::new("TOP LEFT"))
.menu(move |cx| build_menu(cx, "top left")),
)
.child(
- menu_handle("test1")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- })
+ right_click_menu("test1")
+ .trigger(Label::new("BOTTOM LEFT"))
.anchor(AnchorCorner::BottomLeft)
.attach(AnchorCorner::TopLeft)
.menu(move |cx| build_menu(cx, "bottom left")),
@@ -75,26 +63,14 @@ impl Render for ContextMenuStory {
.flex_col()
.justify_between()
.child(
- menu_handle("test3")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- })
+ right_click_menu("test3")
+ .trigger(Label::new("TOP RIGHT"))
.anchor(AnchorCorner::TopRight)
.menu(move |cx| build_menu(cx, "top right")),
)
.child(
- menu_handle("test4")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- })
+ right_click_menu("test4")
+ .trigger(Label::new("BOTTOM RIGHT"))
.anchor(AnchorCorner::BottomRight)
.attach(AnchorCorner::TopRight)
.menu(move |cx| build_menu(cx, "bottom right")),
@@ -20,6 +20,14 @@ impl Render for IconButtonStory {
.w_8()
.child(IconButton::new("icon_a", Icon::Hash).selected(true)),
)
+ .child(Story::label("Selected with `selected_icon`"))
+ .child(
+ div().w_8().child(
+ IconButton::new("icon_a", Icon::AudioOn)
+ .selected(true)
+ .selected_icon(Icon::AudioOff),
+ ),
+ )
.child(Story::label("Disabled"))
.child(
div()
@@ -22,12 +22,12 @@ impl Render for ListStory {
.child(Story::label("With sections"))
.child(
List::new()
- .child(ListHeader::new("Fruits"))
+ .header(ListHeader::new("Produce"))
+ .child(ListSubHeader::new("Fruits"))
.child(ListItem::new("apple").child("Apple"))
.child(ListItem::new("banana").child("Banana"))
.child(ListItem::new("cherry").child("Cherry"))
.child(ListSeparator)
- .child(ListHeader::new("Vegetables"))
.child(ListSubHeader::new("Root Vegetables"))
.child(ListItem::new("carrot").child("Carrot"))
.child(ListItem::new("potato").child("Potato"))
@@ -0,0 +1,33 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::{prelude::*, IconButton};
+use crate::{Icon, ListHeader};
+
+pub struct ListHeaderStory;
+
+impl Render for ListHeaderStory {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<ListHeader>())
+ .child(Story::label("Default"))
+ .child(ListHeader::new("Section 1"))
+ .child(Story::label("With left icon"))
+ .child(ListHeader::new("Section 2").left_icon(Icon::Bell))
+ .child(Story::label("With left icon and meta"))
+ .child(
+ ListHeader::new("Section 3")
+ .left_icon(Icon::BellOff)
+ .meta(IconButton::new("action_1", Icon::Bolt)),
+ )
+ .child(Story::label("With multiple meta"))
+ .child(
+ ListHeader::new("Section 4")
+ .meta(IconButton::new("action_1", Icon::Bolt))
+ .meta(IconButton::new("action_2", Icon::ExclamationTriangle))
+ .meta(IconButton::new("action_3", Icon::Plus)),
+ )
+ }
+}
@@ -1,4 +1,6 @@
use gpui::{px, Styled, WindowContext};
+use settings::Settings;
+use theme::ThemeSettings;
use crate::prelude::*;
use crate::{ElevationIndex, UITextSize};
@@ -60,6 +62,18 @@ pub trait StyledExt: Styled + Sized {
self.text_size(size)
}
+ /// The font size for buffer text.
+ ///
+ /// Retrieves the default font size, or the user's custom font size if set.
+ ///
+ /// This should only be used for text that is displayed in a buffer,
+ /// or other places that text needs to match the user's buffer font size.
+ fn text_buffer(self, cx: &mut WindowContext) -> Self {
+ let settings = ThemeSettings::get_global(cx);
+
+ self.text_size(settings.buffer_font_size)
+ }
+
/// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements
///
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
@@ -219,9 +219,11 @@ impl PathMatcher {
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
- other.as_ref().starts_with(&self.maybe_path)
- || self.glob.is_match(&other)
- || self.check_with_end_separator(other.as_ref())
+ let other_path = other.as_ref();
+ other_path.starts_with(&self.maybe_path)
+ || other_path.ends_with(&self.maybe_path)
+ || self.glob.is_match(other_path)
+ || self.check_with_end_separator(other_path)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
@@ -418,4 +420,14 @@ mod tests {
"Path matcher {path_matcher} should match {path:?}"
);
}
+
+ #[test]
+ fn project_search() {
+ let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
+ let path_matcher = PathMatcher::new("**/node_modules/**").unwrap();
+ assert!(
+ path_matcher.is_match(&path),
+ "Path matcher {path_matcher} should match {path:?}"
+ );
+ }
}
@@ -481,18 +481,21 @@ impl Pane {
pub(crate) fn open_item(
&mut self,
- project_entry_id: ProjectEntryId,
+ project_entry_id: Option<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
let mut existing_item = None;
- for (index, item) in self.items.iter().enumerate() {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
- {
- let item = item.boxed_clone();
- existing_item = Some((index, item));
- break;
+ if let Some(project_entry_id) = project_entry_id {
+ for (index, item) in self.items.iter().enumerate() {
+ if item.is_singleton(cx)
+ && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+ {
+ let item = item.boxed_clone();
+ existing_item = Some((index, item));
+ break;
+ }
}
}
@@ -2129,13 +2129,13 @@ impl Workspace {
})
}
- pub(crate) fn load_path(
+ fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -10,33 +10,33 @@ doctest = false
[features]
test-support = [
- "call2/test-support",
- "client2/test-support",
- "project2/test-support",
- "settings2/test-support",
+ "call/test-support",
+ "client/test-support",
+ "project/test-support",
+ "settings/test-support",
"gpui/test-support",
- "fs2/test-support"
+ "fs/test-support"
]
[dependencies]
-db2 = { path = "../db2" }
-client2 = { path = "../client2" }
+db = { path = "../db2", package = "db2" }
+call = { path = "../call2", package = "call2" }
+client = { path = "../client2", package = "client2" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
-fs2 = { path = "../fs2" }
+fs = { path = "../fs2", package = "fs2" }
gpui = { package = "gpui2", path = "../gpui2" }
-install_cli2 = { path = "../install_cli2" }
-language2 = { path = "../language2" }
+install_cli = { path = "../install_cli2", package = "install_cli2" }
+language = { path = "../language2", package = "language2" }
#menu = { path = "../menu" }
node_runtime = { path = "../node_runtime" }
-project2 = { path = "../project2" }
-settings2 = { path = "../settings2" }
-terminal2 = { path = "../terminal2" }
-theme2 = { path = "../theme2" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+terminal = { path = "../terminal2", package = "terminal2" }
+theme = { path = "../theme2", package = "theme2" }
util = { path = "../util" }
ui = { package = "ui2", path = "../ui2" }
-async-trait.workspace = true
async-recursion = "1.0.0"
itertools = "0.10"
bincode = "1.2.1"
@@ -54,13 +54,13 @@ smallvec.workspace = true
uuid.workspace = true
[dev-dependencies]
-call2 = { path = "../call2", features = ["test-support"] }
-client2 = { path = "../client2", features = ["test-support"] }
-gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
-project2 = { path = "../project2", features = ["test-support"] }
-settings2 = { path = "../settings2", features = ["test-support"] }
-fs2 = { path = "../fs2", features = ["test-support"] }
-db2 = { path = "../db2", features = ["test-support"] }
+call = { path = "../call2", package = "call2", features = ["test-support"] }
+client = { path = "../client2", package = "client2", features = ["test-support"] }
+gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
+project = { path = "../project2", package = "project2", features = ["test-support"] }
+settings = { path = "../settings2", package = "settings2", features = ["test-support"] }
+fs = { path = "../fs2", package = "fs2", features = ["test-support"] }
+db = { path = "../db2", package = "db2", features = ["test-support"] }
indoc.workspace = true
env_logger.workspace = true
@@ -7,8 +7,8 @@ use gpui::{
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
-use ui::prelude::*;
-use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
+use ui::{h_stack, ContextMenu, IconButton, Tooltip};
+use ui::{prelude::*, right_click_menu};
pub enum PanelEvent {
ChangePosition,
@@ -702,7 +702,7 @@ impl Render for PanelButtons {
};
Some(
- menu_handle(name)
+ right_click_menu(name)
.menu(move |cx| {
const POSITIONS: [DockPosition; 3] = [
DockPosition::Left,
@@ -726,14 +726,14 @@ impl Render for PanelButtons {
})
.anchor(menu_anchor)
.attach(menu_attach)
- .child(move |_is_open| {
+ .trigger(
IconButton::new(name, icon)
.selected(is_active_button)
.action(action.boxed_clone())
.tooltip(move |cx| {
Tooltip::for_action(tooltip.clone(), &*action, cx)
- })
- }),
+ }),
+ ),
)
});
@@ -7,7 +7,7 @@ use crate::{
ViewId, Workspace, WorkspaceId,
};
use anyhow::Result;
-use client2::{
+use client::{
proto::{self, PeerId},
Client,
};
@@ -16,10 +16,10 @@ use gpui::{
HighlightStyle, Model, Pixels, Point, SharedString, Task, View, ViewContext, WeakView,
WindowContext,
};
-use project2::{Project, ProjectEntryId, ProjectPath};
+use project::{Project, ProjectEntryId, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings2::Settings;
+use settings::Settings;
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
@@ -33,7 +33,7 @@ use std::{
},
time::Duration,
};
-use theme2::Theme;
+use theme::Theme;
#[derive(Deserialize)]
pub struct ItemSettings {
@@ -110,7 +110,7 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
fn for_each_project_item(
&self,
_: &AppContext,
- _: &mut dyn FnMut(EntityId, &dyn project2::Item),
+ _: &mut dyn FnMut(EntityId, &dyn project::Item),
) {
}
fn is_singleton(&self, _cx: &AppContext) -> bool {
@@ -222,7 +222,7 @@ pub trait ItemHandle: 'static + Send {
fn for_each_project_item(
&self,
_: &AppContext,
- _: &mut dyn FnMut(EntityId, &dyn project2::Item),
+ _: &mut dyn FnMut(EntityId, &dyn project::Item),
);
fn is_singleton(&self, cx: &AppContext) -> bool;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
@@ -347,7 +347,7 @@ impl<T: Item> ItemHandle for View<T> {
fn for_each_project_item(
&self,
cx: &AppContext,
- f: &mut dyn FnMut(EntityId, &dyn project2::Item),
+ f: &mut dyn FnMut(EntityId, &dyn project::Item),
) {
self.read(cx).for_each_project_item(cx, f)
}
@@ -375,6 +375,7 @@ impl<T: Item> ItemHandle for View<T> {
pane: View<Pane>,
cx: &mut ViewContext<Workspace>,
) {
+ let weak_item = self.downgrade();
let history = pane.read(cx).nav_history_for_item(self);
self.update(cx, |this, cx| {
this.set_nav_history(history, cx);
@@ -491,16 +492,15 @@ impl<T: Item> ItemHandle for View<T> {
}
}));
- // todo!()
- // cx.observe_focus(self, move |workspace, item, focused, cx| {
- // if !focused
- // && WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange
- // {
- // Pane::autosave_item(&item, workspace.project.clone(), cx)
- // .detach_and_log_err(cx);
- // }
- // })
- // .detach();
+ cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
+ if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {
+ if let Some(item) = weak_item.upgrade() {
+ Pane::autosave_item(&item, workspace.project.clone(), cx)
+ .detach_and_log_err(cx);
+ }
+ }
+ })
+ .detach();
let item_id = self.item_id();
cx.observe_release(self, move |workspace, _, _| {
@@ -640,7 +640,7 @@ impl<T: Item> WeakItemHandle for WeakView<T> {
}
pub trait ProjectItem: Item {
- type Item: project2::Item;
+ type Item: project::Item;
fn for_project_item(
project: Model<Project>,
@@ -662,19 +662,19 @@ pub trait FollowableEvents {
pub trait FollowableItem: Item {
type FollowableEvent: FollowableEvents;
fn remote_id(&self) -> Option<ViewId>;
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn from_state_proto(
pane: View<Pane>,
project: View<Workspace>,
id: ViewId,
state: &mut Option<proto::view::Variant>,
- cx: &mut AppContext,
+ cx: &mut WindowContext,
) -> Option<Task<Result<View<Self>>>>;
fn add_event_to_update_proto(
&self,
event: &Self::FollowableEvent,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool;
fn apply_update_proto(
&mut self,
@@ -682,20 +682,20 @@ pub trait FollowableItem: Item {
message: proto::update_view::Variant,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>>;
- fn is_project_item(&self, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &WindowContext) -> bool;
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
}
pub trait FollowableItemHandle: ItemHandle {
- fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId>;
+ fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId>;
fn set_leader_peer_id(&self, leader_peer_id: Option<PeerId>, cx: &mut WindowContext);
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant>;
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
fn add_event_to_update_proto(
&self,
event: &dyn Any,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool;
fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent>;
fn apply_update_proto(
@@ -704,11 +704,11 @@ pub trait FollowableItemHandle: ItemHandle {
message: proto::update_view::Variant,
cx: &mut WindowContext,
) -> Task<Result<()>>;
- fn is_project_item(&self, cx: &AppContext) -> bool;
+ fn is_project_item(&self, cx: &WindowContext) -> bool;
}
impl<T: FollowableItem> FollowableItemHandle for View<T> {
- fn remote_id(&self, client: &Arc<Client>, cx: &AppContext) -> Option<ViewId> {
+ fn remote_id(&self, client: &Arc<Client>, cx: &WindowContext) -> Option<ViewId> {
self.read(cx).remote_id().or_else(|| {
client.peer_id().map(|creator| ViewId {
creator,
@@ -721,7 +721,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
self.update(cx, |this, cx| this.set_leader_peer_id(leader_peer_id, cx))
}
- fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
self.read(cx).to_state_proto(cx)
}
@@ -729,7 +729,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
&self,
event: &dyn Any,
update: &mut Option<proto::update_view::Variant>,
- cx: &AppContext,
+ cx: &WindowContext,
) -> bool {
if let Some(event) = event.downcast_ref() {
self.read(cx).add_event_to_update_proto(event, update, cx)
@@ -754,305 +754,315 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
self.update(cx, |this, cx| this.apply_update_proto(project, message, cx))
}
- fn is_project_item(&self, cx: &AppContext) -> bool {
+ fn is_project_item(&self, cx: &WindowContext) -> bool {
self.read(cx).is_project_item(cx)
}
}
-// #[cfg(any(test, feature = "test-support"))]
-// pub mod test {
-// use super::{Item, ItemEvent};
-// use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
-// use gpui::{
-// elements::Empty, AnyElement, AppContext, Element, Entity, Model, Task, View,
-// ViewContext, View, WeakViewHandle,
-// };
-// use project2::{Project, ProjectEntryId, ProjectPath, WorktreeId};
-// use smallvec::SmallVec;
-// use std::{any::Any, borrow::Cow, cell::Cell, path::Path};
-
-// pub struct TestProjectItem {
-// pub entry_id: Option<ProjectEntryId>,
-// pub project_path: Option<ProjectPath>,
-// }
-
-// pub struct TestItem {
-// pub workspace_id: WorkspaceId,
-// pub state: String,
-// pub label: String,
-// pub save_count: usize,
-// pub save_as_count: usize,
-// pub reload_count: usize,
-// pub is_dirty: bool,
-// pub is_singleton: bool,
-// pub has_conflict: bool,
-// pub project_items: Vec<Model<TestProjectItem>>,
-// pub nav_history: Option<ItemNavHistory>,
-// pub tab_descriptions: Option<Vec<&'static str>>,
-// pub tab_detail: Cell<Option<usize>>,
-// }
-
-// impl Entity for TestProjectItem {
-// type Event = ();
-// }
-
-// impl project2::Item for TestProjectItem {
-// fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
-// self.entry_id
-// }
-
-// fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
-// self.project_path.clone()
-// }
-// }
-
-// pub enum TestItemEvent {
-// Edit,
-// }
-
-// impl Clone for TestItem {
-// fn clone(&self) -> Self {
-// Self {
-// state: self.state.clone(),
-// label: self.label.clone(),
-// save_count: self.save_count,
-// save_as_count: self.save_as_count,
-// reload_count: self.reload_count,
-// is_dirty: self.is_dirty,
-// is_singleton: self.is_singleton,
-// has_conflict: self.has_conflict,
-// project_items: self.project_items.clone(),
-// nav_history: None,
-// tab_descriptions: None,
-// tab_detail: Default::default(),
-// workspace_id: self.workspace_id,
-// }
-// }
-// }
-
-// impl TestProjectItem {
-// pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
-// let entry_id = Some(ProjectEntryId::from_proto(id));
-// let project_path = Some(ProjectPath {
-// worktree_id: WorktreeId::from_usize(0),
-// path: Path::new(path).into(),
-// });
-// cx.add_model(|_| Self {
-// entry_id,
-// project_path,
-// })
-// }
-
-// pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
-// cx.add_model(|_| Self {
-// project_path: None,
-// entry_id: None,
-// })
-// }
-// }
-
-// impl TestItem {
-// pub fn new() -> Self {
-// Self {
-// state: String::new(),
-// label: String::new(),
-// save_count: 0,
-// save_as_count: 0,
-// reload_count: 0,
-// is_dirty: false,
-// has_conflict: false,
-// project_items: Vec::new(),
-// is_singleton: true,
-// nav_history: None,
-// tab_descriptions: None,
-// tab_detail: Default::default(),
-// workspace_id: 0,
-// }
-// }
-
-// pub fn new_deserialized(id: WorkspaceId) -> Self {
-// let mut this = Self::new();
-// this.workspace_id = id;
-// this
-// }
-
-// pub fn with_label(mut self, state: &str) -> Self {
-// self.label = state.to_string();
-// self
-// }
-
-// pub fn with_singleton(mut self, singleton: bool) -> Self {
-// self.is_singleton = singleton;
-// self
-// }
-
-// pub fn with_dirty(mut self, dirty: bool) -> Self {
-// self.is_dirty = dirty;
-// self
-// }
-
-// pub fn with_conflict(mut self, has_conflict: bool) -> Self {
-// self.has_conflict = has_conflict;
-// self
-// }
-
-// pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
-// self.project_items.clear();
-// self.project_items.extend(items.iter().cloned());
-// self
-// }
-
-// pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
-// self.push_to_nav_history(cx);
-// self.state = state;
-// }
-
-// fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
-// if let Some(history) = &mut self.nav_history {
-// history.push(Some(Box::new(self.state.clone())), cx);
-// }
-// }
-// }
-
-// impl Entity for TestItem {
-// type Event = TestItemEvent;
-// }
-
-// impl View for TestItem {
-// fn ui_name() -> &'static str {
-// "TestItem"
-// }
-
-// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
-// Empty::new().into_any()
-// }
-// }
-
-// impl Item for TestItem {
-// fn tab_description(&self, detail: usize, _: &AppContext) -> Option<Cow<str>> {
-// self.tab_descriptions.as_ref().and_then(|descriptions| {
-// let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
-// Some(description.into())
-// })
-// }
-
-// fn tab_content<V: 'static>(
-// &self,
-// detail: Option<usize>,
-// _: &theme2::Tab,
-// _: &AppContext,
-// ) -> AnyElement<V> {
-// self.tab_detail.set(detail);
-// Empty::new().into_any()
-// }
-
-// fn for_each_project_item(
-// &self,
-// cx: &AppContext,
-// f: &mut dyn FnMut(usize, &dyn project2::Item),
-// ) {
-// self.project_items
-// .iter()
-// .for_each(|item| f(item.id(), item.read(cx)))
-// }
-
-// fn is_singleton(&self, _: &AppContext) -> bool {
-// self.is_singleton
-// }
-
-// fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
-// self.nav_history = Some(history);
-// }
-
-// fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
-// let state = *state.downcast::<String>().unwrap_or_default();
-// if state != self.state {
-// self.state = state;
-// true
-// } else {
-// false
-// }
-// }
-
-// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
-// self.push_to_nav_history(cx);
-// }
-
-// fn clone_on_split(
-// &self,
-// _workspace_id: WorkspaceId,
-// _: &mut ViewContext<Self>,
-// ) -> Option<Self>
-// where
-// Self: Sized,
-// {
-// Some(self.clone())
-// }
-
-// fn is_dirty(&self, _: &AppContext) -> bool {
-// self.is_dirty
-// }
-
-// fn has_conflict(&self, _: &AppContext) -> bool {
-// self.has_conflict
-// }
-
-// fn can_save(&self, cx: &AppContext) -> bool {
-// !self.project_items.is_empty()
-// && self
-// .project_items
-// .iter()
-// .all(|item| item.read(cx).entry_id.is_some())
-// }
-
-// fn save(
-// &mut self,
-// _: Model<Project>,
-// _: &mut ViewContext<Self>,
-// ) -> Task<anyhow::Result<()>> {
-// self.save_count += 1;
-// self.is_dirty = false;
-// Task::ready(Ok(()))
-// }
-
-// fn save_as(
-// &mut self,
-// _: Model<Project>,
-// _: std::path::PathBuf,
-// _: &mut ViewContext<Self>,
-// ) -> Task<anyhow::Result<()>> {
-// self.save_as_count += 1;
-// self.is_dirty = false;
-// Task::ready(Ok(()))
-// }
-
-// fn reload(
-// &mut self,
-// _: Model<Project>,
-// _: &mut ViewContext<Self>,
-// ) -> Task<anyhow::Result<()>> {
-// self.reload_count += 1;
-// self.is_dirty = false;
-// Task::ready(Ok(()))
-// }
-
-// fn to_item_events(_: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
-// [ItemEvent::UpdateTab, ItemEvent::Edit].into()
-// }
-
-// fn serialized_item_kind() -> Option<&'static str> {
-// Some("TestItem")
-// }
-
-// fn deserialize(
-// _project: Model<Project>,
-// _workspace: WeakViewHandle<Workspace>,
-// workspace_id: WorkspaceId,
-// _item_id: ItemId,
-// cx: &mut ViewContext<Pane>,
-// ) -> Task<anyhow::Result<View<Self>>> {
-// let view = cx.add_view(|_cx| Self::new_deserialized(workspace_id));
-// Task::Ready(Some(anyhow::Ok(view)))
-// }
-// }
-// }
+#[cfg(any(test, feature = "test-support"))]
+pub mod test {
+ use super::{Item, ItemEvent};
+ use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
+ use gpui::{
+ AnyElement, AppContext, Context as _, Div, EntityId, EventEmitter, FocusableView,
+ IntoElement, Model, Render, SharedString, Task, View, ViewContext, VisualContext, WeakView,
+ };
+ use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
+ use std::{any::Any, cell::Cell, path::Path};
+
+ pub struct TestProjectItem {
+ pub entry_id: Option<ProjectEntryId>,
+ pub project_path: Option<ProjectPath>,
+ }
+
+ pub struct TestItem {
+ pub workspace_id: WorkspaceId,
+ pub state: String,
+ pub label: String,
+ pub save_count: usize,
+ pub save_as_count: usize,
+ pub reload_count: usize,
+ pub is_dirty: bool,
+ pub is_singleton: bool,
+ pub has_conflict: bool,
+ pub project_items: Vec<Model<TestProjectItem>>,
+ pub nav_history: Option<ItemNavHistory>,
+ pub tab_descriptions: Option<Vec<&'static str>>,
+ pub tab_detail: Cell<Option<usize>>,
+ focus_handle: gpui::FocusHandle,
+ }
+
+ impl project::Item for TestProjectItem {
+ fn entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
+ self.entry_id
+ }
+
+ fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
+ self.project_path.clone()
+ }
+ }
+
+ pub enum TestItemEvent {
+ Edit,
+ }
+
+ // impl Clone for TestItem {
+ // fn clone(&self) -> Self {
+ // Self {
+ // state: self.state.clone(),
+ // label: self.label.clone(),
+ // save_count: self.save_count,
+ // save_as_count: self.save_as_count,
+ // reload_count: self.reload_count,
+ // is_dirty: self.is_dirty,
+ // is_singleton: self.is_singleton,
+ // has_conflict: self.has_conflict,
+ // project_items: self.project_items.clone(),
+ // nav_history: None,
+ // tab_descriptions: None,
+ // tab_detail: Default::default(),
+ // workspace_id: self.workspace_id,
+ // focus_handle: self.focus_handle.clone(),
+ // }
+ // }
+ // }
+
+ impl TestProjectItem {
+ pub fn new(id: u64, path: &str, cx: &mut AppContext) -> Model<Self> {
+ let entry_id = Some(ProjectEntryId::from_proto(id));
+ let project_path = Some(ProjectPath {
+ worktree_id: WorktreeId::from_usize(0),
+ path: Path::new(path).into(),
+ });
+ cx.build_model(|_| Self {
+ entry_id,
+ project_path,
+ })
+ }
+
+ pub fn new_untitled(cx: &mut AppContext) -> Model<Self> {
+ cx.build_model(|_| Self {
+ project_path: None,
+ entry_id: None,
+ })
+ }
+ }
+
+ impl TestItem {
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ Self {
+ state: String::new(),
+ label: String::new(),
+ save_count: 0,
+ save_as_count: 0,
+ reload_count: 0,
+ is_dirty: false,
+ has_conflict: false,
+ project_items: Vec::new(),
+ is_singleton: true,
+ nav_history: None,
+ tab_descriptions: None,
+ tab_detail: Default::default(),
+ workspace_id: 0,
+ focus_handle: cx.focus_handle(),
+ }
+ }
+
+ pub fn new_deserialized(id: WorkspaceId, cx: &mut ViewContext<Self>) -> Self {
+ let mut this = Self::new(cx);
+ this.workspace_id = id;
+ this
+ }
+
+ pub fn with_label(mut self, state: &str) -> Self {
+ self.label = state.to_string();
+ self
+ }
+
+ pub fn with_singleton(mut self, singleton: bool) -> Self {
+ self.is_singleton = singleton;
+ self
+ }
+
+ pub fn with_dirty(mut self, dirty: bool) -> Self {
+ self.is_dirty = dirty;
+ self
+ }
+
+ pub fn with_conflict(mut self, has_conflict: bool) -> Self {
+ self.has_conflict = has_conflict;
+ self
+ }
+
+ pub fn with_project_items(mut self, items: &[Model<TestProjectItem>]) -> Self {
+ self.project_items.clear();
+ self.project_items.extend(items.iter().cloned());
+ self
+ }
+
+ pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
+ self.push_to_nav_history(cx);
+ self.state = state;
+ }
+
+ fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(history) = &mut self.nav_history {
+ history.push(Some(Box::new(self.state.clone())), cx);
+ }
+ }
+ }
+
+ impl Render for TestItem {
+ type Element = Div;
+
+ fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
+ gpui::div()
+ }
+ }
+
+ impl EventEmitter<ItemEvent> for TestItem {}
+
+ impl FocusableView for TestItem {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.focus_handle.clone()
+ }
+ }
+
+ impl Item for TestItem {
+ fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
+ self.tab_descriptions.as_ref().and_then(|descriptions| {
+ let description = *descriptions.get(detail).or_else(|| descriptions.last())?;
+ Some(description.into())
+ })
+ }
+
+ fn tab_content(
+ &self,
+ detail: Option<usize>,
+ cx: &ui::prelude::WindowContext,
+ ) -> AnyElement {
+ self.tab_detail.set(detail);
+ gpui::div().into_any_element()
+ }
+
+ fn for_each_project_item(
+ &self,
+ cx: &AppContext,
+ f: &mut dyn FnMut(EntityId, &dyn project::Item),
+ ) {
+ self.project_items
+ .iter()
+ .for_each(|item| f(item.entity_id(), item.read(cx)))
+ }
+
+ fn is_singleton(&self, _: &AppContext) -> bool {
+ self.is_singleton
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+ self.nav_history = Some(history);
+ }
+
+ fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
+ let state = *state.downcast::<String>().unwrap_or_default();
+ if state != self.state {
+ self.state = state;
+ true
+ } else {
+ false
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.push_to_nav_history(cx);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<View<Self>>
+ where
+ Self: Sized,
+ {
+ Some(cx.build_view(|cx| Self {
+ state: self.state.clone(),
+ label: self.label.clone(),
+ save_count: self.save_count,
+ save_as_count: self.save_as_count,
+ reload_count: self.reload_count,
+ is_dirty: self.is_dirty,
+ is_singleton: self.is_singleton,
+ has_conflict: self.has_conflict,
+ project_items: self.project_items.clone(),
+ nav_history: None,
+ tab_descriptions: None,
+ tab_detail: Default::default(),
+ workspace_id: self.workspace_id,
+ focus_handle: cx.focus_handle(),
+ }))
+ }
+
+ fn is_dirty(&self, _: &AppContext) -> bool {
+ self.is_dirty
+ }
+
+ fn has_conflict(&self, _: &AppContext) -> bool {
+ self.has_conflict
+ }
+
+ fn can_save(&self, cx: &AppContext) -> bool {
+ !self.project_items.is_empty()
+ && self
+ .project_items
+ .iter()
+ .all(|item| item.read(cx).entry_id.is_some())
+ }
+
+ fn save(
+ &mut self,
+ _: Model<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn save_as(
+ &mut self,
+ _: Model<Project>,
+ _: std::path::PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.save_as_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn reload(
+ &mut self,
+ _: Model<Project>,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.reload_count += 1;
+ self.is_dirty = false;
+ Task::ready(Ok(()))
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("TestItem")
+ }
+
+ fn deserialize(
+ _project: Model<Project>,
+ _workspace: WeakView<Workspace>,
+ workspace_id: WorkspaceId,
+ _item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<View<Self>>> {
+ let view = cx.build_view(|cx| Self::new_deserialized(workspace_id, cx));
+ Task::Ready(Some(anyhow::Ok(view)))
+ }
+ }
+}
@@ -135,24 +135,22 @@ impl Workspace {
}
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
- todo!()
- // self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
- // self.show_notification(toast.id, cx, |cx| {
- // cx.add_view(|_cx| match toast.on_click.as_ref() {
- // Some((click_msg, on_click)) => {
- // let on_click = on_click.clone();
- // simple_message_notification::MessageNotification::new(toast.msg.clone())
- // .with_click_message(click_msg.clone())
- // .on_click(move |cx| on_click(cx))
- // }
- // None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
- // })
- // })
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
+ self.show_notification(toast.id, cx, |cx| {
+ cx.build_view(|_cx| match toast.on_click.as_ref() {
+ Some((click_msg, on_click)) => {
+ let on_click = on_click.clone();
+ simple_message_notification::MessageNotification::new(toast.msg.clone())
+ .with_click_message(click_msg.clone())
+ .on_click(move |cx| on_click(cx))
+ }
+ None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
+ })
+ })
}
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
- todo!()
- // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
}
fn dismiss_notification_internal(
@@ -179,33 +177,10 @@ pub mod simple_message_notification {
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
ViewContext,
};
- use serde::Deserialize;
- use std::{borrow::Cow, sync::Arc};
+ use std::sync::Arc;
use ui::prelude::*;
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
- #[derive(Clone, Default, Deserialize, PartialEq)]
- pub struct OsOpen(pub Cow<'static, str>);
-
- impl OsOpen {
- pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
- OsOpen(url.into())
- }
- }
-
- // todo!()
- // impl_actions!(message_notifications, [OsOpen]);
- //
- // todo!()
- // pub fn init(cx: &mut AppContext) {
- // cx.add_action(MessageNotification::dismiss);
- // cx.add_action(
- // |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
- // cx.platform().open_url(open_action.0.as_ref());
- // },
- // )
- // }
-
enum NotificationMessage {
Text(SharedString),
Element(fn(TextStyle, &AppContext) -> AnyElement),
@@ -213,7 +188,7 @@ pub mod simple_message_notification {
pub struct MessageNotification {
message: NotificationMessage,
- on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
+ on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<SharedString>,
}
@@ -252,7 +227,7 @@ pub mod simple_message_notification {
pub fn on_click<F>(mut self, on_click: F) -> Self
where
- F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
+ F: 'static + Fn(&mut ViewContext<Self>),
{
self.on_click = Some(Arc::new(on_click));
self
@@ -2,19 +2,20 @@ use crate::{
item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
toolbar::Toolbar,
workspace_settings::{AutosaveSetting, WorkspaceSettings},
- SplitDirection, Workspace,
+ NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
};
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
- actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
- FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
- ViewContext, VisualContext, WeakView, WindowContext,
+ actions, overlay, prelude::*, rems, Action, AnchorCorner, AnyWeakView, AppContext,
+ AsyncWindowContext, DismissEvent, Div, EntityId, EventEmitter, FocusHandle, Focusable,
+ FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View, ViewContext,
+ VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
-use project2::{Project, ProjectEntryId, ProjectPath};
+use project::{Project, ProjectEntryId, ProjectPath};
use serde::Deserialize;
-use settings2::Settings;
+use settings::Settings;
use std::{
any::Any,
cmp, fmt, mem,
@@ -25,8 +26,10 @@ use std::{
},
};
-use ui::v_stack;
-use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
+use ui::{
+ h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
+};
+use ui::{v_stack, ContextMenu};
use util::truncate_and_remove_front;
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -50,7 +53,7 @@ pub enum SaveIntent {
//todo!("Do we need the default bound on actions? Decide soon")
// #[register_action]
-#[derive(Clone, Deserialize, PartialEq, Debug)]
+#[derive(Action, Clone, Deserialize, PartialEq, Debug)]
pub struct ActivateItem(pub usize);
// #[derive(Clone, PartialEq)]
@@ -143,17 +146,24 @@ impl fmt::Debug for Event {
}
}
+struct FocusedView {
+ view: AnyWeakView,
+ focus_handle: FocusHandle,
+}
+
pub struct Pane {
focus_handle: FocusHandle,
items: Vec<Box<dyn ItemHandle>>,
activation_history: Vec<EntityId>,
zoomed: bool,
active_item_index: usize,
- // last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
+ last_focused_view_by_item: HashMap<EntityId, FocusHandle>,
autoscroll: bool,
nav_history: NavHistory,
toolbar: View<Toolbar>,
- // tab_bar_context_menu: TabBarContextMenu,
+ tab_bar_focus_handle: FocusHandle,
+ new_item_menu: Option<View<ContextMenu>>,
+ split_item_menu: Option<View<ContextMenu>>,
// tab_context_menu: ViewHandle<ContextMenu>,
workspace: WeakView<Workspace>,
project: Model<Project>,
@@ -306,7 +316,7 @@ impl Pane {
activation_history: Vec::new(),
zoomed: false,
active_item_index: 0,
- // last_focused_view_by_item: Default::default(),
+ last_focused_view_by_item: Default::default(),
autoscroll: false,
nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState {
mode: NavigationMode::Normal,
@@ -318,6 +328,9 @@ impl Pane {
next_timestamp,
}))),
toolbar: cx.build_view(|_| Toolbar::new()),
+ tab_bar_focus_handle: cx.focus_handle(),
+ new_item_menu: None,
+ split_item_menu: None,
// tab_bar_context_menu: TabBarContextMenu {
// kind: TabBarContextMenuKind::New,
// handle: context_menu,
@@ -392,9 +405,48 @@ impl Pane {
}
pub fn has_focus(&self, cx: &WindowContext) -> bool {
+ // todo!(); // inline this manually
self.focus_handle.contains_focused(cx)
}
+ fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+ if !self.has_focus(cx) {
+ cx.emit(Event::Focus);
+ cx.notify();
+ }
+
+ self.toolbar.update(cx, |toolbar, cx| {
+ toolbar.focus_changed(true, cx);
+ });
+
+ if let Some(active_item) = self.active_item() {
+ if self.focus_handle.is_focused(cx) {
+ // Pane was focused directly. We need to either focus a view inside the active item,
+ // or focus the active item itself
+ if let Some(weak_last_focused_view) =
+ self.last_focused_view_by_item.get(&active_item.item_id())
+ {
+ weak_last_focused_view.focus(cx);
+ return;
+ }
+
+ active_item.focus_handle(cx).focus(cx);
+ } else if !self.tab_bar_focus_handle.contains_focused(cx) {
+ if let Some(focused) = cx.focused() {
+ self.last_focused_view_by_item
+ .insert(active_item.item_id(), focused);
+ }
+ }
+ }
+ }
+
+ fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
+ self.toolbar.update(cx, |toolbar, cx| {
+ toolbar.focus_changed(false, cx);
+ });
+ cx.notify();
+ }
+
pub fn active_item_index(&self) -> usize {
self.active_item_index
}
@@ -457,24 +509,49 @@ impl Pane {
!self.nav_history.0.lock().forward_stack.is_empty()
}
+ fn navigate_backward(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let pane = cx.view().downgrade();
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.go_back(pane, cx).detach_and_log_err(cx)
+ })
+ })
+ }
+ }
+
+ fn navigate_forward(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let pane = cx.view().downgrade();
+ cx.window_context().defer(move |cx| {
+ workspace.update(cx, |workspace, cx| {
+ workspace.go_forward(pane, cx).detach_and_log_err(cx)
+ })
+ })
+ }
+ }
+
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
self.toolbar.update(cx, |_, cx| cx.notify());
}
pub(crate) fn open_item(
&mut self,
- project_entry_id: ProjectEntryId,
+ project_entry_id: Option<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
let mut existing_item = None;
- for (index, item) in self.items.iter().enumerate() {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
- {
- let item = item.boxed_clone();
- existing_item = Some((index, item));
- break;
+ if let Some(project_entry_id) = project_entry_id {
+ for (index, item) in self.items.iter().enumerate() {
+ if item.is_singleton(cx)
+ && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+ {
+ let item = item.boxed_clone();
+ existing_item = Some((index, item));
+ break;
+ }
}
}
@@ -630,21 +707,16 @@ impl Pane {
.position(|i| i.item_id() == item.item_id())
}
- // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
- // // Potentially warn the user of the new keybinding
- // let workspace_handle = self.workspace().clone();
- // cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
- // .detach();
-
- // if self.zoomed {
- // cx.emit(Event::ZoomOut);
- // } else if !self.items.is_empty() {
- // if !self.has_focus {
- // cx.focus_self();
- // }
- // cx.emit(Event::ZoomIn);
- // }
- // }
+ pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+ if self.zoomed {
+ cx.emit(Event::ZoomOut);
+ } else if !self.items.is_empty() {
+ if !self.focus_handle.contains_focused(cx) {
+ cx.focus_self();
+ }
+ cx.emit(Event::ZoomIn);
+ }
+ }
pub fn activate_item(
&mut self,
@@ -1381,7 +1453,7 @@ impl Pane {
let close_right = ItemSettings::get_global(cx).close_position.right();
let is_active = ix == self.active_item_index;
- div()
+ let tab = div()
.group("")
.id(ix)
.cursor_pointer()
@@ -1455,43 +1527,68 @@ impl Pane {
.children((!close_right).then(|| close_icon()))
.child(label)
.children(close_right.then(|| close_icon())),
- )
+ );
+
+ right_click_menu(ix).trigger(tab).menu(|cx| {
+ ContextMenu::build(cx, |menu, cx| {
+ menu.action(
+ "Close Active Item",
+ CloseActiveItem { save_intent: None }.boxed_clone(),
+ )
+ .action("Close Inactive Items", CloseInactiveItems.boxed_clone())
+ .action("Close Clean Items", CloseCleanItems.boxed_clone())
+ .action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone())
+ .action(
+ "Close Items To The Right",
+ CloseItemsToTheRight.boxed_clone(),
+ )
+ .action(
+ "Close All Items",
+ CloseAllItems { save_intent: None }.boxed_clone(),
+ )
+ })
+ })
}
fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
div()
- .group("tab_bar")
.id("tab_bar")
+ .group("tab_bar")
+ .track_focus(&self.tab_bar_focus_handle)
.w_full()
+ // 30px @ 16px/rem
+ .h(rems(1.875))
+ .overflow_hidden()
.flex()
+ .flex_none()
.bg(cx.theme().colors().tab_bar_background)
// Left Side
.child(
- div()
- .relative()
- .px_1()
+ h_stack()
+ .px_2()
.flex()
.flex_none()
- .gap_2()
+ .gap_1()
// Nav Buttons
.child(
- div()
- .right_0()
- .flex()
- .items_center()
- .gap_px()
- .child(
- div().border().border_color(gpui::red()).child(
- IconButton::new("navigate_backward", Icon::ArrowLeft)
- .disabled(!self.can_navigate_backward()),
- ),
- )
- .child(
- div().border().border_color(gpui::red()).child(
- IconButton::new("navigate_forward", Icon::ArrowRight)
- .disabled(!self.can_navigate_forward()),
- ),
- ),
+ div().border().border_color(gpui::red()).child(
+ IconButton::new("navigate_backward", Icon::ArrowLeft)
+ .on_click({
+ let view = cx.view().clone();
+ move |_, cx| view.update(cx, Self::navigate_backward)
+ })
+ .disabled(!self.can_navigate_backward()),
+ ),
+ )
+ .child(
+ div().border().border_color(gpui::red()).child(
+ IconButton::new("navigate_forward", Icon::ArrowRight)
+ .on_click({
+ let view = cx.view().clone();
+ move |_, cx| view.update(cx, Self::navigate_backward)
+ })
+ .disabled(!self.can_navigate_forward()),
+ ),
),
)
.child(
@@ -1520,20 +1617,78 @@ impl Pane {
.gap_px()
.child(
div()
+ .bg(gpui::blue())
.border()
.border_color(gpui::red())
- .child(IconButton::new("plus", Icon::Plus)),
+ .child(IconButton::new("plus", Icon::Plus).on_click(
+ cx.listener(|this, _, cx| {
+ let menu = ContextMenu::build(cx, |menu, cx| {
+ menu.action("New File", NewFile.boxed_clone())
+ .action(
+ "New Terminal",
+ NewCenterTerminal.boxed_clone(),
+ )
+ .action("New Search", NewSearch.boxed_clone())
+ });
+ cx.subscribe(
+ &menu,
+ |this, _, event: &DismissEvent, cx| {
+ this.focus(cx);
+ this.new_item_menu = None;
+ },
+ )
+ .detach();
+ this.new_item_menu = Some(menu);
+ }),
+ ))
+ .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
+ el.child(Self::render_menu_overlay(new_item_menu))
+ }),
)
.child(
div()
.border()
.border_color(gpui::red())
- .child(IconButton::new("split", Icon::Split)),
+ .child(IconButton::new("split", Icon::Split).on_click(
+ cx.listener(|this, _, cx| {
+ let menu = ContextMenu::build(cx, |menu, cx| {
+ menu.action("Split Right", SplitRight.boxed_clone())
+ .action("Split Left", SplitLeft.boxed_clone())
+ .action("Split Up", SplitUp.boxed_clone())
+ .action("Split Down", SplitDown.boxed_clone())
+ });
+ cx.subscribe(
+ &menu,
+ |this, _, event: &DismissEvent, cx| {
+ this.focus(cx);
+ this.split_item_menu = None;
+ },
+ )
+ .detach();
+ this.split_item_menu = Some(menu);
+ }),
+ ))
+ .when_some(
+ self.split_item_menu.as_ref(),
+ |el, split_item_menu| {
+ el.child(Self::render_menu_overlay(split_item_menu))
+ },
+ ),
),
),
)
}
+ fn render_menu_overlay(menu: &View<ContextMenu>) -> Div {
+ div()
+ .absolute()
+ .z_index(1)
+ .bottom_0()
+ .right_0()
+ .size_0()
+ .child(overlay().anchor(AnchorCorner::TopRight).child(menu.clone()))
+ }
+
// fn render_tabs(&mut self, cx: &mut ViewContext<Self>) -> impl Element<Self> {
// let theme = theme::current(cx).clone();
@@ -1932,40 +2087,78 @@ impl Render for Pane {
type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let this = cx.view().downgrade();
+
v_stack()
.key_context("Pane")
.track_focus(&self.focus_handle)
- .on_action(cx.listener(|pane: &mut Pane, _: &SplitLeft, cx| {
- pane.split(SplitDirection::Left, cx)
- }))
+ .on_focus_in({
+ let this = this.clone();
+ move |event, cx| {
+ this.update(cx, |this, cx| this.focus_in(cx)).ok();
+ }
+ })
+ .on_focus_out({
+ let this = this.clone();
+ move |event, cx| {
+ this.update(cx, |this, cx| this.focus_out(cx)).ok();
+ }
+ })
+ .on_action(cx.listener(|pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx)))
+ .on_action(cx.listener(|pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)))
.on_action(
- cx.listener(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)),
+ cx.listener(|pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)),
)
- .on_action(cx.listener(|pane: &mut Pane, _: &SplitRight, cx| {
- pane.split(SplitDirection::Right, cx)
+ .on_action(cx.listener(|pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)))
+ .on_action(cx.listener(|pane, _: &GoBack, cx| pane.navigate_backward(cx)))
+ .on_action(cx.listener(|pane, _: &GoForward, cx| pane.navigate_forward(cx)))
+ .on_action(cx.listener(Pane::toggle_zoom))
+ .on_action(cx.listener(|pane: &mut Pane, action: &ActivateItem, cx| {
+ pane.activate_item(action.0, true, true, cx);
+ }))
+ .on_action(cx.listener(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+ pane.activate_item(pane.items.len() - 1, true, true, cx);
}))
- .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| {
- pane.split(SplitDirection::Down, cx)
+ .on_action(cx.listener(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+ pane.activate_prev_item(true, cx);
+ }))
+ .on_action(cx.listener(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+ pane.activate_next_item(true, cx);
+ }))
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
+ pane.close_active_item(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseInactiveItems, cx| {
+ pane.close_inactive_items(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseCleanItems, cx| {
+ pane.close_clean_items(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseItemsToTheLeft, cx| {
+ pane.close_items_to_the_left(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseItemsToTheRight, cx| {
+ pane.close_items_to_the_right(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .on_action(cx.listener(|pane: &mut Self, action: &CloseAllItems, cx| {
+ pane.close_all_items(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
}))
- // cx.add_action(Pane::toggle_zoom);
- // cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
- // pane.activate_item(action.0, true, true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
- // pane.activate_item(pane.items.len() - 1, true, true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
- // pane.activate_prev_item(true, cx);
- // });
- // cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
- // pane.activate_next_item(true, cx);
- // });
- // cx.add_async_action(Pane::close_active_item);
- // cx.add_async_action(Pane::close_inactive_items);
- // cx.add_async_action(Pane::close_clean_items);
- // cx.add_async_action(Pane::close_items_to_the_left);
- // cx.add_async_action(Pane::close_items_to_the_right);
- // cx.add_async_action(Pane::close_all_items);
.size_full()
.on_action(
cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
@@ -1978,8 +2171,11 @@ impl Render for Pane {
.child(if let Some(item) = self.active_item() {
div().flex().flex_1().child(item.to_any())
} else {
- // todo!()
- div().child("Empty Pane")
+ h_stack()
+ .items_center()
+ .size_full()
+ .justify_center()
+ .child(Label::new("Open a file or project to get started.").color(Color::Muted))
})
// enum MouseNavigationHandler {}
@@ -1,18 +1,20 @@
use crate::{AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, bail, Result};
+use call::{ActiveCall, ParticipantLocation};
use collections::HashMap;
-use db2::sqlez::{
+use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{
- point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
+ point, size, AnyWeakView, Bounds, Div, Entity as _, IntoElement, Model, Pixels, Point, View,
+ ViewContext,
};
use parking_lot::Mutex;
-use project2::Project;
+use project::Project;
use serde::Deserialize;
use std::sync::Arc;
-use ui::prelude::*;
+use ui::{prelude::*, Button};
const HANDLE_HITBOX_SIZE: f32 = 4.0;
const HORIZONTAL_MIN_SIZE: f32 = 80.;
@@ -126,6 +128,7 @@ impl PaneGroup {
&self,
project: &Model<Project>,
follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
@@ -135,6 +138,7 @@ impl PaneGroup {
project,
0,
follower_states,
+ active_call,
active_pane,
zoomed,
app_state,
@@ -196,6 +200,7 @@ impl Member {
project: &Model<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
+ active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
@@ -203,19 +208,89 @@ impl Member {
) -> impl IntoElement {
match self {
Member::Pane(pane) => {
- // todo!()
- // let pane_element = if Some(pane.into()) == zoomed {
- // None
- // } else {
- // Some(pane)
- // };
-
- div().size_full().child(pane.clone()).into_any()
-
- // Stack::new()
- // .with_child(pane_element.contained().with_border(leader_border))
- // .with_children(leader_status_box)
- // .into_any()
+ let leader = follower_states.get(pane).and_then(|state| {
+ let room = active_call?.read(cx).room()?.read(cx);
+ room.remote_participant_for_peer_id(state.leader_id)
+ });
+
+ let mut leader_border = None;
+ let mut leader_status_box = None;
+ if let Some(leader) = &leader {
+ let mut leader_color = cx
+ .theme()
+ .players()
+ .color_for_participant(leader.participant_index.0)
+ .cursor;
+ leader_color.fade_out(0.3);
+ leader_border = Some(leader_color);
+
+ leader_status_box = match leader.location {
+ ParticipantLocation::SharedProject {
+ project_id: leader_project_id,
+ } => {
+ if Some(leader_project_id) == project.read(cx).remote_id() {
+ None
+ } else {
+ let leader_user = leader.user.clone();
+ let leader_user_id = leader.user.id;
+ Some(
+ Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "Follow {} to their active project",
+ leader_user.github_login,
+ ),
+ )
+ .on_click(cx.listener(
+ move |this, _, cx| {
+ crate::join_remote_project(
+ leader_project_id,
+ leader_user_id,
+ this.app_state().clone(),
+ cx,
+ )
+ .detach_and_log_err(cx);
+ },
+ )),
+ )
+ }
+ }
+ ParticipantLocation::UnsharedProject => Some(Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "{} is viewing an unshared Zed project",
+ leader.user.github_login
+ ),
+ )),
+ ParticipantLocation::External => Some(Button::new(
+ ("leader-status", pane.entity_id()),
+ format!(
+ "{} is viewing a window outside of Zed",
+ leader.user.github_login
+ ),
+ )),
+ };
+ }
+
+ div()
+ .relative()
+ .size_full()
+ .child(pane.clone())
+ .when_some(leader_border, |this, color| {
+ this.border_2().border_color(color)
+ })
+ .when_some(leader_status_box, |this, status_box| {
+ this.child(
+ div()
+ .absolute()
+ .w_96()
+ .bottom_3()
+ .right_3()
+ .z_index(1)
+ .child(status_box),
+ )
+ })
+ .into_any()
// let el = div()
// .flex()
@@ -5,7 +5,7 @@ pub mod model;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
-use db2::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
+use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::WindowBounds;
use util::{unzip_option, ResultExt};
@@ -552,7 +552,7 @@ impl WorkspaceDb {
#[cfg(test)]
mod tests {
use super::*;
- use db2::open_test_db;
+ use db::open_test_db;
use gpui;
#[gpui::test]
@@ -3,12 +3,12 @@ use crate::{
};
use anyhow::{Context, Result};
use async_recursion::async_recursion;
-use db2::sqlez::{
+use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Model, Task, View, WeakView, WindowBounds};
-use project2::Project;
+use project::Project;
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -4,7 +4,7 @@ use gpui::{
AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
WindowContext,
};
-use project2::search::SearchQuery;
+use project::search::SearchQuery;
use crate::{
item::{Item, WeakItemHandle},
@@ -1,5 +1,9 @@
-use crate::participant::{Frame, RemoteVideoTrack};
+use crate::{
+ item::{Item, ItemEvent},
+ ItemNavHistory, WorkspaceId,
+};
use anyhow::Result;
+use call::participant::{Frame, RemoteVideoTrack};
use client::{proto::PeerId, User};
use futures::StreamExt;
use gpui::{
@@ -9,7 +13,6 @@ use gpui::{
};
use std::sync::{Arc, Weak};
use ui::{h_stack, Icon, IconElement};
-use workspace::{item::Item, ItemNavHistory, WorkspaceId};
pub enum Event {
Close,
@@ -56,7 +59,7 @@ impl SharedScreen {
}
impl EventEmitter<Event> for SharedScreen {}
-impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
+impl EventEmitter<ItemEvent> for SharedScreen {}
impl FocusableView for SharedScreen {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
@@ -6,7 +6,7 @@ use gpui::{
WindowContext,
};
use ui::prelude::*;
-use ui::{h_stack, Button, Icon, IconButton};
+use ui::{h_stack, Icon, IconButton};
use util::ResultExt;
pub trait StatusItemView: Render {
@@ -52,49 +52,12 @@ impl Render for StatusBar {
h_stack()
.gap_4()
.child(
- h_stack()
- .gap_1()
- .child(
- // TODO: Line / column numbers
- div()
- .border()
- .border_color(gpui::red())
- .child(Button::new("status_line_column_numbers", "15:22")),
- )
- .child(
- // TODO: Language picker
- div()
- .border()
- .border_color(gpui::red())
- .child(Button::new("status_buffer_language", "Rust")),
- ),
- )
- .child(
- h_stack()
- .gap_1()
- .child(
- // Github tool
- div()
- .border()
- .border_color(gpui::red())
- .child(IconButton::new("status-copilot", Icon::Copilot)),
- )
- .child(
- // Feedback Tool
- div()
- .border()
- .border_color(gpui::red())
- .child(IconButton::new("status-feedback", Icon::Envelope)),
- ),
- )
- .child(
- // Bottom Dock
h_stack().gap_1().child(
- // Terminal
+ // Feedback Tool
div()
.border()
.border_color(gpui::red())
- .child(IconButton::new("status-terminal", Icon::Terminal)),
+ .child(IconButton::new("status-feedback", Icon::Envelope)),
),
)
.child(
@@ -133,7 +96,7 @@ impl StatusBar {
h_stack()
.items_center()
.gap_2()
- .children(self.right_items.iter().map(|item| item.to_any()))
+ .children(self.right_items.iter().rev().map(|item| item.to_any()))
}
}
@@ -4,7 +4,7 @@ use gpui::{
ViewContext, WindowContext,
};
use ui::prelude::*;
-use ui::{h_stack, v_stack, ButtonLike, Color, Icon, IconButton, Label};
+use ui::{h_stack, v_stack, Icon, IconButton};
pub enum ToolbarItemEvent {
ChangeLocation(ToolbarItemLocation),
@@ -80,7 +80,6 @@ impl Render for Toolbar {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- //dbg!(&self.items.len());
v_stack()
.border_b()
.border_color(cx.theme().colors().border_variant)
@@ -88,17 +87,8 @@ impl Render for Toolbar {
.child(
h_stack()
.justify_between()
- .child(
- // Toolbar left side
- h_stack().border().border_color(gpui::red()).p_1().child(
- ButtonLike::new("breadcrumb")
- .child(Label::new("crates/workspace2/src/toolbar.rs"))
- .child(Label::new("›").color(Color::Muted))
- .child(Label::new("impl Render for Toolbar"))
- .child(Label::new("›").color(Color::Muted))
- .child(Label::new("fn render")),
- ),
- )
+ // Toolbar left side
+ .children(self.items.iter().map(|(child, _)| child.to_any()))
// Toolbar right side
.child(
h_stack()
@@ -117,7 +107,6 @@ impl Render for Toolbar {
),
),
)
- .children(self.items.iter().map(|(child, _)| child.to_any()))
}
}
@@ -10,15 +10,16 @@ mod persistence;
pub mod searchable;
// todo!()
mod modal_layer;
+pub mod shared_screen;
mod status_bar;
mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
-use async_trait::async_trait;
-use client2::{
+use call::ActiveCall;
+use client::{
proto::{self, PeerId},
- Client, TypedEnvelope, User, UserStore,
+ Client, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -28,16 +29,16 @@ use futures::{
Future, FutureExt, StreamExt,
};
use gpui::{
- actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
- AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
- FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
- ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
- View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
+ actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AnyWindowHandle, AppContext,
+ AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
+ FocusHandle, FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model,
+ ModelContext, ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+ Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
-use language2::{LanguageRegistry, Rope};
+use language::{LanguageRegistry, Rope};
use lazy_static::lazy_static;
pub use modal_layer::*;
use node_runtime::NodeRuntime;
@@ -49,9 +50,10 @@ pub use persistence::{
WorkspaceDb, DB,
};
use postage::stream::Stream;
-use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use serde::Deserialize;
-use settings2::Settings;
+use settings::Settings;
+use shared_screen::SharedScreen;
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use std::{
@@ -62,7 +64,7 @@ use std::{
sync::{atomic::AtomicUsize, Arc},
time::Duration,
};
-use theme2::{ActiveTheme, ThemeSettings};
+use theme::{ActiveTheme, ThemeSettings};
pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{h_stack, v_stack, StyledExt};
@@ -210,6 +212,7 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
notifications::init(cx);
+
// cx.add_global_action({
// let app_state = Arc::downgrade(&app_state);
// move |_: &Open, cx: &mut AppContext| {
@@ -248,7 +251,7 @@ type FollowableItemBuilder = fn(
View<Workspace>,
ViewId,
&mut Option<proto::view::Variant>,
- &mut AppContext,
+ &mut WindowContext,
) -> Option<Task<Result<Box<dyn FollowableItemHandle>>>>;
type FollowableItemBuilders = HashMap<
TypeId,
@@ -302,8 +305,7 @@ pub struct AppState {
pub client: Arc<Client>,
pub user_store: Model<UserStore>,
pub workspace_store: Model<WorkspaceStore>,
- pub fs: Arc<dyn fs2::Fs>,
- pub call_factory: CallFactory,
+ pub fs: Arc<dyn fs::Fs>,
pub build_window_options:
fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
pub node_runtime: Arc<dyn NodeRuntime>,
@@ -313,7 +315,7 @@ pub struct WorkspaceStore {
workspaces: HashSet<WindowHandle<Workspace>>,
followers: Vec<Follower>,
client: Arc<Client>,
- _subscriptions: Vec<client2::Subscription>,
+ _subscriptions: Vec<client::Subscription>,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
@@ -322,89 +324,26 @@ struct Follower {
peer_id: PeerId,
}
-#[cfg(any(test, feature = "test-support"))]
-pub struct TestCallHandler;
-
-#[cfg(any(test, feature = "test-support"))]
-impl CallHandler for TestCallHandler {
- fn peer_state(
- &mut self,
- id: PeerId,
- project: &Model<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(bool, bool)> {
- None
- }
-
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>> {
- None
- }
-
- fn room_id(&self, cx: &AppContext) -> Option<u64> {
- None
- }
-
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
- Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
- }
-
- fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
- None
- }
-
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>> {
- unimplemented!()
- }
-
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
- None
- }
-
- fn is_muted(&self, cx: &AppContext) -> Option<bool> {
- None
- }
-
- fn toggle_mute(&self, cx: &mut AppContext) {}
-
- fn toggle_screen_share(&self, cx: &mut AppContext) {}
-
- fn toggle_deafen(&self, cx: &mut AppContext) {}
-
- fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
- None
- }
-}
-
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
use node_runtime::FakeNodeRuntime;
- use settings2::SettingsStore;
+ use settings::SettingsStore;
if !cx.has_global::<SettingsStore>() {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
}
- let fs = fs2::FakeFs::new(cx.background_executor().clone());
+ let fs = fs::FakeFs::new(cx.background_executor().clone());
let languages = Arc::new(LanguageRegistry::test());
let http_client = util::http::FakeHttpClient::with_404_response();
let client = Client::new(http_client.clone(), cx);
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
- theme2::init(theme2::LoadThemes::JustBase, cx);
- client2::init(&client, cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ client::init(&client, cx);
crate::init_settings(cx);
Arc::new(Self {
@@ -415,7 +354,6 @@ impl AppState {
workspace_store,
node_runtime: FakeNodeRuntime::new(),
build_window_options: |_, _, _| Default::default(),
- call_factory: |_| Box::new(TestCallHandler),
})
}
}
@@ -472,40 +410,6 @@ pub enum Event {
WorkspaceCreated(WeakView<Workspace>),
}
-#[async_trait(?Send)]
-pub trait CallHandler {
- fn peer_state(
- &mut self,
- id: PeerId,
- project: &Model<Project>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<(bool, bool)>;
- fn shared_screen_for_peer(
- &self,
- peer_id: PeerId,
- pane: &View<Pane>,
- cx: &mut ViewContext<Workspace>,
- ) -> Option<Box<dyn ItemHandle>>;
- fn room_id(&self, cx: &AppContext) -> Option<u64>;
- fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
- self.room_id(cx).is_some()
- }
- fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
- fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
- fn invite(
- &mut self,
- called_user_id: u64,
- initial_project: Option<Model<Project>>,
- cx: &mut AppContext,
- ) -> Task<Result<()>>;
- fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
- fn is_muted(&self, cx: &AppContext) -> Option<bool>;
- fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
- fn toggle_mute(&self, cx: &mut AppContext);
- fn toggle_deafen(&self, cx: &mut AppContext);
- fn toggle_screen_share(&self, cx: &mut AppContext);
-}
-
pub struct Workspace {
window_self: WindowHandle<Self>,
weak_self: WeakView<Self>,
@@ -526,10 +430,10 @@ pub struct Workspace {
titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
- call_handler: Box<dyn CallHandler>,
follower_states: HashMap<View<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
window_edited: bool,
+ active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
@@ -557,7 +461,6 @@ struct FollowerState {
enum WorkspaceBounds {}
-type CallFactory = fn(&mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
impl Workspace {
pub fn new(
workspace_id: WorkspaceId,
@@ -568,29 +471,29 @@ impl Workspace {
cx.observe(&project, |_, _, cx| cx.notify()).detach();
cx.subscribe(&project, move |this, _, event, cx| {
match event {
- project2::Event::RemoteIdChanged(_) => {
+ project::Event::RemoteIdChanged(_) => {
this.update_window_title(cx);
}
- project2::Event::CollaboratorLeft(peer_id) => {
+ project::Event::CollaboratorLeft(peer_id) => {
this.collaborator_left(*peer_id, cx);
}
- project2::Event::WorktreeRemoved(_) | project2::Event::WorktreeAdded => {
+ project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
this.update_window_title(cx);
this.serialize_workspace(cx);
}
- project2::Event::DisconnectedFromHost => {
+ project::Event::DisconnectedFromHost => {
this.update_window_edited(cx);
cx.blur();
}
- project2::Event::Closed => {
+ project::Event::Closed => {
cx.remove_window();
}
- project2::Event::DeletedEntry(entry_id) => {
+ project::Event::DeletedEntry(entry_id) => {
for pane in this.panes.iter() {
pane.update(cx, |pane, cx| {
pane.handle_deleted_project_item(*entry_id, cx)
@@ -598,7 +501,7 @@ impl Workspace {
}
}
- project2::Event::Notification(message) => this.show_notification(0, cx, |cx| {
+ project::Event::Notification(message) => this.show_notification(0, cx, |cx| {
cx.build_view(|_| MessageNotification::new(message.clone()))
}),
@@ -649,19 +552,9 @@ impl Workspace {
mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
- let mut cx2 = cx.clone();
- let t = this.clone();
-
- Workspace::process_leader_update(&this, leader_id, update, &mut cx)
+ Self::process_leader_update(&this, leader_id, update, &mut cx)
.await
.log_err();
-
- // this.update(&mut cx, |this, cxx| {
- // this.call_handler
- // .process_leader_update(leader_id, update, cx2)
- // })?
- // .await
- // .log_err();
}
Ok(())
@@ -694,6 +587,14 @@ impl Workspace {
// drag_and_drop.register_container(weak_handle.clone());
// });
+ let mut active_call = None;
+ if cx.has_global::<Model<ActiveCall>>() {
+ let call = cx.global::<Model<ActiveCall>>().clone();
+ let mut subscriptions = Vec::new();
+ subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
+ active_call = Some((call, subscriptions));
+ }
+
let subscriptions = vec![
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |_, cx| {
@@ -770,8 +671,7 @@ impl Workspace {
follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
-
- call_handler: (app_state.call_factory)(cx),
+ active_call,
database_id: workspace_id,
app_state,
_observe_current_user,
@@ -1218,7 +1118,7 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
//todo!(saveing)
-
+ let active_call = self.active_call().cloned();
let window = cx.window_handle();
cx.spawn(|this, mut cx| async move {
@@ -1229,27 +1129,27 @@ impl Workspace {
.count()
})?;
- if !quitting
- && workspace_count == 1
- && this
- .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx))
- .log_err()
- .unwrap_or_default()
- {
- let answer = window.update(&mut cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Do you want to leave the current call?",
- &["Close window and hang up", "Cancel"],
- )
- })?;
+ if let Some(active_call) = active_call {
+ if !quitting
+ && workspace_count == 1
+ && active_call.read_with(&cx, |call, _| call.room().is_some())?
+ {
+ let answer = window.update(&mut cx, |_, cx| {
+ cx.prompt(
+ PromptLevel::Warning,
+ "Do you want to leave the current call?",
+ &["Close window and hang up", "Cancel"],
+ )
+ })?;
- if answer.await.log_err() == Some(1) {
- return anyhow::Ok(false);
- } else {
- this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))?
- .await
- .log_err();
+ if answer.await.log_err() == Some(1) {
+ return anyhow::Ok(false);
+ } else {
+ active_call
+ .update(&mut cx, |call, cx| call.hang_up(cx))?
+ .await
+ .log_err();
+ }
}
}
@@ -1451,7 +1351,7 @@ impl Workspace {
.map(|entry| entry.id);
if let Some(entry_id) = entry_id {
workspace.project.update(cx, |_, cx| {
- cx.emit(project2::Event::ActiveEntryChanged(Some(entry_id)));
+ cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
})
}
})
@@ -1813,8 +1713,7 @@ impl Workspace {
});
cx.subscribe(&pane, Self::handle_pane_event).detach();
self.panes.push(pane.clone());
- // todo!()
- // cx.focus(&pane);
+ cx.focus_view(&pane);
cx.emit(Event::PaneAdded(pane.clone()));
pane
}
@@ -1955,13 +1854,13 @@ impl Workspace {
})
}
- pub(crate) fn load_path(
+ fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -1989,7 +1888,7 @@ impl Workspace {
where
T: ProjectItem,
{
- use project2::Item as _;
+ use project::Item as _;
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
@@ -2014,7 +1913,7 @@ impl Workspace {
where
T: ProjectItem,
{
- use project2::Item as _;
+ use project::Item as _;
let entry_id = project_item.read(cx).entry_id(cx);
if let Some(item) = entry_id
@@ -2034,7 +1933,7 @@ impl Workspace {
pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
self.active_pane.update(cx, |pane, cx| {
- pane.add_item(shared_screen, false, true, None, cx)
+ pane.add_item(Box::new(shared_screen), false, true, None, cx)
});
}
}
@@ -2372,60 +2271,60 @@ impl Workspace {
cx.notify();
}
- // fn start_following(
- // &mut self,
- // leader_id: PeerId,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let pane = self.active_pane().clone();
-
- // self.last_leaders_by_pane
- // .insert(pane.downgrade(), leader_id);
- // self.unfollow(&pane, cx);
- // self.follower_states.insert(
- // pane.clone(),
- // FollowerState {
- // leader_id,
- // active_view_id: None,
- // items_by_leader_view_id: Default::default(),
- // },
- // );
- // cx.notify();
-
- // let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
- // let project_id = self.project.read(cx).remote_id();
- // let request = self.app_state.client.request(proto::Follow {
- // room_id,
- // project_id,
- // leader_id: Some(leader_id),
- // });
+ fn start_following(
+ &mut self,
+ leader_id: PeerId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let pane = self.active_pane().clone();
+
+ self.last_leaders_by_pane
+ .insert(pane.downgrade(), leader_id);
+ self.unfollow(&pane, cx);
+ self.follower_states.insert(
+ pane.clone(),
+ FollowerState {
+ leader_id,
+ active_view_id: None,
+ items_by_leader_view_id: Default::default(),
+ },
+ );
+ cx.notify();
- // Some(cx.spawn(|this, mut cx| async move {
- // let response = request.await?;
- // this.update(&mut cx, |this, _| {
- // let state = this
- // .follower_states
- // .get_mut(&pane)
- // .ok_or_else(|| anyhow!("following interrupted"))?;
- // state.active_view_id = if let Some(active_view_id) = response.active_view_id {
- // Some(ViewId::from_proto(active_view_id)?)
- // } else {
- // None
- // };
- // Ok::<_, anyhow::Error>(())
- // })??;
- // Self::add_views_from_leader(
- // this.clone(),
- // leader_id,
- // vec![pane],
- // response.views,
- // &mut cx,
- // )
- // .await?;
- // this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
- // Ok(())
- // }))
- // }
+ let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+ let project_id = self.project.read(cx).remote_id();
+ let request = self.app_state.client.request(proto::Follow {
+ room_id,
+ project_id,
+ leader_id: Some(leader_id),
+ });
+
+ Some(cx.spawn(|this, mut cx| async move {
+ let response = request.await?;
+ this.update(&mut cx, |this, _| {
+ let state = this
+ .follower_states
+ .get_mut(&pane)
+ .ok_or_else(|| anyhow!("following interrupted"))?;
+ state.active_view_id = if let Some(active_view_id) = response.active_view_id {
+ Some(ViewId::from_proto(active_view_id)?)
+ } else {
+ None
+ };
+ Ok::<_, anyhow::Error>(())
+ })??;
+ Self::add_views_from_leader(
+ this.clone(),
+ leader_id,
+ vec![pane],
+ response.views,
+ &mut cx,
+ )
+ .await?;
+ this.update(&mut cx, |this, cx| this.leader_updated(leader_id, cx))?;
+ Ok(())
+ }))
+ }
// pub fn follow_next_collaborator(
// &mut self,
@@ -2464,67 +2363,67 @@ impl Workspace {
// self.follow(leader_id, cx)
// }
- // pub fn follow(
- // &mut self,
- // leader_id: PeerId,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<Task<Result<()>>> {
- // let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
- // let project = self.project.read(cx);
+ pub fn follow(
+ &mut self,
+ leader_id: PeerId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let room = ActiveCall::global(cx).read(cx).room()?.read(cx);
+ let project = self.project.read(cx);
- // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
- // return None;
- // };
+ let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else {
+ return None;
+ };
- // let other_project_id = match remote_participant.location {
- // call::ParticipantLocation::External => None,
- // call::ParticipantLocation::UnsharedProject => None,
- // call::ParticipantLocation::SharedProject { project_id } => {
- // if Some(project_id) == project.remote_id() {
- // None
- // } else {
- // Some(project_id)
- // }
- // }
- // };
+ let other_project_id = match remote_participant.location {
+ call::ParticipantLocation::External => None,
+ call::ParticipantLocation::UnsharedProject => None,
+ call::ParticipantLocation::SharedProject { project_id } => {
+ if Some(project_id) == project.remote_id() {
+ None
+ } else {
+ Some(project_id)
+ }
+ }
+ };
- // // if they are active in another project, follow there.
- // if let Some(project_id) = other_project_id {
- // let app_state = self.app_state.clone();
- // return Some(crate::join_remote_project(
- // project_id,
- // remote_participant.user.id,
- // app_state,
- // cx,
- // ));
- // }
+ // if they are active in another project, follow there.
+ if let Some(project_id) = other_project_id {
+ let app_state = self.app_state.clone();
+ return Some(crate::join_remote_project(
+ project_id,
+ remote_participant.user.id,
+ app_state,
+ cx,
+ ));
+ }
- // // if you're already following, find the right pane and focus it.
- // for (pane, state) in &self.follower_states {
- // if leader_id == state.leader_id {
- // cx.focus(pane);
- // return None;
- // }
- // }
+ // if you're already following, find the right pane and focus it.
+ for (pane, state) in &self.follower_states {
+ if leader_id == state.leader_id {
+ cx.focus_view(pane);
+ return None;
+ }
+ }
- // // Otherwise, follow.
- // self.start_following(leader_id, cx)
- // }
+ // Otherwise, follow.
+ self.start_following(leader_id, cx)
+ }
pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
- let follower_states = &mut self.follower_states;
- let state = follower_states.remove(pane)?;
+ let state = self.follower_states.remove(pane)?;
let leader_id = state.leader_id;
for (_, item) in state.items_by_leader_view_id {
item.set_leader_peer_id(None, cx);
}
- if follower_states
+ if self
+ .follower_states
.values()
.all(|state| state.leader_id != state.leader_id)
{
let project_id = self.project.read(cx).remote_id();
- let room_id = self.call_handler.room_id(cx)?;
+ let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
self.app_state
.client
.send(proto::Unfollow {
@@ -2593,8 +2492,7 @@ impl Workspace {
title.push_str(" ↗");
}
- // todo!()
- // cx.set_window_title(&title);
+ cx.set_window_title(&title);
}
fn update_window_edited(&mut self, cx: &mut ViewContext<Self>) {
@@ -2660,57 +2558,55 @@ impl Workspace {
}
}
- // // RPC handlers
+ // RPC handlers
fn handle_follow(
&mut self,
- _follower_project_id: Option<u64>,
- _cx: &mut ViewContext<Self>,
+ follower_project_id: Option<u64>,
+ cx: &mut ViewContext<Self>,
) -> proto::FollowResponse {
- todo!()
+ let client = &self.app_state.client;
+ let project_id = self.project.read(cx).remote_id();
- // let client = &self.app_state.client;
- // let project_id = self.project.read(cx).remote_id();
+ let active_view_id = self.active_item(cx).and_then(|i| {
+ Some(
+ i.to_followable_item_handle(cx)?
+ .remote_id(client, cx)?
+ .to_proto(),
+ )
+ });
- // let active_view_id = self.active_item(cx).and_then(|i| {
- // Some(
- // i.to_followable_item_handle(cx)?
- // .remote_id(client, cx)?
- // .to_proto(),
- // )
- // });
+ cx.notify();
- // cx.notify();
-
- // self.last_active_view_id = active_view_id.clone();
- // proto::FollowResponse {
- // active_view_id,
- // views: self
- // .panes()
- // .iter()
- // .flat_map(|pane| {
- // let leader_id = self.leader_for_pane(pane);
- // pane.read(cx).items().filter_map({
- // let cx = &cx;
- // move |item| {
- // let item = item.to_followable_item_handle(cx)?;
- // if (project_id.is_none() || project_id != follower_project_id)
- // && item.is_project_item(cx)
- // {
- // return None;
- // }
- // let id = item.remote_id(client, cx)?.to_proto();
- // let variant = item.to_state_proto(cx)?;
- // Some(proto::View {
- // id: Some(id),
- // leader_id,
- // variant: Some(variant),
- // })
- // }
- // })
- // })
- // .collect(),
- // }
+ self.last_active_view_id = active_view_id.clone();
+ proto::FollowResponse {
+ active_view_id,
+ views: self
+ .panes()
+ .iter()
+ .flat_map(|pane| {
+ let leader_id = self.leader_for_pane(pane);
+ pane.read(cx).items().filter_map({
+ let cx = &cx;
+ move |item| {
+ let item = item.to_followable_item_handle(cx)?;
+ if (project_id.is_none() || project_id != follower_project_id)
+ && item.is_project_item(cx)
+ {
+ return None;
+ }
+ let id = item.remote_id(client, cx)?.to_proto();
+ let variant = item.to_state_proto(cx)?;
+ Some(proto::View {
+ id: Some(id),
+ leader_id,
+ variant: Some(variant),
+ })
+ }
+ })
+ })
+ .collect(),
+ }
}
fn handle_update_followers(
@@ -2730,6 +2626,8 @@ impl Workspace {
update: proto::UpdateFollowers,
cx: &mut AsyncWindowContext,
) -> Result<()> {
+ dbg!("process_leader_update", &update);
+
match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
this.update(cx, |this, _| {
@@ -2881,9 +2779,8 @@ impl Workspace {
} else {
None
};
- let room_id = self.call_handler.room_id(cx)?;
self.app_state().workspace_store.update(cx, |store, cx| {
- store.update_followers(project_id, room_id, update, cx)
+ store.update_followers(project_id, update, cx)
})
}
@@ -2891,12 +2788,31 @@ impl Workspace {
self.follower_states.get(pane).map(|state| state.leader_id)
}
- pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+ fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
cx.notify();
- let (leader_in_this_project, leader_in_this_app) =
- self.call_handler.peer_state(leader_id, &self.project, cx)?;
+ let call = self.active_call()?;
+ let room = call.read(cx).room()?.read(cx);
+ let participant = room.remote_participant_for_peer_id(leader_id)?;
let mut items_to_activate = Vec::new();
+
+ let leader_in_this_app;
+ let leader_in_this_project;
+ match participant.location {
+ call::ParticipantLocation::SharedProject { project_id } => {
+ leader_in_this_app = true;
+ leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
+ }
+ call::ParticipantLocation::UnsharedProject => {
+ leader_in_this_app = true;
+ leader_in_this_project = false;
+ }
+ call::ParticipantLocation::External => {
+ leader_in_this_app = false;
+ leader_in_this_project = false;
+ }
+ };
+
for (pane, state) in &self.follower_states {
if state.leader_id != leader_id {
continue;
@@ -2917,7 +2833,7 @@ impl Workspace {
}
if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
- items_to_activate.push((pane.clone(), shared_screen));
+ items_to_activate.push((pane.clone(), Box::new(shared_screen)));
}
}
@@ -2926,8 +2842,8 @@ impl Workspace {
if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
} else {
- pane.update(cx, |pane, mut cx| {
- pane.add_item(item.boxed_clone(), false, false, None, &mut cx)
+ pane.update(cx, |pane, cx| {
+ pane.add_item(item.boxed_clone(), false, false, None, cx)
});
}
@@ -2944,21 +2860,20 @@ impl Workspace {
peer_id: PeerId,
pane: &View<Pane>,
cx: &mut ViewContext<Self>,
- ) -> Option<Box<dyn ItemHandle>> {
- self.call_handler.shared_screen_for_peer(peer_id, pane, cx)
- // let call = self.active_call()?;
- // let room = call.read(cx).room()?.read(cx);
- // let participant = room.remote_participant_for_peer_id(peer_id)?;
- // let track = participant.video_tracks.values().next()?.clone();
- // let user = participant.user.clone();
-
- // for item in pane.read(cx).items_of_type::<SharedScreen>() {
- // if item.read(cx).peer_id == peer_id {
- // return Some(item);
- // }
- // }
+ ) -> Option<View<SharedScreen>> {
+ let call = self.active_call()?;
+ let room = call.read(cx).room()?.read(cx);
+ let participant = room.remote_participant_for_peer_id(peer_id)?;
+ let track = participant.video_tracks.values().next()?.clone();
+ let user = participant.user.clone();
+
+ for item in pane.read(cx).items_of_type::<SharedScreen>() {
+ if item.read(cx).peer_id == peer_id {
+ return Some(item);
+ }
+ }
- // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+ Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
}
pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
@@ -2987,6 +2902,25 @@ impl Workspace {
}
}
+ fn active_call(&self) -> Option<&Model<ActiveCall>> {
+ self.active_call.as_ref().map(|(call, _)| call)
+ }
+
+ fn on_active_call_event(
+ &mut self,
+ _: Model<ActiveCall>,
+ event: &call::room::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ call::room::Event::ParticipantLocationChanged { participant_id }
+ | call::room::Event::RemoteVideoTracksChanged { participant_id } => {
+ self.leader_updated(*participant_id, cx);
+ }
+ _ => {}
+ }
+ }
+
pub fn database_id(&self) -> WorkspaceId {
self.database_id
}
@@ -3396,7 +3330,6 @@ impl Workspace {
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
node_runtime: FakeNodeRuntime::new(),
- call_factory: |_| Box::new(TestCallHandler),
});
let workspace = Self::new(0, project, app_state, cx);
workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3475,10 +3408,6 @@ impl Workspace {
self.modal_layer
.update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
}
-
- pub fn call_state(&mut self) -> &mut dyn CallHandler {
- &mut *self.call_handler
- }
}
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3588,93 +3517,12 @@ fn open_items(
})
}
-// todo!()
-// fn notify_of_new_dock(workspace: &WeakView<Workspace>, cx: &mut AsyncAppContext) {
-// const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
-// const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
-// const MESSAGE_ID: usize = 2;
-
-// if workspace
-// .read_with(cx, |workspace, cx| {
-// workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-// })
-// .unwrap_or(false)
-// {
-// return;
-// }
-
-// if db::kvp::KEY_VALUE_STORE
-// .read_kvp(NEW_DOCK_HINT_KEY)
-// .ok()
-// .flatten()
-// .is_some()
-// {
-// if !workspace
-// .read_with(cx, |workspace, cx| {
-// workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
-// })
-// .unwrap_or(false)
-// {
-// cx.update(|cx| {
-// cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
-// let entry = tracker
-// .entry(TypeId::of::<MessageNotification>())
-// .or_default();
-// if !entry.contains(&MESSAGE_ID) {
-// entry.push(MESSAGE_ID);
-// }
-// });
-// });
-// }
-
-// return;
-// }
-
-// cx.spawn(|_| async move {
-// db::kvp::KEY_VALUE_STORE
-// .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
-// .await
-// .ok();
-// })
-// .detach();
-
-// workspace
-// .update(cx, |workspace, cx| {
-// workspace.show_notification_once(2, cx, |cx| {
-// cx.build_view(|_| {
-// MessageNotification::new_element(|text, _| {
-// Text::new(
-// "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
-// text,
-// )
-// .with_custom_runs(vec![26..32, 34..46], |_, bounds, cx| {
-// let code_span_background_color = settings::get::<ThemeSettings>(cx)
-// .theme
-// .editor
-// .document_highlight_read_background;
-
-// cx.scene().push_quad(gpui::Quad {
-// bounds,
-// background: Some(code_span_background_color),
-// border: Default::default(),
-// corner_radii: (2.0).into(),
-// })
-// })
-// .into_any()
-// })
-// .with_click_message("Read more about the new panel system")
-// .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
-// })
-// })
-// })
-// .ok();
-
fn notify_if_database_failed(workspace: WindowHandle<Workspace>, cx: &mut AsyncAppContext) {
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
workspace
.update(cx, |workspace, cx| {
- if (*db2::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
+ if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
workspace.show_notification_once(0, cx, |cx| {
cx.build_view(|_| {
MessageNotification::new("Failed to load the database file.")
@@ -1,6 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use settings2::Settings;
+use settings::Settings;
#[derive(Deserialize)]
pub struct WorkspaceSettings {
@@ -172,7 +172,7 @@ osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"]
[package.metadata.bundle-nightly]
-icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
identifier = "dev.zed.Zed-Nightly"
name = "Zed Nightly"
osx_minimum_system_version = "10.15.7"
@@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
}
pub fn ensure_only_instance() -> IsOnlyInstance {
- if *db::ZED_STATELESS {
+ if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
return IsOnlyInstance::Yes;
}
@@ -615,8 +615,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
})
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
}
@@ -625,8 +625,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, file_path), false, cx)
})
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
let editor = workspace
@@ -763,7 +763,7 @@ mod tests {
AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
};
use language::LanguageRegistry;
- use project::{Project, ProjectPath};
+ use project::{project_settings::ProjectSettings, Project, ProjectPath};
use serde_json::json;
use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
use std::{
@@ -1308,6 +1308,122 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+ });
+ });
+ });
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "ignored_dir\n",
+ ".git": {
+ "HEAD": "ref: refs/heads/main",
+ },
+ "regular_dir": {
+ "file": "regular file contents",
+ },
+ "ignored_dir": {
+ "ignored_subdir": {
+ "file": "ignored subfile contents",
+ },
+ "file": "ignored file contents",
+ },
+ "excluded_dir": {
+ "file": "excluded file contents",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+
+ let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+ let paths_to_open = [
+ Path::new("/root/excluded_dir/file").to_path_buf(),
+ Path::new("/root/.git/HEAD").to_path_buf(),
+ Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+ ];
+ let (opened_workspace, new_items) = cx
+ .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+ .await
+ .unwrap();
+
+ assert_eq!(
+ opened_workspace.id(),
+ workspace.id(),
+ "Excluded files in subfolders of a workspace root should be opened in the workspace"
+ );
+ let mut opened_paths = cx.read(|cx| {
+ assert_eq!(
+ new_items.len(),
+ paths_to_open.len(),
+ "Expect to get the same number of opened items as submitted paths to open"
+ );
+ new_items
+ .iter()
+ .zip(paths_to_open.iter())
+ .map(|(i, path)| {
+ match i {
+ Some(Ok(i)) => {
+ Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+ }
+ Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+ None => None,
+ }
+ .flatten()
+ })
+ .collect::<Vec<_>>()
+ });
+ opened_paths.sort();
+ assert_eq!(
+ opened_paths,
+ vec![
+ None,
+ Some(".git/HEAD".to_string()),
+ Some("excluded_dir/file".to_string()),
+ ],
+ "Excluded files should get opened, excluded dir should not get opened"
+ );
+
+ let entries = cx.read(|cx| workspace.file_project_paths(cx));
+ assert_eq!(
+ initial_entries, entries,
+ "Workspace entries should not change after opening excluded files and directories paths"
+ );
+
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ let mut opened_buffer_paths = pane
+ .items()
+ .map(|i| {
+ i.project_path(cx)
+ .expect("all excluded files that got open should have a path")
+ .path
+ .display()
+ .to_string()
+ })
+ .collect::<Vec<_>>();
+ opened_buffer_paths.sort();
+ assert_eq!(
+ opened_buffer_paths,
+ vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+ "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+ );
+ });
+ }
+
#[gpui::test]
async fn test_save_conflicting_item(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -19,7 +19,7 @@ ai = { package = "ai2", path = "../ai2"}
audio = { package = "audio2", path = "../audio2" }
activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
auto_update = { package = "auto_update2", path = "../auto_update2" }
-# breadcrumbs = { path = "../breadcrumbs" }
+breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
call = { package = "call2", path = "../call2" }
channel = { package = "channel2", path = "../channel2" }
cli = { path = "../cli" }
@@ -30,7 +30,7 @@ command_palette = { package="command_palette2", path = "../command_palette2" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
-# copilot_button = { path = "../copilot_button" }
+copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
@@ -44,13 +44,13 @@ gpui = { package = "gpui2", path = "../gpui2" }
install_cli = { package = "install_cli2", path = "../install_cli2" }
journal = { package = "journal2", path = "../journal2" }
language = { package = "language2", path = "../language2" }
-# language_selector = { path = "../language_selector" }
+language_selector = { package = "language_selector2", path = "../language_selector2" }
lsp = { package = "lsp2", path = "../lsp2" }
menu = { package = "menu2", path = "../menu2" }
# language_tools = { path = "../language_tools" }
node_runtime = { path = "../node_runtime" }
# assistant = { path = "../assistant" }
-# outline = { path = "../outline" }
+outline = { package = "outline2", path = "../outline2" }
# plugin_runtime = { path = "../plugin_runtime",optional = true }
project = { package = "project2", path = "../project2" }
project_panel = { package = "project_panel2", path = "../project_panel2" }
@@ -167,7 +167,7 @@ osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"]
[package.metadata.bundle-nightly]
-icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
+icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"]
identifier = "dev.zed.Zed-Dev"
name = "Zed Nightly"
osx_minimum_system_version = "10.15.7"
@@ -191,7 +191,6 @@ fn main() {
user_store: user_store.clone(),
fs,
build_window_options,
- call_factory: call::Call::new,
workspace_store,
node_runtime,
});
@@ -205,7 +204,7 @@ fn main() {
go_to_line::init(cx);
file_finder::init(cx);
- // outline::init(cx);
+ outline::init(cx);
// project_symbols::init(cx);
project_panel::init(Assets, cx);
channel::init(&client, user_store.clone(), cx);
@@ -216,7 +215,7 @@ fn main() {
terminal_view::init(cx);
// journal2::init(app_state.clone(), cx);
- // language_selector::init(cx);
+ language_selector::init(cx);
theme_selector::init(cx);
// activity_indicator::init(cx);
// language_tools::init(cx);
@@ -39,7 +39,7 @@ pub enum IsOnlyInstance {
}
pub fn ensure_only_instance() -> IsOnlyInstance {
- if *db::ZED_STATELESS {
+ if *db::ZED_STATELESS || *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
return IsOnlyInstance::Yes;
}
@@ -7,6 +7,7 @@ mod only_instance;
mod open_listener;
pub use assets::*;
+use breadcrumbs::Breadcrumbs;
use collections::VecDeque;
use editor::{Editor, MultiBuffer};
use gpui::{
@@ -95,11 +96,11 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
if let workspace::Event::PaneAdded(pane) = event {
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
- // todo!()
- // let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
- // toolbar.add_item(breadcrumbs, cx);
+ let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace));
+ toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
toolbar.add_item(buffer_search_bar.clone(), cx);
+ // todo!()
// let quick_action_bar = cx.add_view(|_| {
// QuickActionBar::new(buffer_search_bar, workspace)
// });
@@ -135,27 +136,31 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
- // let copilot =
- // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+ let copilot =
+ cx.build_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
let diagnostic_summary =
cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator =
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
- // let active_buffer_language =
- // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+ let active_buffer_language =
+ cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
let feedback_button = cx
.build_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
// let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+ // let feedback_button = cx.add_view(|_| {
+ // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
+ // });
+ let cursor_position = cx.build_view(|_| editor::items::CursorPosition::new());
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(diagnostic_summary, cx);
status_bar.add_left_item(activity_indicator, cx);
-
status_bar.add_right_item(feedback_button, cx);
// status_bar.add_right_item(copilot, cx);
- // status_bar.add_right_item(active_buffer_language, cx);
+ status_bar.add_right_item(copilot, cx);
+ status_bar.add_right_item(active_buffer_language, cx);
// status_bar.add_right_item(vim_mode_indicator, cx);
- // status_bar.add_right_item(cursor_position, cx);
+ status_bar.add_right_item(cursor_position, cx);
});
auto_update::notify_of_any_new_update(cx);
@@ -580,8 +585,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
})?
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
}
@@ -590,8 +595,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, file_path), false, cx)
})?
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
let editor = workspace
@@ -716,3 +721,1846 @@ fn open_bundled_file(
})
.detach_and_log_err(cx);
}
+
+// todo!()
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use assets::Assets;
+// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
+// use fs::{FakeFs, Fs};
+// use gpui::{
+// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
+// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
+// };
+// use language::LanguageRegistry;
+// use project::{project_settings::ProjectSettings, Project, ProjectPath};
+// use serde_json::json;
+// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
+// use std::{
+// collections::HashSet,
+// path::{Path, PathBuf},
+// };
+// use theme::{ThemeRegistry, ThemeSettings};
+// use workspace::{
+// item::{Item, ItemHandle},
+// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
+// };
+
+// #[gpui::test]
+// async fn test_open_paths_action(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "aa": null,
+// "ab": null,
+// },
+// "b": {
+// "ba": null,
+// "bb": null,
+// },
+// "c": {
+// "ca": null,
+// "cb": null,
+// },
+// "d": {
+// "da": null,
+// "db": null,
+// },
+// }),
+// )
+// .await;
+
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
+// &app_state,
+// None,
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+// workspace_1.update(cx, |workspace, cx| {
+// assert_eq!(workspace.worktrees(cx).count(), 2);
+// assert!(workspace.left_dock().read(cx).is_open());
+// assert!(workspace.active_pane().is_focused(cx));
+// });
+
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+// &app_state,
+// None,
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 2);
+
+// // Replace existing windows
+// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+// &app_state,
+// Some(window),
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 2);
+// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+// workspace_1.update(cx, |workspace, cx| {
+// assert_eq!(
+// workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).abs_path())
+// .collect::<Vec<_>>(),
+// &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+// );
+// assert!(workspace.left_dock().read(cx).is_open());
+// assert!(workspace.active_pane().is_focused(cx));
+// });
+// }
+
+// #[gpui::test]
+// async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree("/root", json!({"a": "hey"}))
+// .await;
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+
+// // When opening the workspace, the window is not in a edited state.
+// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+// let workspace = window.root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert!(!window.is_edited(cx));
+
+// // Editing a buffer marks the window as edited.
+// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+// assert!(window.is_edited(cx));
+
+// // Undoing the edit restores the window's edited state.
+// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
+// assert!(!window.is_edited(cx));
+
+// // Redoing the edit marks the window as edited again.
+// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
+// assert!(window.is_edited(cx));
+
+// // Closing the item restores the window's edited state.
+// let close = pane.update(cx, |pane, cx| {
+// drop(editor);
+// pane.close_active_item(&Default::default(), cx).unwrap()
+// });
+// executor.run_until_parked();
+
+// window.simulate_prompt_answer(1, cx);
+// close.await.unwrap();
+// assert!(!window.is_edited(cx));
+
+// // Opening the buffer again doesn't impact the window's edited state.
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert!(!window.is_edited(cx));
+
+// // Editing the buffer marks the window as edited.
+// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+// assert!(window.is_edited(cx));
+
+// // Ensure closing the window via the mouse gets preempted due to the
+// // buffer having unsaved changes.
+// assert!(!window.simulate_close(cx));
+// executor.run_until_parked();
+// assert_eq!(cx.windows().len(), 1);
+
+// // The window is successfully closed after the user dismisses the prompt.
+// window.simulate_prompt_answer(1, cx);
+// executor.run_until_parked();
+// assert_eq!(cx.windows().len(), 0);
+// }
+
+// #[gpui::test]
+// async fn test_new_empty_workspace(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// cx.update(|cx| {
+// open_new(&app_state, cx, |workspace, cx| {
+// Editor::new_file(workspace, &Default::default(), cx)
+// })
+// })
+// .await;
+
+// let window = cx
+// .windows()
+// .first()
+// .unwrap()
+// .downcast::<Workspace>()
+// .unwrap();
+// let workspace = window.root(cx);
+
+// let editor = workspace.update(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<editor::Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(editor.text(cx).is_empty());
+// assert!(!editor.is_dirty(cx));
+// });
+
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name");
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_entry(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1",
+// "file2": "contents 2",
+// "file3": "contents 3",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+
+// // Open the first entry
+// let entry_1 = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file1.clone())
+// );
+// assert_eq!(pane.items_len(), 1);
+// });
+
+// // Open the second entry
+// workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file2.clone())
+// );
+// assert_eq!(pane.items_len(), 2);
+// });
+
+// // Open the first entry again. The existing pane item is activated.
+// let entry_1b = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+// assert_eq!(entry_1.id(), entry_1b.id());
+
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file1.clone())
+// );
+// assert_eq!(pane.items_len(), 2);
+// });
+
+// // Split the pane with the first entry, then open the second entry again.
+// workspace
+// .update(cx, |w, cx| {
+// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
+// w.open_path(file2.clone(), None, true, cx)
+// })
+// .await
+// .unwrap();
+
+// workspace.read_with(cx, |w, cx| {
+// assert_eq!(
+// w.active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .project_path(cx),
+// Some(file2.clone())
+// );
+// });
+
+// // Open the third entry twice concurrently. Only one pane item is added.
+// let (t1, t2) = workspace.update(cx, |w, cx| {
+// (
+// w.open_path(file3.clone(), None, true, cx),
+// w.open_path(file3.clone(), None, true, cx),
+// )
+// });
+// t1.await.unwrap();
+// t2.await.unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file3.clone())
+// );
+// let pane_entries = pane
+// .items()
+// .map(|i| i.project_path(cx).unwrap())
+// .collect::<Vec<_>>();
+// assert_eq!(pane_entries, &[file1, file2, file3]);
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_paths(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/",
+// json!({
+// "dir1": {
+// "a.txt": ""
+// },
+// "dir2": {
+// "b.txt": ""
+// },
+// "dir3": {
+// "c.txt": ""
+// },
+// "d.txt": ""
+// }),
+// )
+// .await;
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+// let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+
+// #[track_caller]
+// fn assert_project_panel_selection(
+// workspace: &Workspace,
+// expected_worktree_path: &Path,
+// expected_entry_path: &Path,
+// cx: &AppContext,
+// ) {
+// let project_panel = [
+// workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+// workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+// workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+// ]
+// .into_iter()
+// .find_map(std::convert::identity)
+// .expect("found no project panels")
+// .read(cx);
+// let (selected_worktree, selected_entry) = project_panel
+// .selected_entry(cx)
+// .expect("project panel should have a selected entry");
+// assert_eq!(
+// selected_worktree.abs_path().as_ref(),
+// expected_worktree_path,
+// "Unexpected project panel selected worktree path"
+// );
+// assert_eq!(
+// selected_entry.path.as_ref(),
+// expected_entry_path,
+// "Unexpected project panel selected entry path"
+// );
+// }
+
+// // Open a file within an existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "a.txt"
+// );
+// });
+
+// // Open a file outside of any existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "b.txt"
+// );
+// });
+
+// // Ensure opening a directory and one of its children only adds one worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "c.txt"
+// );
+// });
+
+// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/d.txt".into()], false, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+
+// let visible_worktree_roots = workspace
+// .visible_worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// visible_worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "d.txt"
+// );
+// });
+// }
+
+// #[gpui::test]
+// async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// cx.update(|cx| {
+// cx.update_global::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+// project_settings.file_scan_exclusions =
+// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+// });
+// });
+// });
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// ".gitignore": "ignored_dir\n",
+// ".git": {
+// "HEAD": "ref: refs/heads/main",
+// },
+// "regular_dir": {
+// "file": "regular file contents",
+// },
+// "ignored_dir": {
+// "ignored_subdir": {
+// "file": "ignored subfile contents",
+// },
+// "file": "ignored file contents",
+// },
+// "excluded_dir": {
+// "file": "excluded file contents",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let paths_to_open = [
+// Path::new("/root/excluded_dir/file").to_path_buf(),
+// Path::new("/root/.git/HEAD").to_path_buf(),
+// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+// ];
+// let (opened_workspace, new_items) = cx
+// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+// .await
+// .unwrap();
+
+// assert_eq!(
+// opened_workspace.id(),
+// workspace.id(),
+// "Excluded files in subfolders of a workspace root should be opened in the workspace"
+// );
+// let mut opened_paths = cx.read(|cx| {
+// assert_eq!(
+// new_items.len(),
+// paths_to_open.len(),
+// "Expect to get the same number of opened items as submitted paths to open"
+// );
+// new_items
+// .iter()
+// .zip(paths_to_open.iter())
+// .map(|(i, path)| {
+// match i {
+// Some(Ok(i)) => {
+// Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+// }
+// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+// None => None,
+// }
+// .flatten()
+// })
+// .collect::<Vec<_>>()
+// });
+// opened_paths.sort();
+// assert_eq!(
+// opened_paths,
+// vec![
+// None,
+// Some(".git/HEAD".to_string()),
+// Some("excluded_dir/file".to_string()),
+// ],
+// "Excluded files should get opened, excluded dir should not get opened"
+// );
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// assert_eq!(
+// initial_entries, entries,
+// "Workspace entries should not change after opening excluded files and directories paths"
+// );
+
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// let mut opened_buffer_paths = pane
+// .items()
+// .map(|i| {
+// i.project_path(cx)
+// .expect("all excluded files that got open should have a path")
+// .path
+// .display()
+// .to_string()
+// })
+// .collect::<Vec<_>>();
+// opened_buffer_paths.sort();
+// assert_eq!(
+// opened_buffer_paths,
+// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+// );
+// });
+// }
+
+// #[gpui::test]
+// async fn test_save_conflicting_item(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree("/root", json!({ "a.txt": "" }))
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// // Open a file within an existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
+// })
+// .await;
+// let editor = cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// let item = pane.active_item().unwrap();
+// item.downcast::<Editor>().unwrap()
+// });
+
+// editor.update(cx, |editor, cx| editor.handle_input("x", cx));
+// app_state
+// .fs
+// .as_fake()
+// .insert_file("/root/a.txt", "changed".to_string())
+// .await;
+// editor
+// .condition(cx, |editor, cx| editor.has_conflict(cx))
+// .await;
+// cx.read(|cx| assert!(editor.is_dirty(cx)));
+
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// window.simulate_prompt_answer(0, cx);
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert!(!editor.has_conflict(cx));
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// project.update(cx, |project, _| project.languages().add(rust_lang()));
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
+
+// // Create a new untitled buffer
+// cx.dispatch_action(window.into(), NewFile);
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "untitled");
+// assert!(Arc::ptr_eq(
+// &editor.language_at(0, cx).unwrap(),
+// &languages::PLAIN_TEXT
+// ));
+// editor.handle_input("hi", cx);
+// assert!(editor.is_dirty(cx));
+// });
+
+// // Save the buffer. This prompts for a filename.
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|parent_dir| {
+// assert_eq!(parent_dir, Path::new("/root"));
+// Some(parent_dir.join("the-new-name.rs"))
+// });
+// cx.read(|cx| {
+// assert!(editor.is_dirty(cx));
+// assert_eq!(editor.read(cx).title(cx), "untitled");
+// });
+
+// // When the save completes, the buffer's title is updated and the language is assigned based
+// // on the path.
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name.rs");
+// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
+// });
+
+// // Edit the file and save it again. This time, there is no filename prompt.
+// editor.update(cx, |editor, cx| {
+// editor.handle_input(" there", cx);
+// assert!(editor.is_dirty(cx));
+// });
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// save_task.await.unwrap();
+// assert!(!cx.did_prompt_for_new_path());
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name.rs")
+// });
+
+// // Open the same newly-created file in another pane item. The new editor should reuse
+// // the same buffer.
+// cx.dispatch_action(window.into(), NewFile);
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.split_and_clone(
+// workspace.active_pane().clone(),
+// SplitDirection::Right,
+// cx,
+// );
+// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
+// })
+// .await
+// .unwrap();
+// let editor2 = workspace.update(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// cx.read(|cx| {
+// assert_eq!(
+// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+// editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+// );
+// })
+// }
+
+// #[gpui::test]
+// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+// let project = Project::test(app_state.fs.clone(), [], cx).await;
+// project.update(cx, |project, _| project.languages().add(rust_lang()));
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// // Create a new untitled buffer
+// cx.dispatch_action(window.into(), NewFile);
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(Arc::ptr_eq(
+// &editor.language_at(0, cx).unwrap(),
+// &languages::PLAIN_TEXT
+// ));
+// editor.handle_input("hi", cx);
+// assert!(editor.is_dirty(cx));
+// });
+
+// // Save the buffer. This prompts for a filename.
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
+// save_task.await.unwrap();
+// // The buffer is not dirty anymore and the language is assigned based on the path.
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
+// });
+// }
+
+// #[gpui::test]
+// async fn test_pane_actions(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1",
+// "file2": "contents 2",
+// "file3": "contents 3",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+
+// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
+
+// workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+
+// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
+// let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+// assert_eq!(editor.project_path(cx), Some(file1.clone()));
+// let buffer = editor.update(cx, |editor, cx| {
+// editor.insert("dirt", cx);
+// editor.buffer().downgrade()
+// });
+// (editor.downgrade(), buffer)
+// });
+
+// cx.dispatch_action(window.into(), pane::SplitRight);
+// let editor_2 = cx.update(|cx| {
+// let pane_2 = workspace.read(cx).active_pane().clone();
+// assert_ne!(pane_1, pane_2);
+
+// let pane2_item = pane_2.read(cx).active_item().unwrap();
+// assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
+
+// pane2_item.downcast::<Editor>().unwrap().downgrade()
+// });
+// cx.dispatch_action(
+// window.into(),
+// workspace::CloseActiveItem { save_intent: None },
+// );
+
+// cx.foreground().run_until_parked();
+// workspace.read_with(cx, |workspace, _| {
+// assert_eq!(workspace.panes().len(), 1);
+// assert_eq!(workspace.active_pane(), &pane_1);
+// });
+
+// cx.dispatch_action(
+// window.into(),
+// workspace::CloseActiveItem { save_intent: None },
+// );
+// cx.foreground().run_until_parked();
+// window.simulate_prompt_answer(1, cx);
+// cx.foreground().run_until_parked();
+
+// workspace.read_with(cx, |workspace, cx| {
+// assert_eq!(workspace.panes().len(), 1);
+// assert!(workspace.active_item(cx).is_none());
+// });
+
+// cx.assert_dropped(editor_1);
+// cx.assert_dropped(editor_2);
+// cx.assert_dropped(buffer);
+// }
+
+// #[gpui::test]
+// async fn test_navigation(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1\n".repeat(20),
+// "file2": "contents 2\n".repeat(20),
+// "file3": "contents 3\n".repeat(20),
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let workspace = cx
+// .add_window(|cx| Workspace::test_new(project.clone(), cx))
+// .root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+
+// let editor1 = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
+// });
+// });
+// let editor2 = workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// let editor3 = workspace
+// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+
+// editor3
+// .update(cx, |editor, cx| {
+// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
+// });
+// editor.newline(&Default::default(), cx);
+// editor.newline(&Default::default(), cx);
+// editor.move_down(&Default::default(), cx);
+// editor.move_down(&Default::default(), cx);
+// editor.save(project.clone(), cx)
+// })
+// .await
+// .unwrap();
+// editor3.update(cx, |editor, cx| {
+// editor.set_scroll_position(vec2f(0., 12.5), cx)
+// });
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file2.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go back one more time and ensure we don't navigate past the first item in the history.
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file2.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+// // location.
+// pane.update(cx, |pane, cx| {
+// let editor3_id = editor3.id();
+// drop(editor3);
+// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+// pane.update(cx, |pane, cx| {
+// let editor2_id = editor2.id();
+// drop(editor2);
+// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// app_state
+// .fs
+// .remove_file(Path::new("/root/a/file2"), Default::default())
+// .await
+// .unwrap();
+// cx.foreground().run_until_parked();
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Modify file to collapse multiple nav history entries into the same location.
+// // Ensure we don't visit the same location twice when navigating.
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
+// })
+// });
+
+// for _ in 0..5 {
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+// });
+// });
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
+// })
+// });
+// }
+
+// editor1.update(cx, |editor, cx| {
+// editor.transact(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
+// });
+// editor.insert("", cx);
+// })
+// });
+
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+// })
+// });
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(2, 0), 0.)
+// );
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(3, 0), 0.)
+// );
+
+// fn active_location(
+// workspace: &ViewHandle<Workspace>,
+// cx: &mut TestAppContext,
+// ) -> (ProjectPath, DisplayPoint, f32) {
+// workspace.update(cx, |workspace, cx| {
+// let item = workspace.active_item(cx).unwrap();
+// let editor = item.downcast::<Editor>().unwrap();
+// let (selections, scroll_position) = editor.update(cx, |editor, cx| {
+// (
+// editor.selections.display_ranges(cx),
+// editor.scroll_position(cx),
+// )
+// });
+// (
+// item.project_path(cx).unwrap(),
+// selections[0].start,
+// scroll_position.y(),
+// )
+// })
+// }
+// }
+
+// #[gpui::test]
+// async fn test_reopening_closed_items(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "",
+// "file2": "",
+// "file3": "",
+// "file4": "",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let workspace = cx
+// .add_window(|cx| Workspace::test_new(project, cx))
+// .root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+// let file4 = entries[3].clone();
+
+// let file1_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file2_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file3_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file4_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// // Close all the pane items in some arbitrary order.
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), None);
+
+// // Reopen all the closed items, ensuring they are reopened in the same order
+// // in which they were closed.
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// // Reopening past the last closed item is a no-op.
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// // Reopening closed items doesn't interfere with navigation history.
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// fn active_path(
+// workspace: &ViewHandle<Workspace>,
+// cx: &TestAppContext,
+// ) -> Option<ProjectPath> {
+// workspace.read_with(cx, |workspace, cx| {
+// let item = workspace.active_item(cx)?;
+// item.project_path(cx)
+// })
+// }
+// }
+
+// #[gpui::test]
+// async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+// struct TestView;
+
+// impl Entity for TestView {
+// type Event = ();
+// }
+
+// impl View for TestView {
+// fn ui_name() -> &'static str {
+// "TestView"
+// }
+
+// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+// Empty::new().into_any()
+// }
+// }
+
+// let executor = cx.background();
+// let fs = FakeFs::new(executor.clone());
+
+// actions!(test, [A, B]);
+// // From the Atom keymap
+// actions!(workspace, [ActivatePreviousPane]);
+// // From the JetBrains keymap
+// actions!(pane, [ActivatePrevItem]);
+
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "Atom"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::A"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.update(|cx| {
+// cx.set_global(SettingsStore::test(cx));
+// theme::init(Assets, cx);
+// welcome::init(cx);
+
+// cx.add_global_action(|_: &A, _cx| {});
+// cx.add_global_action(|_: &B, _cx| {});
+// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+// let settings_rx = watch_config_file(
+// executor.clone(),
+// fs.clone(),
+// PathBuf::from("/settings.json"),
+// );
+// let keymap_rx =
+// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+// handle_keymap_file_changes(keymap_rx, cx);
+// handle_settings_file_changes(settings_rx, cx);
+// });
+
+// cx.foreground().run_until_parked();
+
+// let window = cx.add_window(|_| TestView);
+
+// // Test loading the keymap base at all
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the users keymap, while retaining the base keymap
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::B"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the base, while retaining the users keymap
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "JetBrains"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &B), ("[", &ActivatePrevItem)],
+// line!(),
+// );
+
+// #[track_caller]
+// fn assert_key_bindings_for<'a>(
+// window: AnyWindowHandle,
+// cx: &TestAppContext,
+// actions: Vec<(&'static str, &'a dyn Action)>,
+// line: u32,
+// ) {
+// for (key, action) in actions {
+// // assert that...
+// assert!(
+// cx.available_actions(window, 0)
+// .into_iter()
+// .any(|(_, bound_action, b)| {
+// // action names match...
+// bound_action.name() == action.name()
+// && bound_action.namespace() == action.namespace()
+// // and key strokes contain the given key
+// && b.iter()
+// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+// }),
+// "On {} Failed to find {} with key binding {}",
+// line,
+// action.name(),
+// key
+// );
+// }
+// }
+// }
+
+// #[gpui::test]
+// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+// struct TestView;
+
+// impl Entity for TestView {
+// type Event = ();
+// }
+
+// impl View for TestView {
+// fn ui_name() -> &'static str {
+// "TestView"
+// }
+
+// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+// Empty::new().into_any()
+// }
+// }
+
+// let executor = cx.background();
+// let fs = FakeFs::new(executor.clone());
+
+// actions!(test, [A, B]);
+// // From the Atom keymap
+// actions!(workspace, [ActivatePreviousPane]);
+// // From the JetBrains keymap
+// actions!(pane, [ActivatePrevItem]);
+
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "Atom"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::A"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.update(|cx| {
+// cx.set_global(SettingsStore::test(cx));
+// theme::init(Assets, cx);
+// welcome::init(cx);
+
+// cx.add_global_action(|_: &A, _cx| {});
+// cx.add_global_action(|_: &B, _cx| {});
+// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+// let settings_rx = watch_config_file(
+// executor.clone(),
+// fs.clone(),
+// PathBuf::from("/settings.json"),
+// );
+// let keymap_rx =
+// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+// handle_keymap_file_changes(keymap_rx, cx);
+// handle_settings_file_changes(settings_rx, cx);
+// });
+
+// cx.foreground().run_until_parked();
+
+// let window = cx.add_window(|_| TestView);
+
+// // Test loading the keymap base at all
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test disabling the key binding for the base keymap
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": null
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the base, while retaining the users keymap
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "JetBrains"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
+
+// #[track_caller]
+// fn assert_key_bindings_for<'a>(
+// window: AnyWindowHandle,
+// cx: &TestAppContext,
+// actions: Vec<(&'static str, &'a dyn Action)>,
+// line: u32,
+// ) {
+// for (key, action) in actions {
+// // assert that...
+// assert!(
+// cx.available_actions(window, 0)
+// .into_iter()
+// .any(|(_, bound_action, b)| {
+// // action names match...
+// bound_action.name() == action.name()
+// && bound_action.namespace() == action.namespace()
+// // and key strokes contain the given key
+// && b.iter()
+// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+// }),
+// "On {} Failed to find {} with key binding {}",
+// line,
+// action.name(),
+// key
+// );
+// }
+// }
+// }
+
+// #[gpui::test]
+// fn test_bundled_settings_and_themes(cx: &mut AppContext) {
+// cx.platform()
+// .fonts()
+// .add_fonts(&[
+// Assets
+// .load("fonts/zed-sans/zed-sans-extended.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// Assets
+// .load("fonts/zed-mono/zed-mono-extended.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// Assets
+// .load("fonts/plex/IBMPlexSans-Regular.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// ])
+// .unwrap();
+// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
+// let mut settings = SettingsStore::default();
+// settings
+// .set_default_settings(&settings::default_settings(), cx)
+// .unwrap();
+// cx.set_global(settings);
+// theme::init(Assets, cx);
+
+// let mut has_default_theme = false;
+// for theme_name in themes.list(false).map(|meta| meta.name) {
+// let theme = themes.get(&theme_name).unwrap();
+// assert_eq!(theme.meta.name, theme_name);
+// if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
+// has_default_theme = true;
+// }
+// }
+// assert!(has_default_theme);
+// }
+
+// #[gpui::test]
+// fn test_bundled_languages(cx: &mut AppContext) {
+// cx.set_global(SettingsStore::test(cx));
+// let mut languages = LanguageRegistry::test();
+// languages.set_executor(cx.background().clone());
+// let languages = Arc::new(languages);
+// let node_runtime = node_runtime::FakeNodeRuntime::new();
+// languages::init(languages.clone(), node_runtime, cx);
+// for name in languages.language_names() {
+// languages.language_for_name(&name);
+// }
+// cx.foreground().run_until_parked();
+// }
+
+// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+// cx.foreground().forbid_parking();
+// cx.update(|cx| {
+// let mut app_state = AppState::test(cx);
+// let state = Arc::get_mut(&mut app_state).unwrap();
+// state.initialize_workspace = initialize_workspace;
+// state.build_window_options = build_window_options;
+// theme::init((), cx);
+// audio::init((), cx);
+// channel::init(&app_state.client, app_state.user_store.clone(), cx);
+// call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+// workspace::init(app_state.clone(), cx);
+// Project::init_settings(cx);
+// language::init(cx);
+// editor::init(cx);
+// project_panel::init_settings(cx);
+// collab_ui::init(&app_state, cx);
+// pane::init(cx);
+// project_panel::init((), cx);
+// terminal_view::init(cx);
+// assistant::init(cx);
+// app_state
+// })
+// }
+
+// fn rust_lang() -> Arc<language::Language> {
+// Arc::new(language::Language::new(
+// language::LanguageConfig {
+// name: "Rust".into(),
+// path_suffixes: vec!["rs".to_string()],
+// ..Default::default()
+// },
+// Some(tree_sitter_rust::language()),
+// ))
+// }
+// }
@@ -1,5618 +0,0 @@
-#![feature(prelude_import)]
-#![allow(dead_code, unused_variables)]
-#[prelude_import]
-use std::prelude::rust_2021::*;
-#[macro_use]
-extern crate std;
-use color::black;
-use components::button;
-use element::Element;
-use frame::frame;
-use gpui::{
- geometry::{rect::RectF, vector::vec2f},
- platform::WindowOptions,aa
-};
-use log::LevelFilter;a
-use simplelog::SimpleLogger;
-use themes::{rose_pine, ThemeColors};
-use view::view;a
-mod adapter {
- use crate::element::AnyElement;
- use crate::element::{LayoutContext, PaintContext};
- use gpui::{geometry::rect::RectF, LayoutEngine};aaaa
- use util::ResultExt;
- pub struct Adapter<V>(pub(crate) AnyElement<V>);
- impl<V: 'static> gpui::Element<V> for Adapter<V> {aa
- type LayoutState = Option<LayaoutEngine>;
- type PaintState = ();
- fn layout(
- &mut self,
- constraint: gpui::SizeConstraint,
- view: &mut V,
- cx: &mut LayoutContext<V>,aa
- ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
- cx.push_layout_engine(LayoutEngine::new());
- let node = self.0.layout(view, cx).log_err();a
- if let Some(node) = node {
- let layout_engine = cx.layout_engine().unwrap();
- layout_engine.compute_layout(node, constraint.max).log_err();
- }
- let layout_engine = cx.pop_layout_engine();
- if true {a
- if !layout_engine.is_some() {
- ::core::panicking::panic("assertion failed: layout_engine.is_some()")
- }
- }
- (constraint.max, layout_engine)a
- }
- fn paint(a
- &mut self,
- scene: &mut gpui::SceneBuilder,
- bounds: RectF,
- visible_bounds: RectF,
- layout_engine: &mut Option<LayoutEngine>,
- view: &mut V,
- legacy_cx: &mut gpui::PaintContext<V>,aaa
- ) -> Self::PaintState {
- legacy_cx.push_layout_engine(layout_engine.take().unwrap());
- let mut cx = PaintContext::new(legacy_cx, scene);
- self.0.paint(view, &mut cx).log_err();
- *layout_engine = legacy_cx.pop_layout_engine();
- if true {
- if !layout_engine.is_some() {
- ::core::panicking::panic("assertion failed: layout_engine.is_some()")
- }
- }
- }
- fn rect_for_text_range(
- &self,
- range_utf16: std::ops::Range<usize>,
- bounds: RectF,
- visible_bounds: RectF,
- layout: &Self::LayoutState,
- paint: &Self::PaintState,
- view: &V,
- cx: &gpui::ViewContext<V>,
- ) -> Option<RectF> {
- ::core::panicking::panic("not yet implemented")
- }
- fn debug(
- &self,
- bounds: RectF,
- layout: &Self::LayoutState,
- paint: &Self::PaintState,
- view: &V,
- cx: &gpui::ViewContext<V>,
- ) -> gpui::serde_json::Value {
- ::core::panicking::panic("not yet implemented")
- }
- }
-}
-mod color {
- #![allow(dead_code)]
- use smallvec::SmallVec;
- use std::{num::ParseIntError, ops::Range};
- pub fn rgb<C: From<Rgba>>(hex: u32) -> C {
- let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
- let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
- let b = (hex & 0xFF) as f32 / 255.0;
- Rgba { r, g, b, a: 1.0 }.into()
- }
- pub struct Rgba {
- pub r: f32,
- pub g: f32,
- pub b: f32,
- pub a: f32,
- }
- #[automatically_derived]
- impl ::core::clone::Clone for Rgba {
- #[inline]
- fn clone(&self) -> Rgba {
- let _: ::core::clone::AssertParamIsClone<f32>;
- *self
- }
- }
- #[automatically_derived]
- impl ::core::marker::Copy for Rgba {}
- #[automatically_derived]
- impl ::core::default::Default for Rgba {
- #[inline]
- fn default() -> Rgba {
- Rgba {
- r: ::core::default::Default::default(),
- g: ::core::default::Default::default(),
- b: ::core::default::Default::default(),
- a: ::core::default::Default::default(),
- }
- }
- }
- #[automatically_derived]
- impl ::core::fmt::Debug for Rgba {
- fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
- ::core::fmt::Formatter::debug_struct_field4_finish(
- f, "Rgba", "r", &self.r, "g", &self.g, "b", &self.b, "a", &&self.a,
- )
- }
- }
- pub trait Lerp {
- fn lerp(&self, level: f32) -> Hsla;
- }
- impl Lerp for Range<Hsla> {
- fn lerp(&self, level: f32) -> Hsla {
- let level = level.clamp(0., 1.);
- Hsla {
- h: self.start.h + (level * (self.end.h - self.start.h)),
- s: self.start.s + (level * (self.end.s - self.start.s)),
- l: self.start.l + (level * (self.end.l - self.start.l)),
- a: self.start.a + (level * (self.end.a - self.start.a)),
- }
- }
- }
- impl From<gpui::color::Color> for Rgba {
- fn from(value: gpui::color::Color) -> Self {
- Self {
- r: value.0.r as f32 / 255.0,
- g: value.0.g as f32 / 255.0,
- b: value.0.b as f32 / 255.0,
- a: value.0.a as f32 / 255.0,
- }
- }
- }
- impl From<Hsla> for Rgba {
- fn from(color: Hsla) -> Self {
- let h = color.h;
- let s = color.s;
- let l = color.l;
- let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
- let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
- let m = l - c / 2.0;
- let cm = c + m;
- let xm = x + m;
- let (r, g, b) = match (h * 6.0).floor() as i32 {
- 0 | 6 => (cm, xm, m),
- 1 => (xm, cm, m),
- 2 => (m, cm, xm),
- 3 => (m, xm, cm),
- 4 => (xm, m, cm),
- _ => (cm, m, xm),
- };
- Rgba {
- r,
- g,
- b,
- a: color.a,
- }
- }
- }
- impl TryFrom<&'_ str> for Rgba {
- type Error = ParseIntError;
- fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
- let r = u8::from_str_radix(&value[1..3], 16)? as f32 / 255.0;
- let g = u8::from_str_radix(&value[3..5], 16)? as f32 / 255.0;
- let b = u8::from_str_radix(&value[5..7], 16)? as f32 / 255.0;
- let a = if value.len() > 7 {
- u8::from_str_radix(&value[7..9], 16)? as f32 / 255.0
- } else {
- 1.0
- };
- Ok(Rgba { r, g, b, a })
- }
- }
- impl Into<gpui::color::Color> for Rgba {
- fn into(self) -> gpui::color::Color {
- gpui::color::rgba(self.r, self.g, self.b, self.a)
- }
- }
- pub struct Hsla {
- pub h: f32,
- pub s: f32,
- pub l: f32,
- pub a: f32,
- }
- #[automatically_derived]
- impl ::core::default::Default for Hsla {
- #[inline]
- fn default() -> Hsla {
- Hsla {
- h: ::core::default::Default::default(),
- s: ::core::default::Default::default(),
- l: ::core::default::Default::default(),
- a: ::core::default::Default::default(),
- }
- }
- }
- #[automatically_derived]
- impl ::core::marker::Copy for Hsla {}
- #[automatically_derived]
- impl ::core::clone::Clone for Hsla {
- #[inline]
- fn clone(&self) -> Hsla {
- let _: ::core::clone::AssertParamIsClone<f32>;
- *self
- }
- }
- #[automatically_derived]
- impl ::core::fmt::Debug for Hsla {
- fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
- ::core::fmt::Formatter::debug_struct_field4_finish(
- f, "Hsla", "h", &self.h, "s", &self.s, "l", &self.l, "a", &&self.a,
- )
- }
- }
- #[automatically_derived]
- impl ::core::marker::StructuralPartialEq for Hsla {}
- #[automatically_derived]
- impl ::core::cmp::PartialEq for Hsla {
- #[inline]
- fn eq(&self, other: &Hsla) -> bool {
- self.h == other.h && self.s == other.s && self.l == other.l && self.a == other.a
- }
- }
- pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
- Hsla {
- h: h.clamp(0., 1.),
- s: s.clamp(0., 1.),
- l: l.clamp(0., 1.),
- a: a.clamp(0., 1.),
- }
- }
- pub fn black() -> Hsla {
- Hsla {
- h: 0.,
- s: 0.,
- l: 0.,
- a: 1.,
- }
- }
- impl From<Rgba> for Hsla {
- fn from(color: Rgba) -> Self {
- let r = color.r;
- let g = color.g;
- let b = color.b;
- let max = r.max(g.max(b));
- let min = r.min(g.min(b));
- let delta = max - min;
- let l = (max + min) / 2.0;
- let s = if l == 0.0 || l == 1.0 {
- 0.0
- } else if l < 0.5 {
- delta / (2.0 * l)
- } else {
- delta / (2.0 - 2.0 * l)
- };
- let h = if delta == 0.0 {
- 0.0
- } else if max == r {
- ((g - b) / delta).rem_euclid(6.0) / 6.0
- } else if max == g {
- ((b - r) / delta + 2.0) / 6.0
- } else {
- ((r - g) / delta + 4.0) / 6.0
- };
- Hsla {
- h,
- s,
- l,
- a: color.a,
- }
- }
- }
- impl Hsla {
- /// Scales the saturation and lightness by the given values, clamping at 1.0.
- pub fn scale_sl(mut self, s: f32, l: f32) -> Self {
- self.s = (self.s * s).clamp(0., 1.);
- self.l = (self.l * l).clamp(0., 1.);
- self
- }
- /// Increases the saturation of the color by a certain amount, with a max
- /// value of 1.0.
- pub fn saturate(mut self, amount: f32) -> Self {
- self.s += amount;
- self.s = self.s.clamp(0.0, 1.0);
- self
- }
- /// Decreases the saturation of the color by a certain amount, with a min
- /// value of 0.0.
- pub fn desaturate(mut self, amount: f32) -> Self {
- self.s -= amount;
- self.s = self.s.max(0.0);
- if self.s < 0.0 {
- self.s = 0.0;
- }
- self
- }
- /// Brightens the color by increasing the lightness by a certain amount,
- /// with a max value of 1.0.
- pub fn brighten(mut self, amount: f32) -> Self {
- self.l += amount;
- self.l = self.l.clamp(0.0, 1.0);
- self
- }
- /// Darkens the color by decreasing the lightness by a certain amount,
- /// with a max value of 0.0.
- pub fn darken(mut self, amount: f32) -> Self {
- self.l -= amount;
- self.l = self.l.clamp(0.0, 1.0);
- self
- }
- }
- impl From<gpui::color::Color> for Hsla {
- fn from(value: gpui::color::Color) -> Self {
- Rgba::from(value).into()
- }
- }
- impl Into<gpui::color::Color> for Hsla {
- fn into(self) -> gpui::color::Color {
- Rgba::from(self).into()
- }
- }
- pub struct ColorScale {
- colors: SmallVec<[Hsla; 2]>,
- positions: SmallVec<[f32; 2]>,
- }
- pub fn scale<I, C>(colors: I) -> ColorScale
- where
- I: IntoIterator<Item = C>,
- C: Into<Hsla>,
- {
- let mut scale = ColorScale {
- colors: colors.into_iter().map(Into::into).collect(),
- positions: SmallVec::new(),
- };
- let num_colors: f32 = scale.colors.len() as f32 - 1.0;
- scale.positions = (0..scale.colors.len())
- .map(|i| i as f32 / num_colors)
- .collect();
- scale
- }
- impl ColorScale {
- fn at(&self, t: f32) -> Hsla {
- if true {
- if !(0.0 <= t && t <= 1.0) {
- {
- ::core::panicking::panic_fmt(format_args!(
- "t value {0} is out of range. Expected value in range 0.0 to 1.0",
- t,
- ));
- }
- }
- }
- let position = match self
- .positions
- .binary_search_by(|a| a.partial_cmp(&t).unwrap())
- {
- Ok(index) | Err(index) => index,
- };
- let lower_bound = position.saturating_sub(1);
- let upper_bound = position.min(self.colors.len() - 1);
- let lower_color = &self.colors[lower_bound];
- let upper_color = &self.colors[upper_bound];
- match upper_bound.checked_sub(lower_bound) {
- Some(0) | None => *lower_color,
- Some(_) => {
- let interval_t = (t - self.positions[lower_bound])
- / (self.positions[upper_bound] - self.positions[lower_bound]);
- let h = lower_color.h + interval_t * (upper_color.h - lower_color.h);
- let s = lower_color.s + interval_t * (upper_color.s - lower_color.s);
- let l = lower_color.l + interval_t * (upper_color.l - lower_color.l);
- let a = lower_color.a + interval_t * (upper_color.a - lower_color.a);
- Hsla { h, s, l, a }
- }
- }
- }
- }
-}
-mod components {
- use crate::{
- element::{Element, ElementMetadata},
- frame,
- text::ArcCow,
- themes::rose_pine,
- };
- use gpui::{platform::MouseButton, ViewContext};
- use gpui_macros::Element;
- use std::{marker::PhantomData, rc::Rc};
- struct ButtonHandlers<V, D> {
- click: Option<Rc<dyn Fn(&mut V, &D, &mut ViewContext<V>)>>,
- }
- impl<V, D> Default for ButtonHandlers<V, D> {
- fn default() -> Self {
- Self { click: None }
- }
- }
- #[element_crate = "crate"]
- pub struct Button<V: 'static, D: 'static> {
- metadata: ElementMetadata<V>,
- handlers: ButtonHandlers<V, D>,
- label: Option<ArcCow<'static, str>>,
- icon: Option<ArcCow<'static, str>>,
- data: Rc<D>,
- view_type: PhantomData<V>,
- }
- impl<V: 'static, D: 'static> crate::element::Element<V> for Button<V, D> {
- type Layout = crate::element::AnyElement<V>;
- fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
- &mut self.metadata.style
- }
- fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
- &mut self.metadata.handlers
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut crate::element::LayoutContext<V>,
- ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
- let mut element = self.render(view, cx).into_any();
- let node_id = element.layout(view, cx)?;
- Ok((node_id, element))
- }
- fn paint<'a>(
- &mut self,
- layout: crate::element::Layout<'a, Self::Layout>,
- view: &mut V,
- cx: &mut crate::element::PaintContext<V>,
- ) -> anyhow::Result<()> {
- layout.from_element.paint(view, cx)?;
- Ok(())
- }
- }
- impl<V: 'static, D: 'static> crate::element::IntoElement<V> for Button<V, D> {
- type Element = Self;
- fn into_element(self) -> Self {
- self
- }
- }
- impl<V: 'static> Button<V, ()> {
- fn new() -> Self {
- Self {
- metadata: Default::default(),
- handlers: ButtonHandlers::default(),
- label: None,
- icon: None,
- data: Rc::new(()),
- view_type: PhantomData,
- }
- }
- pub fn data<D: 'static>(self, data: D) -> Button<V, D> {
- Button {
- metadata: Default::default(),
- handlers: ButtonHandlers::default(),
- label: self.label,
- icon: self.icon,
- data: Rc::new(data),
- view_type: PhantomData,
- }
- }
- }
- impl<V: 'static, D: 'static> Button<V, D> {
- pub fn label(mut self, label: impl Into<ArcCow<'static, str>>) -> Self {
- self.label = Some(label.into());
- self
- }
- pub fn icon(mut self, icon: impl Into<ArcCow<'static, str>>) -> Self {
- self.icon = Some(icon.into());
- self
- }
- pub fn click(self, handler: impl Fn(&mut V, &D, &mut ViewContext<V>) + 'static) -> Self {
- let data = self.data.clone();
- Element::click(self, MouseButton::Left, move |view, _, cx| {
- handler(view, data.as_ref(), cx);
- })
- }
- }
- pub fn button<V>() -> Button<V, ()> {
- Button::new()
- }
- impl<V: 'static, D: 'static> Button<V, D> {
- fn render(&mut self, view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
- let button = frame()
- .fill(rose_pine::dawn().error(0.5))
- .h_4()
- .children(self.label.clone());
- if let Some(handler) = self.handlers.click.clone() {
- let data = self.data.clone();
- button.mouse_down(MouseButton::Left, move |view, event, cx| {
- handler(view, data.as_ref(), cx)
- })
- } else {
- button
- }
- }
- }
-}
-mod element {
- pub use crate::paint_context::PaintContext;
- use crate::{
- adapter::Adapter,
- color::Hsla,
- hoverable::Hoverable,
- style::{Display, Fill, OptionalStyle, Overflow, Position},
- };
- use anyhow::Result;
- pub use gpui::LayoutContext;
- use gpui::{
- geometry::{DefinedLength, Length, OptionalPoint},
- platform::{MouseButton, MouseButtonEvent},
- EngineLayout, EventContext, RenderContext, ViewContext,
- };
- use gpui_macros::tailwind_lengths;
- use std::{
- any::{Any, TypeId},
- cell::Cell,
- rc::Rc,
- };
- pub use taffy::tree::NodeId;
- pub struct Layout<'a, E: ?Sized> {
- pub from_engine: EngineLayout,
- pub from_element: &'a mut E,
- }
- pub struct ElementMetadata<V> {
- pub style: OptionalStyle,
- pub handlers: Vec<EventHandler<V>>,
- }
- pub struct EventHandler<V> {
- handler: Rc<dyn Fn(&mut V, &dyn Any, &mut EventContext<V>)>,
- event_type: TypeId,
- outside_bounds: bool,
- }
- impl<V> Clone for EventHandler<V> {
- fn clone(&self) -> Self {
- Self {
- handler: self.handler.clone(),
- event_type: self.event_type,
- outside_bounds: self.outside_bounds,
- }
- }
- }
- impl<V> Default for ElementMetadata<V> {
- fn default() -> Self {
- Self {
- style: OptionalStyle::default(),
- handlers: Vec::new(),
- }
- }
- }
- pub trait Element<V: 'static>: 'static {
- type Layout: 'static;
- fn declared_style(&mut self) -> &mut OptionalStyle;
- fn computed_style(&mut self) -> &OptionalStyle {
- self.declared_style()
- }
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut LayoutContext<V>,
- ) -> Result<(NodeId, Self::Layout)>;
- fn paint<'a>(
- &mut self,
- layout: Layout<Self::Layout>,
- view: &mut V,
- cx: &mut PaintContext<V>,
- ) -> Result<()>;
- /// Convert to a dynamically-typed element suitable for layout and paint.
- fn into_any(self) -> AnyElement<V>
- where
- Self: 'static + Sized,
- {
- AnyElement {
- element: Box::new(self) as Box<dyn ElementObject<V>>,
- layout: None,
- }
- }
- fn adapt(self) -> Adapter<V>
- where
- Self: Sized,
- Self: Element<V>,
- {
- Adapter(self.into_any())
- }
- fn click(
- self,
- button: MouseButton,
- handler: impl Fn(&mut V, &MouseButtonEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
- where
- Self: Sized,
- {
- let pressed: Rc<Cell<bool>> = Default::default();
- self.mouse_down(button, {
- let pressed = pressed.clone();
- move |_, _, _| {
- pressed.set(true);
- }
- })
- .mouse_up_outside(button, {
- let pressed = pressed.clone();
- move |_, _, _| {
- pressed.set(false);
- }
- })
- .mouse_up(button, move |view, event, event_cx| {
- if pressed.get() {
- pressed.set(false);
- handler(view, event, event_cx);
- }
- })
- }
- fn mouse_down(
- mut self,
- button: MouseButton,
- handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
- ) -> Self
- where
- Self: Sized,
- {
- self.handlers_mut().push(EventHandler {
- handler: Rc::new(move |view, event, event_cx| {
- let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
- if event.button == button && event.is_down {
- handler(view, event, event_cx);
- }
- }),
- event_type: TypeId::of::<MouseButtonEvent>(),
- outside_bounds: false,
- });
- self
- }
- fn mouse_down_outside(
- mut self,
- button: MouseButton,
- handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
- ) -> Self
- where
- Self: Sized,
- {
- self.handlers_mut().push(EventHandler {
- handler: Rc::new(move |view, event, event_cx| {
- let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
- if event.button == button && event.is_down {
- handler(view, event, event_cx);
- }
- }),
- event_type: TypeId::of::<MouseButtonEvent>(),
- outside_bounds: true,
- });
- self
- }
- fn mouse_up(
- mut self,
- button: MouseButton,
- handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
- ) -> Self
- where
- Self: Sized,
- {
- self.handlers_mut().push(EventHandler {
- handler: Rc::new(move |view, event, event_cx| {
- let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
- if event.button == button && !event.is_down {
- handler(view, event, event_cx);
- }
- }),
- event_type: TypeId::of::<MouseButtonEvent>(),
- outside_bounds: false,
- });
- self
- }
- fn mouse_up_outside(
- mut self,
- button: MouseButton,
- handler: impl Fn(&mut V, &MouseButtonEvent, &mut EventContext<V>) + 'static,
- ) -> Self
- where
- Self: Sized,
- {
- self.handlers_mut().push(EventHandler {
- handler: Rc::new(move |view, event, event_cx| {
- let event = event.downcast_ref::<MouseButtonEvent>().unwrap();
- if event.button == button && !event.is_down {
- handler(view, event, event_cx);
- }
- }),
- event_type: TypeId::of::<MouseButtonEvent>(),
- outside_bounds: true,
- });
- self
- }
- fn block(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().display = Some(Display::Block);
- self
- }
- fn flex(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().display = Some(Display::Flex);
- self
- }
- fn grid(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().display = Some(Display::Grid);
- self
- }
- fn overflow_visible(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow = OptionalPoint {
- x: Some(Overflow::Visible),
- y: Some(Overflow::Visible),
- };
- self
- }
- fn overflow_hidden(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow = OptionalPoint {
- x: Some(Overflow::Hidden),
- y: Some(Overflow::Hidden),
- };
- self
- }
- fn overflow_scroll(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow = OptionalPoint {
- x: Some(Overflow::Scroll),
- y: Some(Overflow::Scroll),
- };
- self
- }
- fn overflow_x_visible(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.x = Some(Overflow::Visible);
- self
- }
- fn overflow_x_hidden(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.x = Some(Overflow::Hidden);
- self
- }
- fn overflow_x_scroll(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.x = Some(Overflow::Scroll);
- self
- }
- fn overflow_y_visible(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.y = Some(Overflow::Visible);
- self
- }
- fn overflow_y_hidden(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.y = Some(Overflow::Hidden);
- self
- }
- fn overflow_y_scroll(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().overflow.y = Some(Overflow::Scroll);
- self
- }
- fn relative(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().position = Some(Position::Relative);
- self
- }
- fn absolute(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().position = Some(Position::Absolute);
- self
- }
- fn inset_0(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(0.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_px(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(1.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_0_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.125).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.25).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.375).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.5).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.625).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.75).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.875).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_4(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.25).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_6(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.5).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_7(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.75).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_8(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_9(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.25).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_10(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.5).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_11(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.75).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_12(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_14(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.5).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_16(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(4.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_20(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(5.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_24(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(6.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_28(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(7.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_32(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(8.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_36(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(9.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_40(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(10.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_44(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(11.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_48(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(12.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_52(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(13.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_56(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(14.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_60(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(15.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_64(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(16.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_72(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(18.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_80(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(20.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_96(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(24.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_half(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(20.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(40.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(60.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_4_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(80.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_4_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_5_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_1_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(8.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_2_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_3_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_4_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_5_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(41.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_6_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_7_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(58.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_8_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_9_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_10_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_11_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(91.666667).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn inset_full(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(100.).into();
- {
- let inset = self
- .computed_style()
- .inset
- .get_or_insert_with(Default::default);
- inset.top = length;
- inset.right = length;
- inset.bottom = length;
- inset.left = length;
- self
- }
- }
- fn w(mut self, width: impl Into<Length>) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.width = Some(width.into());
- self
- }
- fn w_auto(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.width = Some(Length::Auto);
- self
- }
- fn w_0(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(0.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_px(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(1.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_0_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.125).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.25).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.375).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.5).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.625).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.75).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.875).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_4(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.25).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_6(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.5).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_7(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.75).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_8(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_9(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.25).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_10(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.5).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_11(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.75).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_12(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_14(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.5).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_16(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(4.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_20(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(5.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_24(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(6.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_28(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(7.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_32(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(8.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_36(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(9.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_40(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(10.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_44(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(11.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_48(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(12.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_52(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(13.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_56(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(14.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_60(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(15.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_64(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(16.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_72(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(18.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_80(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(20.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_96(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(24.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_half(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(20.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(40.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(60.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_4_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(80.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_4_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_5_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_1_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(8.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_2_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_3_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_4_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_5_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(41.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_6_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_7_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(58.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_8_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_9_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_10_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_11_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(91.666667).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn w_full(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(100.).into();
- {
- self.declared_style().size.width = Some(length);
- self
- }
- }
- fn min_w_0(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(0.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_px(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(1.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_0_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.125).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.25).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.375).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.5).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.625).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.75).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.875).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_4(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.25).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_6(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.5).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_7(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.75).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_8(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_9(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.25).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_10(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.5).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_11(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.75).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_12(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_14(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.5).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_16(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(4.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_20(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(5.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_24(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(6.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_28(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(7.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_32(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(8.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_36(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(9.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_40(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(10.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_44(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(11.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_48(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(12.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_52(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(13.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_56(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(14.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_60(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(15.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_64(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(16.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_72(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(18.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_80(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(20.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_96(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(24.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_half(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(20.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(40.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(60.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_4_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(80.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_4_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_5_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_1_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(8.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_2_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_3_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_4_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_5_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(41.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_6_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_7_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(58.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_8_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_9_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_10_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_11_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(91.666667).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn min_w_full(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(100.).into();
- {
- self.declared_style().min_size.width = Some(length);
- self
- }
- }
- fn h(mut self, height: impl Into<Length>) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.height = Some(height.into());
- self
- }
- fn h_auto(mut self) -> Self
- where
- Self: Sized,
- {
- self.declared_style().size.height = Some(Length::Auto);
- self
- }
- fn h_0(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Pixels(0.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_px(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Pixels(1.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_0_5(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.125).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.25).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_5(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.375).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.5).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_5(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.625).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.75).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3_5(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(0.875).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_4(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(1.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_5(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(1.25).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_6(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(1.5).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_7(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(1.75).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_8(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(2.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_9(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(2.25).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_10(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(2.5).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_11(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(2.75).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_12(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(3.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_14(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(3.5).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_16(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(4.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_20(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(5.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_24(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(6.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_28(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(7.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_32(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(8.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_36(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(9.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_40(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(10.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_44(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(11.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_48(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(12.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_52(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(13.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_56(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(14.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_60(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(15.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_64(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(16.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_72(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(18.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_80(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(20.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_96(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Rems(24.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_half(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(25.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(75.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(20.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(40.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(60.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_4_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(80.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_4_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_5_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_1_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(8.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_2_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_3_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(25.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_4_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_5_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(41.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_6_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(50.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_7_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(58.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_8_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_9_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(75.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_10_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_11_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(91.666667).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn h_full(mut self) -> Self
- where
- Self: Sized,
- {
- let height = DefinedLength::Percent(100.).into();
- {
- self.declared_style().size.height = Some(height);
- self
- }
- }
- fn min_h_0(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(0.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_px(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Pixels(1.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_0_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.125).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.25).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.375).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.5).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.625).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.75).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(0.875).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_4(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_5(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.25).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_6(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.5).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_7(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(1.75).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_8(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_9(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.25).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_10(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.5).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_11(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(2.75).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_12(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_14(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(3.5).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_16(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(4.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_20(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(5.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_24(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(6.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_28(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(7.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_32(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(8.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_36(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(9.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_40(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(10.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_44(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(11.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_48(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(12.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_52(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(13.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_56(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(14.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_60(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(15.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_64(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(16.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_72(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(18.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_80(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(20.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_96(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Rems(24.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_half(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_3rd(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3_4th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(20.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(40.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(60.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_4_5th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(80.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_4_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_5_6th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_1_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(8.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_2_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(16.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_3_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(25.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_4_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(33.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_5_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(41.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_6_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(50.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_7_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(58.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_8_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(66.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_9_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(75.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_10_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(83.333333).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_11_12th(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(91.666667).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn min_h_full(mut self) -> Self
- where
- Self: Sized,
- {
- let length = DefinedLength::Percent(100.).into();
- {
- self.declared_style().min_size.height = Some(length);
- self
- }
- }
- fn hoverable(self) -> Hoverable<V, Self>
- where
- Self: Sized,
- {
- Hoverable::new(self)
- }
- fn fill(mut self, fill: impl Into<Fill>) -> Self
- where
- Self: Sized,
- {
- self.declared_style().fill = Some(Some(fill.into()));
- self
- }
- fn text_color(mut self, color: impl Into<Hsla>) -> Self
- where
- Self: Sized,
- {
- self.declared_style().text_color = Some(Some(color.into()));
- self
- }
- }
- trait ElementObject<V> {
- fn style(&mut self) -> &mut OptionalStyle;
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>>;
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut LayoutContext<V>,
- ) -> Result<(NodeId, Box<dyn Any>)>;
- fn paint(
- &mut self,
- layout: Layout<dyn Any>,
- view: &mut V,
- cx: &mut PaintContext<V>,
- ) -> Result<()>;
- }
- impl<V: 'static, E: Element<V>> ElementObject<V> for E {
- fn style(&mut self) -> &mut OptionalStyle {
- Element::declared_style(self)
- }
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
- Element::handlers_mut(self)
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut LayoutContext<V>,
- ) -> Result<(NodeId, Box<dyn Any>)> {
- let (node_id, layout) = self.layout(view, cx)?;
- let layout = Box::new(layout) as Box<dyn Any>;
- Ok((node_id, layout))
- }
- fn paint(
- &mut self,
- layout: Layout<dyn Any>,
- view: &mut V,
- cx: &mut PaintContext<V>,
- ) -> Result<()> {
- let layout = Layout {
- from_engine: layout.from_engine,
- from_element: layout.from_element.downcast_mut::<E::Layout>().unwrap(),
- };
- self.paint(layout, view, cx)
- }
- }
- /// A dynamically typed element.
- pub struct AnyElement<V> {
- element: Box<dyn ElementObject<V>>,
- layout: Option<(NodeId, Box<dyn Any>)>,
- }
- impl<V: 'static> AnyElement<V> {
- pub fn layout(&mut self, view: &mut V, cx: &mut LayoutContext<V>) -> Result<NodeId> {
- let pushed_text_style = self.push_text_style(cx);
- let (node_id, layout) = self.element.layout(view, cx)?;
- self.layout = Some((node_id, layout));
- if pushed_text_style {
- cx.pop_text_style();
- }
- Ok(node_id)
- }
- pub fn push_text_style(&mut self, cx: &mut impl RenderContext) -> bool {
- let text_style = self.element.style().text_style();
- if let Some(text_style) = text_style {
- let mut current_text_style = cx.text_style();
- text_style.apply(&mut current_text_style);
- cx.push_text_style(current_text_style);
- true
- } else {
- false
- }
- }
- pub fn paint(&mut self, view: &mut V, cx: &mut PaintContext<V>) -> Result<()> {
- let pushed_text_style = self.push_text_style(cx);
- let (layout_node_id, element_layout) =
- self.layout.as_mut().expect("paint called before layout");
- let layout = Layout {
- from_engine: cx
- .layout_engine()
- .unwrap()
- .computed_layout(*layout_node_id)
- .expect("make sure you're using this within a gpui2 adapter element"),
- from_element: element_layout.as_mut(),
- };
- let style = self.element.style();
- let fill_color = style.fill.flatten().and_then(|fill| fill.color());
- if let Some(fill_color) = fill_color {
- cx.scene.push_quad(gpui::scene::Quad {
- bounds: layout.from_engine.bounds,
- background: Some(fill_color.into()),
- border: Default::default(),
- corner_radii: Default::default(),
- });
- }
- for event_handler in self.element.handlers_mut().iter().cloned() {
- let EngineLayout { order, bounds } = layout.from_engine;
- let view_id = cx.view_id();
- let view_event_handler = event_handler.handler.clone();
- cx.scene
- .interactive_regions
- .push(gpui::scene::InteractiveRegion {
- order,
- bounds,
- outside_bounds: event_handler.outside_bounds,
- event_handler: Rc::new(move |view, event, window_cx, view_id| {
- let mut view_context = ViewContext::mutable(window_cx, view_id);
- let mut event_context = EventContext::new(&mut view_context);
- view_event_handler(
- view.downcast_mut().unwrap(),
- event,
- &mut event_context,
- );
- }),
- event_type: event_handler.event_type,
- view_id,
- });
- }
- self.element.paint(layout, view, cx)?;
- if pushed_text_style {
- cx.pop_text_style();
- }
- Ok(())
- }
- }
- impl<V: 'static> Element<V> for AnyElement<V> {
- type Layout = ();
- fn declared_style(&mut self) -> &mut OptionalStyle {
- self.element.style()
- }
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
- self.element.handlers_mut()
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut LayoutContext<V>,
- ) -> Result<(NodeId, Self::Layout)> {
- Ok((self.layout(view, cx)?, ()))
- }
- fn paint(
- &mut self,
- layout: Layout<()>,
- view: &mut V,
- cx: &mut PaintContext<V>,
- ) -> Result<()> {
- self.paint(view, cx)
- }
- }
- pub trait IntoElement<V: 'static> {
- type Element: Element<V>;
- fn into_element(self) -> Self::Element;
- fn into_any_element(self) -> AnyElement<V>
- where
- Self: Sized,
- {
- self.into_element().into_any()
- }
- }
-}
-mod frame {
- use crate::{
- element::{
- AnyElement, Element, EventHandler, IntoElement, Layout, LayoutContext, NodeId,
- PaintContext,
- },
- style::{OptionalStyle, Style},
- };
- use anyhow::{anyhow, Result};
- use gpui::LayoutNodeId;
- use gpui_macros::IntoElement;
- #[element_crate = "crate"]
- pub struct Frame<V: 'static> {
- style: OptionalStyle,
- handlers: Vec<EventHandler<V>>,
- children: Vec<AnyElement<V>>,
- }
- impl<V: 'static> crate::element::IntoElement<V> for Frame<V> {
- type Element = Self;
- fn into_element(self) -> Self {
- self
- }
- }
- pub fn frame<V>() -> Frame<V> {
- Frame {
- style: OptionalStyle::default(),
- handlers: Vec::new(),
- children: Vec::new(),
- }
- }
- impl<V: 'static> Element<V> for Frame<V> {
- type Layout = ();
- fn declared_style(&mut self) -> &mut OptionalStyle {
- &mut self.style
- }
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
- &mut self.handlers
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut LayoutContext<V>,
- ) -> Result<(NodeId, Self::Layout)> {
- let child_layout_node_ids = self
- .children
- .iter_mut()
- .map(|child| child.layout(view, cx))
- .collect::<Result<Vec<LayoutNodeId>>>()?;
- let rem_size = cx.rem_pixels();
- let style: Style = self.style.into();
- let node_id = cx
- .layout_engine()
- .ok_or_else(|| {
- ::anyhow::__private::must_use({
- let error =
- ::anyhow::__private::format_err(format_args!("no layout engine"));
- error
- })
- })?
- .add_node(style.to_taffy(rem_size), child_layout_node_ids)?;
- Ok((node_id, ()))
- }
- fn paint(
- &mut self,
- layout: Layout<()>,
- view: &mut V,
- cx: &mut PaintContext<V>,
- ) -> Result<()> {
- for child in &mut self.children {
- child.paint(view, cx)?;
- }
- Ok(())
- }
- }
- impl<V: 'static> Frame<V> {
- pub fn child(mut self, child: impl IntoElement<V>) -> Self {
- self.children.push(child.into_any_element());
- self
- }
- pub fn children<I, E>(mut self, children: I) -> Self
- where
- I: IntoIterator<Item = E>,
- E: IntoElement<V>,
- {
- self.children
- .extend(children.into_iter().map(|e| e.into_any_element()));
- self
- }
- }
-}
-mod hoverable {
- use crate::{
- element::Element,
- style::{OptionalStyle, Style},
- };
- use gpui::{
- geometry::{rect::RectF, vector::Vector2F},
- scene::MouseMove,
- EngineLayout,
- };
- use std::{cell::Cell, marker::PhantomData, rc::Rc};
- pub struct Hoverable<V, E> {
- hover_style: OptionalStyle,
- computed_style: Option<Style>,
- view_type: PhantomData<V>,
- child: E,
- }
- impl<V, E> Hoverable<V, E> {
- pub fn new(child: E) -> Self {
- Self {
- hover_style: OptionalStyle::default(),
- computed_style: None,
- view_type: PhantomData,
- child,
- }
- }
- }
- impl<V: 'static, E: Element<V>> Element<V> for Hoverable<V, E> {
- type Layout = E::Layout;
- fn declared_style(&mut self) -> &mut OptionalStyle {
- &mut self.hover_style
- }
- fn computed_style(&mut self) -> &OptionalStyle {
- ::core::panicking::panic("not yet implemented")
- }
- fn handlers_mut(&mut self) -> &mut Vec<crate::element::EventHandler<V>> {
- self.child.handlers_mut()
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut gpui::LayoutContext<V>,
- ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
- self.child.layout(view, cx)
- }
- fn paint<'a>(
- &mut self,
- layout: crate::element::Layout<Self::Layout>,
- view: &mut V,
- cx: &mut crate::element::PaintContext<V>,
- ) -> anyhow::Result<()> {
- let EngineLayout { bounds, order } = layout.from_engine;
- let window_bounds = RectF::new(Vector2F::zero(), cx.window_size());
- let was_hovered = Rc::new(Cell::new(false));
- self.child.paint(layout, view, cx)?;
- cx.draw_interactive_region(
- order,
- window_bounds,
- false,
- move |view, event: &MouseMove, cx| {
- let is_hovered = bounds.contains_point(cx.mouse_position());
- if is_hovered != was_hovered.get() {
- was_hovered.set(is_hovered);
- cx.repaint();
- }
- },
- );
- Ok(())
- }
- }
-}
-mod paint_context {
- use derive_more::{Deref, DerefMut};
- use gpui::{geometry::rect::RectF, EventContext, RenderContext, ViewContext};
- pub use gpui::{LayoutContext, PaintContext as LegacyPaintContext};
- use std::{any::TypeId, rc::Rc};
- pub use taffy::tree::NodeId;
- pub struct PaintContext<'a, 'b, 'c, 'd, V> {
- #[deref]
- #[deref_mut]
- pub(crate) legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
- pub(crate) scene: &'d mut gpui::SceneBuilder,
- }
- impl<'a, 'b, 'c, 'd, V> ::core::ops::Deref for PaintContext<'a, 'b, 'c, 'd, V> {
- type Target = &'d mut LegacyPaintContext<'a, 'b, 'c, V>;
- #[inline]
- fn deref(&self) -> &Self::Target {
- &self.legacy_cx
- }
- }
- impl<'a, 'b, 'c, 'd, V> ::core::ops::DerefMut for PaintContext<'a, 'b, 'c, 'd, V> {
- #[inline]
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.legacy_cx
- }
- }
- impl<V> RenderContext for PaintContext<'_, '_, '_, '_, V> {
- fn text_style(&self) -> gpui::fonts::TextStyle {
- self.legacy_cx.text_style()
- }
- fn push_text_style(&mut self, style: gpui::fonts::TextStyle) {
- self.legacy_cx.push_text_style(style)
- }
- fn pop_text_style(&mut self) {
- self.legacy_cx.pop_text_style()
- }
- }
- impl<'a, 'b, 'c, 'd, V: 'static> PaintContext<'a, 'b, 'c, 'd, V> {
- pub fn new(
- legacy_cx: &'d mut LegacyPaintContext<'a, 'b, 'c, V>,
- scene: &'d mut gpui::SceneBuilder,
- ) -> Self {
- Self { legacy_cx, scene }
- }
- pub fn draw_interactive_region<E: 'static>(
- &mut self,
- order: u32,
- bounds: RectF,
- outside_bounds: bool,
- event_handler: impl Fn(&mut V, &E, &mut EventContext<V>) + 'static,
- ) {
- self.scene
- .interactive_regions
- .push(gpui::scene::InteractiveRegion {
- order,
- bounds,
- outside_bounds,
- event_handler: Rc::new(move |view, event, window_cx, view_id| {
- let mut view_context = ViewContext::mutable(window_cx, view_id);
- let mut event_context = EventContext::new(&mut view_context);
- event_handler(
- view.downcast_mut().unwrap(),
- event.downcast_ref().unwrap(),
- &mut event_context,
- );
- }),
- event_type: TypeId::of::<E>(),
- view_id: self.view_id(),
- });
- }
- }
-}
-mod style {
- use crate::color::Hsla;
- use gpui::geometry::{
- DefinedLength, Edges, Length, OptionalEdges, OptionalPoint, OptionalSize, Point, Size,
- };
- use optional::Optional;
- pub use taffy::style::{
- AlignContent, AlignItems, AlignSelf, Display, FlexDirection, FlexWrap, JustifyContent,
- Overflow, Position,
- };
- pub struct Style {
- /// What layout strategy should be used?
- pub display: Display,
- /// How children overflowing their container should affect layout
- #[optional]
- pub overflow: Point<Overflow>,
- /// How much space (in points) should be reserved for the scrollbars of `Overflow::Scroll` and `Overflow::Auto` nodes.
- pub scrollbar_width: f32,
- /// What should the `position` value of this struct use as a base offset?
- pub position: Position,
- /// How should the position of this element be tweaked relative to the layout defined?
- pub inset: Edges<Length>,
- /// Sets the initial size of the item
- #[optional]
- pub size: Size<Length>,
- /// Controls the minimum size of the item
- #[optional]
- pub min_size: Size<Length>,
- /// Controls the maximum size of the item
- #[optional]
- pub max_size: Size<Length>,
- /// Sets the preferred aspect ratio for the item. The ratio is calculated as width divided by height.
- pub aspect_ratio: Option<f32>,
- /// How large should the margin be on each side?
- #[optional]
- pub margin: Edges<Length>,
- /// How large should the padding be on each side?
- pub padding: Edges<DefinedLength>,
- /// How large should the border be on each side?
- pub border: Edges<DefinedLength>,
- /// How this node's children aligned in the cross/block axis?
- pub align_items: Option<AlignItems>,
- /// How this node should be aligned in the cross/block axis. Falls back to the parents [`AlignItems`] if not set
- pub align_self: Option<AlignSelf>,
- /// How should content contained within this item be aligned in the cross/block axis
- pub align_content: Option<AlignContent>,
- /// How should contained within this item be aligned in the main/inline axis
- pub justify_content: Option<JustifyContent>,
- /// How large should the gaps between items in a flex container be?
- pub gap: Size<DefinedLength>,
- /// Which direction does the main axis flow in?
- pub flex_direction: FlexDirection,
- /// Should elements wrap, or stay in a single line?
- pub flex_wrap: FlexWrap,
- /// Sets the initial main axis size of the item
- pub flex_basis: Length,
- /// The relative rate at which this item grows when it is expanding to fill space, 0.0 is the default value, and this value must be positive.
- pub flex_grow: f32,
- /// The relative rate at which this item shrinks when it is contracting to fit into space, 1.0 is the default value, and this value must be positive.
- pub flex_shrink: f32,
- /// The fill color of this element
- pub fill: Option<Fill>,
- /// The color of text within this element. Cascades to children unless overridden.
- pub text_color: Option<Hsla>,
- }
- #[automatically_derived]
- impl ::core::clone::Clone for Style {
- #[inline]
- fn clone(&self) -> Style {
- Style {
- display: ::core::clone::Clone::clone(&self.display),
- overflow: ::core::clone::Clone::clone(&self.overflow),
- scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
- position: ::core::clone::Clone::clone(&self.position),
- inset: ::core::clone::Clone::clone(&self.inset),
- size: ::core::clone::Clone::clone(&self.size),
- min_size: ::core::clone::Clone::clone(&self.min_size),
- max_size: ::core::clone::Clone::clone(&self.max_size),
- aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
- margin: ::core::clone::Clone::clone(&self.margin),
- padding: ::core::clone::Clone::clone(&self.padding),
- border: ::core::clone::Clone::clone(&self.border),
- align_items: ::core::clone::Clone::clone(&self.align_items),
- align_self: ::core::clone::Clone::clone(&self.align_self),
- align_content: ::core::clone::Clone::clone(&self.align_content),
- justify_content: ::core::clone::Clone::clone(&self.justify_content),
- gap: ::core::clone::Clone::clone(&self.gap),
- flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
- flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
- flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
- flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
- flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
- fill: ::core::clone::Clone::clone(&self.fill),
- text_color: ::core::clone::Clone::clone(&self.text_color),
- }
- }
- }
- pub struct OptionalStyle {
- pub display: Option<Display>,
- pub overflow: OptionalPoint<Overflow>,
- pub scrollbar_width: Option<f32>,
- pub position: Option<Position>,
- pub inset: Option<Edges<Length>>,
- pub size: OptionalSize<Length>,
- pub min_size: OptionalSize<Length>,
- pub max_size: OptionalSize<Length>,
- pub aspect_ratio: Option<Option<f32>>,
- pub margin: OptionalEdges<Length>,
- pub padding: Option<Edges<DefinedLength>>,
- pub border: Option<Edges<DefinedLength>>,
- pub align_items: Option<Option<AlignItems>>,
- pub align_self: Option<Option<AlignSelf>>,
- pub align_content: Option<Option<AlignContent>>,
- pub justify_content: Option<Option<JustifyContent>>,
- pub gap: Option<Size<DefinedLength>>,
- pub flex_direction: Option<FlexDirection>,
- pub flex_wrap: Option<FlexWrap>,
- pub flex_basis: Option<Length>,
- pub flex_grow: Option<f32>,
- pub flex_shrink: Option<f32>,
- pub fill: Option<Option<Fill>>,
- pub text_color: Option<Option<Hsla>>,
- }
- #[automatically_derived]
- impl ::core::default::Default for OptionalStyle {
- #[inline]
- fn default() -> OptionalStyle {
- OptionalStyle {
- display: ::core::default::Default::default(),
- overflow: ::core::default::Default::default(),
- scrollbar_width: ::core::default::Default::default(),
- position: ::core::default::Default::default(),
- inset: ::core::default::Default::default(),
- size: ::core::default::Default::default(),
- min_size: ::core::default::Default::default(),
- max_size: ::core::default::Default::default(),
- aspect_ratio: ::core::default::Default::default(),
- margin: ::core::default::Default::default(),
- padding: ::core::default::Default::default(),
- border: ::core::default::Default::default(),
- align_items: ::core::default::Default::default(),
- align_self: ::core::default::Default::default(),
- align_content: ::core::default::Default::default(),
- justify_content: ::core::default::Default::default(),
- gap: ::core::default::Default::default(),
- flex_direction: ::core::default::Default::default(),
- flex_wrap: ::core::default::Default::default(),
- flex_basis: ::core::default::Default::default(),
- flex_grow: ::core::default::Default::default(),
- flex_shrink: ::core::default::Default::default(),
- fill: ::core::default::Default::default(),
- text_color: ::core::default::Default::default(),
- }
- }
- }
- #[automatically_derived]
- impl ::core::clone::Clone for OptionalStyle {
- #[inline]
- fn clone(&self) -> OptionalStyle {
- OptionalStyle {
- display: ::core::clone::Clone::clone(&self.display),
- overflow: ::core::clone::Clone::clone(&self.overflow),
- scrollbar_width: ::core::clone::Clone::clone(&self.scrollbar_width),
- position: ::core::clone::Clone::clone(&self.position),
- inset: ::core::clone::Clone::clone(&self.inset),
- size: ::core::clone::Clone::clone(&self.size),
- min_size: ::core::clone::Clone::clone(&self.min_size),
- max_size: ::core::clone::Clone::clone(&self.max_size),
- aspect_ratio: ::core::clone::Clone::clone(&self.aspect_ratio),
- margin: ::core::clone::Clone::clone(&self.margin),
- padding: ::core::clone::Clone::clone(&self.padding),
- border: ::core::clone::Clone::clone(&self.border),
- align_items: ::core::clone::Clone::clone(&self.align_items),
- align_self: ::core::clone::Clone::clone(&self.align_self),
- align_content: ::core::clone::Clone::clone(&self.align_content),
- justify_content: ::core::clone::Clone::clone(&self.justify_content),
- gap: ::core::clone::Clone::clone(&self.gap),
- flex_direction: ::core::clone::Clone::clone(&self.flex_direction),
- flex_wrap: ::core::clone::Clone::clone(&self.flex_wrap),
- flex_basis: ::core::clone::Clone::clone(&self.flex_basis),
- flex_grow: ::core::clone::Clone::clone(&self.flex_grow),
- flex_shrink: ::core::clone::Clone::clone(&self.flex_shrink),
- fill: ::core::clone::Clone::clone(&self.fill),
- text_color: ::core::clone::Clone::clone(&self.text_color),
- }
- }
- }
- impl Optional for OptionalStyle {
- type Base = Style;
- fn assign(&self, base: &mut Self::Base) {
- if let Some(value) = self.display.clone() {
- base.display = value;
- }
- if let Some(value) = self.overflow.clone() {
- base.overflow = value;
- }
- if let Some(value) = self.scrollbar_width.clone() {
- base.scrollbar_width = value;
- }
- if let Some(value) = self.position.clone() {
- base.position = value;
- }
- if let Some(value) = self.inset.clone() {
- base.inset = value;
- }
- if let Some(value) = self.size.clone() {
- base.size = value;
- }
- if let Some(value) = self.min_size.clone() {
- base.min_size = value;
- }
- if let Some(value) = self.max_size.clone() {
- base.max_size = value;
- }
- if let Some(value) = self.aspect_ratio.clone() {
- base.aspect_ratio = value;
- }
- if let Some(value) = self.margin.clone() {
- base.margin = value;
- }
- if let Some(value) = self.padding.clone() {
- base.padding = value;
- }
- if let Some(value) = self.border.clone() {
- base.border = value;
- }
- if let Some(value) = self.align_items.clone() {
- base.align_items = value;
- }
- if let Some(value) = self.align_self.clone() {
- base.align_self = value;
- }
- if let Some(value) = self.align_content.clone() {
- base.align_content = value;
- }
- if let Some(value) = self.justify_content.clone() {
- base.justify_content = value;
- }
- if let Some(value) = self.gap.clone() {
- base.gap = value;
- }
- if let Some(value) = self.flex_direction.clone() {
- base.flex_direction = value;
- }
- if let Some(value) = self.flex_wrap.clone() {
- base.flex_wrap = value;
- }
- if let Some(value) = self.flex_basis.clone() {
- base.flex_basis = value;
- }
- if let Some(value) = self.flex_grow.clone() {
- base.flex_grow = value;
- }
- if let Some(value) = self.flex_shrink.clone() {
- base.flex_shrink = value;
- }
- if let Some(value) = self.fill.clone() {
- base.fill = value;
- }
- if let Some(value) = self.text_color.clone() {
- base.text_color = value;
- }
- }
- }
- impl From<OptionalStyle> for Style
- where
- Style: Default,
- {
- fn from(wrapper: OptionalStyle) -> Self {
- let mut base = Self::default();
- wrapper.assign(&mut base);
- base
- }
- }
- impl Style {
- pub const DEFAULT: Style = Style {
- display: Display::DEFAULT,
- overflow: Point {
- x: Overflow::Visible,
- y: Overflow::Visible,
- },
- scrollbar_width: 0.0,
- position: Position::Relative,
- inset: Edges::auto(),
- margin: Edges::<Length>::zero(),
- padding: Edges::<DefinedLength>::zero(),
- border: Edges::<DefinedLength>::zero(),
- size: Size::auto(),
- min_size: Size::auto(),
- max_size: Size::auto(),
- aspect_ratio: None,
- gap: Size::zero(),
- align_items: None,
- align_self: None,
- align_content: None,
- justify_content: None,
- flex_direction: FlexDirection::Row,
- flex_wrap: FlexWrap::NoWrap,
- flex_grow: 0.0,
- flex_shrink: 1.0,
- flex_basis: Length::Auto,
- fill: None,
- text_color: None,
- };
- pub fn new() -> Self {
- Self::DEFAULT.clone()
- }
- pub fn to_taffy(&self, rem_size: f32) -> taffy::style::Style {
- taffy::style::Style {
- display: self.display,
- overflow: self.overflow.clone().into(),
- scrollbar_width: self.scrollbar_width,
- position: self.position,
- inset: self.inset.to_taffy(rem_size),
- size: self.size.to_taffy(rem_size),
- min_size: self.min_size.to_taffy(rem_size),
- max_size: self.max_size.to_taffy(rem_size),
- aspect_ratio: self.aspect_ratio,
- margin: self.margin.to_taffy(rem_size),
- padding: self.padding.to_taffy(rem_size),
- border: self.border.to_taffy(rem_size),
- align_items: self.align_items,
- align_self: self.align_self,
- align_content: self.align_content,
- justify_content: self.justify_content,
- gap: self.gap.to_taffy(rem_size),
- flex_direction: self.flex_direction,
- flex_wrap: self.flex_wrap,
- flex_basis: self.flex_basis.to_taffy(rem_size).into(),
- flex_grow: self.flex_grow,
- flex_shrink: self.flex_shrink,
- ..Default::default()
- }
- }
- }
- impl Default for Style {
- fn default() -> Self {
- Self::DEFAULT.clone()
- }
- }
- impl OptionalStyle {
- pub fn text_style(&self) -> Option<OptionalTextStyle> {
- self.text_color.map(|color| OptionalTextStyle { color })
- }
- }
- pub struct OptionalTextStyle {
- color: Option<Hsla>,
- }
- impl OptionalTextStyle {
- pub fn apply(&self, style: &mut gpui::fonts::TextStyle) {
- if let Some(color) = self.color {
- style.color = color.into();
- }
- }
- }
- pub enum Fill {
- Color(Hsla),
- }
- #[automatically_derived]
- impl ::core::clone::Clone for Fill {
- #[inline]
- fn clone(&self) -> Fill {
- match self {
- Fill::Color(__self_0) => Fill::Color(::core::clone::Clone::clone(__self_0)),
- }
- }
- }
- impl Fill {
- pub fn color(&self) -> Option<Hsla> {
- match self {
- Fill::Color(color) => Some(*color),
- }
- }
- }
- impl Default for Fill {
- fn default() -> Self {
- Self::Color(Hsla::default())
- }
- }
- impl From<Hsla> for Fill {
- fn from(color: Hsla) -> Self {
- Self::Color(color)
- }
- }
-}
-mod text {
- use crate::{
- element::{Element, ElementMetadata, EventHandler, IntoElement},
- style::Style,
- };
- use gpui::{geometry::Size, text_layout::LineLayout, RenderContext};
- use parking_lot::Mutex;
- use std::sync::Arc;
- impl<V: 'static, S: Into<ArcCow<'static, str>>> IntoElement<V> for S {
- type Element = Text<V>;
- fn into_element(self) -> Self::Element {
- Text {
- text: self.into(),
- metadata: Default::default(),
- }
- }
- }
- pub struct Text<V> {
- text: ArcCow<'static, str>,
- metadata: ElementMetadata<V>,
- }
- impl<V: 'static> Element<V> for Text<V> {
- type Layout = Arc<Mutex<Option<TextLayout>>>;
- fn declared_style(&mut self) -> &mut crate::style::OptionalStyle {
- &mut self.metadata.style
- }
- fn layout(
- &mut self,
- view: &mut V,
- cx: &mut gpui::LayoutContext<V>,
- ) -> anyhow::Result<(taffy::tree::NodeId, Self::Layout)> {
- let rem_size = cx.rem_pixels();
- let fonts = cx.platform().fonts();
- let text_style = cx.text_style();
- let line_height = cx.font_cache().line_height(text_style.font_size);
- let layout_engine = cx.layout_engine().expect("no layout engine present");
- let text = self.text.clone();
- let layout = Arc::new(Mutex::new(None));
- let style: Style = self.metadata.style.into();
- let node_id = layout_engine.add_measured_node(style.to_taffy(rem_size), {
- let layout = layout.clone();
- move |params| {
- let line_layout = fonts.layout_line(
- text.as_ref(),
- text_style.font_size,
- &[(text.len(), text_style.to_run())],
- );
- let size = Size {
- width: line_layout.width,
- height: line_height,
- };
- layout.lock().replace(TextLayout {
- line_layout: Arc::new(line_layout),
- line_height,
- });
- size
- }
- })?;
- Ok((node_id, layout))
- }
- fn paint<'a>(
- &mut self,
- layout: crate::element::Layout<Arc<Mutex<Option<TextLayout>>>>,
- view: &mut V,
- cx: &mut crate::element::PaintContext<V>,
- ) -> anyhow::Result<()> {
- let element_layout_lock = layout.from_element.lock();
- let element_layout = element_layout_lock
- .as_ref()
- .expect("layout has not been performed");
- let line_layout = element_layout.line_layout.clone();
- let line_height = element_layout.line_height;
- drop(element_layout_lock);
- let text_style = cx.text_style();
- let line = gpui::text_layout::Line::new(
- line_layout,
- &[(self.text.len(), text_style.to_run())],
- );
- line.paint(
- cx.scene,
- layout.from_engine.bounds.origin(),
- layout.from_engine.bounds,
- line_height,
- cx.legacy_cx,
- );
- Ok(())
- }
- fn handlers_mut(&mut self) -> &mut Vec<EventHandler<V>> {
- &mut self.metadata.handlers
- }
- }
- pub struct TextLayout {
- line_layout: Arc<LineLayout>,
- line_height: f32,
- }
- pub enum ArcCow<'a, T: ?Sized> {
- Borrowed(&'a T),
- Owned(Arc<T>),
- }
- impl<'a, T: ?Sized> Clone for ArcCow<'a, T> {
- fn clone(&self) -> Self {
- match self {
- Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
- Self::Owned(owned) => Self::Owned(owned.clone()),
- }
- }
- }
- impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
- fn from(s: &'a T) -> Self {
- Self::Borrowed(s)
- }
- }
- impl<T> From<Arc<T>> for ArcCow<'_, T> {
- fn from(s: Arc<T>) -> Self {
- Self::Owned(s)
- }
- }
- impl From<String> for ArcCow<'_, str> {
- fn from(value: String) -> Self {
- Self::Owned(value.into())
- }
- }
- impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
- type Target = T;
- fn deref(&self) -> &Self::Target {
- match self {
- ArcCow::Borrowed(s) => s,
- ArcCow::Owned(s) => s.as_ref(),
- }
- }
- }
- impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
- fn as_ref(&self) -> &T {
- match self {
- ArcCow::Borrowed(borrowed) => borrowed,
- ArcCow::Owned(owned) => owned.as_ref(),
- }
- }
- }
-}
-mod themes {
- use crate::color::{Hsla, Lerp};
- use std::ops::Range;
- pub mod rose_pine {
- use crate::{
- color::{hsla, rgb, Hsla},
- ThemeColors,
- };
- use std::ops::Range;
- pub struct RosePineThemes {
- pub default: RosePinePalette,
- pub dawn: RosePinePalette,
- pub moon: RosePinePalette,
- }
- pub struct RosePinePalette {
- pub base: Hsla,
- pub surface: Hsla,
- pub overlay: Hsla,
- pub muted: Hsla,
- pub subtle: Hsla,
- pub text: Hsla,
- pub love: Hsla,
- pub gold: Hsla,
- pub rose: Hsla,
- pub pine: Hsla,
- pub foam: Hsla,
- pub iris: Hsla,
- pub highlight_low: Hsla,
- pub highlight_med: Hsla,
- pub highlight_high: Hsla,
- }
- #[automatically_derived]
- impl ::core::clone::Clone for RosePinePalette {
- #[inline]
- fn clone(&self) -> RosePinePalette {
- let _: ::core::clone::AssertParamIsClone<Hsla>;
- *self
- }
- }
- #[automatically_derived]
- impl ::core::marker::Copy for RosePinePalette {}
- #[automatically_derived]
- impl ::core::fmt::Debug for RosePinePalette {
- fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
- let names: &'static _ = &[
- "base",
- "surface",
- "overlay",
- "muted",
- "subtle",
- "text",
- "love",
- "gold",
- "rose",
- "pine",
- "foam",
- "iris",
- "highlight_low",
- "highlight_med",
- "highlight_high",
- ];
- let values: &[&dyn::core::fmt::Debug] = &[
- &self.base,
- &self.surface,
- &self.overlay,
- &self.muted,
- &self.subtle,
- &self.text,
- &self.love,
- &self.gold,
- &self.rose,
- &self.pine,
- &self.foam,
- &self.iris,
- &self.highlight_low,
- &self.highlight_med,
- &&self.highlight_high,
- ];
- ::core::fmt::Formatter::debug_struct_fields_finish(
- f,
- "RosePinePalette",
- names,
- values,
- )
- }
- }
- impl RosePinePalette {
- pub fn default() -> RosePinePalette {
- RosePinePalette {
- base: rgb(0x191724),
- surface: rgb(0x1f1d2e),
- overlay: rgb(0x26233a),
- muted: rgb(0x6e6a86),
- subtle: rgb(0x908caa),
- text: rgb(0xe0def4),
- love: rgb(0xeb6f92),
- gold: rgb(0xf6c177),
- rose: rgb(0xebbcba),
- pine: rgb(0x31748f),
- foam: rgb(0x9ccfd8),
- iris: rgb(0xc4a7e7),
- highlight_low: rgb(0x21202e),
- highlight_med: rgb(0x403d52),
- highlight_high: rgb(0x524f67),
- }
- }
- pub fn moon() -> RosePinePalette {
- RosePinePalette {
- base: rgb(0x232136),
- surface: rgb(0x2a273f),
- overlay: rgb(0x393552),
- muted: rgb(0x6e6a86),
- subtle: rgb(0x908caa),
- text: rgb(0xe0def4),
- love: rgb(0xeb6f92),
- gold: rgb(0xf6c177),
- rose: rgb(0xea9a97),
- pine: rgb(0x3e8fb0),
- foam: rgb(0x9ccfd8),
- iris: rgb(0xc4a7e7),
- highlight_low: rgb(0x2a283e),
- highlight_med: rgb(0x44415a),
- highlight_high: rgb(0x56526e),
- }
- }
- pub fn dawn() -> RosePinePalette {
- RosePinePalette {
- base: rgb(0xfaf4ed),
- surface: rgb(0xfffaf3),
- overlay: rgb(0xf2e9e1),
- muted: rgb(0x9893a5),
- subtle: rgb(0x797593),
- text: rgb(0x575279),
- love: rgb(0xb4637a),
- gold: rgb(0xea9d34),
- rose: rgb(0xd7827e),
- pine: rgb(0x286983),
- foam: rgb(0x56949f),
- iris: rgb(0x907aa9),
- highlight_low: rgb(0xf4ede8),
- highlight_med: rgb(0xdfdad9),
- highlight_high: rgb(0xcecacd),
- }
- }
- }
- pub fn default() -> ThemeColors {
- theme_colors(&RosePinePalette::default())
- }
- pub fn moon() -> ThemeColors {
- theme_colors(&RosePinePalette::moon())
- }
- pub fn dawn() -> ThemeColors {
- theme_colors(&RosePinePalette::dawn())
- }
- fn theme_colors(p: &RosePinePalette) -> ThemeColors {
- ThemeColors {
- base: scale_sl(p.base, (0.8, 0.8), (1.2, 1.2)),
- surface: scale_sl(p.surface, (0.8, 0.8), (1.2, 1.2)),
- overlay: scale_sl(p.overlay, (0.8, 0.8), (1.2, 1.2)),
- muted: scale_sl(p.muted, (0.8, 0.8), (1.2, 1.2)),
- subtle: scale_sl(p.subtle, (0.8, 0.8), (1.2, 1.2)),
- text: scale_sl(p.text, (0.8, 0.8), (1.2, 1.2)),
- highlight_low: scale_sl(p.highlight_low, (0.8, 0.8), (1.2, 1.2)),
- highlight_med: scale_sl(p.highlight_med, (0.8, 0.8), (1.2, 1.2)),
- highlight_high: scale_sl(p.highlight_high, (0.8, 0.8), (1.2, 1.2)),
- success: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
- warning: scale_sl(p.gold, (0.8, 0.8), (1.2, 1.2)),
- error: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
- inserted: scale_sl(p.foam, (0.8, 0.8), (1.2, 1.2)),
- deleted: scale_sl(p.love, (0.8, 0.8), (1.2, 1.2)),
- modified: scale_sl(p.rose, (0.8, 0.8), (1.2, 1.2)),
- }
- }
- /// Produces a range by multiplying the saturation and lightness of the base color by the given
- /// start and end factors.
- fn scale_sl(
- base: Hsla,
- (start_s, start_l): (f32, f32),
- (end_s, end_l): (f32, f32),
- ) -> Range<Hsla> {
- let start = hsla(base.h, base.s * start_s, base.l * start_l, base.a);
- let end = hsla(base.h, base.s * end_s, base.l * end_l, base.a);
- Range { start, end }
- }
- }
- pub struct ThemeColors {
- pub base: Range<Hsla>,
- pub surface: Range<Hsla>,
- pub overlay: Range<Hsla>,
- pub muted: Range<Hsla>,
- pub subtle: Range<Hsla>,
- pub text: Range<Hsla>,
- pub highlight_low: Range<Hsla>,
- pub highlight_med: Range<Hsla>,
- pub highlight_high: Range<Hsla>,
- pub success: Range<Hsla>,
- pub warning: Range<Hsla>,
- pub error: Range<Hsla>,
- pub inserted: Range<Hsla>,
- pub deleted: Range<Hsla>,
- pub modified: Range<Hsla>,
- }
- impl ThemeColors {
- pub fn base(&self, level: f32) -> Hsla {
- self.base.lerp(level)
- }
- pub fn surface(&self, level: f32) -> Hsla {
- self.surface.lerp(level)
- }
- pub fn overlay(&self, level: f32) -> Hsla {
- self.overlay.lerp(level)
- }
- pub fn muted(&self, level: f32) -> Hsla {
- self.muted.lerp(level)
- }
- pub fn subtle(&self, level: f32) -> Hsla {
- self.subtle.lerp(level)
- }
- pub fn text(&self, level: f32) -> Hsla {
- self.text.lerp(level)
- }
- pub fn highlight_low(&self, level: f32) -> Hsla {
- self.highlight_low.lerp(level)
- }
- pub fn highlight_med(&self, level: f32) -> Hsla {
- self.highlight_med.lerp(level)
- }
- pub fn highlight_high(&self, level: f32) -> Hsla {
- self.highlight_high.lerp(level)
- }
- pub fn success(&self, level: f32) -> Hsla {
- self.success.lerp(level)
- }
- pub fn warning(&self, level: f32) -> Hsla {
- self.warning.lerp(level)
- }
- pub fn error(&self, level: f32) -> Hsla {
- self.error.lerp(level)
- }
- pub fn inserted(&self, level: f32) -> Hsla {
- self.inserted.lerp(level)
- }
- pub fn deleted(&self, level: f32) -> Hsla {
- self.deleted.lerp(level)
- }
- pub fn modified(&self, level: f32) -> Hsla {
- self.modified.lerp(level)
- }
- }
-}
-mod view {
- use crate::element::{AnyElement, Element};
- use gpui::{Element as _, ViewContext};
- pub fn view<F, E>(mut render: F) -> ViewFn
- where
- F: 'static + FnMut(&mut ViewContext<ViewFn>) -> E,
- E: Element<ViewFn>,
- {
- ViewFn(Box::new(move |cx| (render)(cx).into_any()))
- }
- pub struct ViewFn(Box<dyn FnMut(&mut ViewContext<ViewFn>) -> AnyElement<ViewFn>>);
- impl gpui::Entity for ViewFn {
- type Event = ();
- }
- impl gpui::View for ViewFn {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
- (self.0)(cx).adapt().into_any()
- }
- }
-}
-fn main() {
- SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");
- gpui::App::new(()).unwrap().run(|cx| {
- cx.add_window(
- WindowOptions {
- bounds: gpui::platform::WindowBounds::Fixed(RectF::new(
- vec2f(0., 0.),
- vec2f(400., 300.),
- )),
- center: true,
- ..Default::default()
- },
- |_| view(|_| storybook(&rose_pine::moon())),
- );
- cx.platform().activate(true);
- });
-}
-fn storybook<V: 'static>(theme: &ThemeColors) -> impl Element<V> {
- frame()
- .text_color(black())
- .h_full()
- .w_half()
- .fill(theme.success(0.5))
- .child(button().label("Hello").click(|_, _, _| {
- ::std::io::_print(format_args!("click!\n"));
- }))
-}