Merge branch 'main' into project_search2

This commit is contained in:
Piotr Osiewicz 2023-12-11 16:47:48 +01:00
commit ddbd5cf2cb
145 changed files with 16798 additions and 12900 deletions

View file

@ -81,12 +81,11 @@ jobs:
- name: Limit target directory size - name: Limit target directory size
run: script/clear-target-dir-if-larger-than 100 run: script/clear-target-dir-if-larger-than 100
- name: Set release channel to nightly, add nightly prefix to the final version - name: Set release channel to nightly
run: | run: |
set -eu set -eu
version=$(git rev-parse --short HEAD) version=$(git rev-parse --short HEAD)
echo "Publishing version: ${version} on release channel nightly" echo "Publishing version: ${version} on release channel nightly"
sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml
echo "nightly" > crates/zed/RELEASE_CHANNEL echo "nightly" > crates/zed/RELEASE_CHANNEL
- name: Generate license file - name: Generate license file

43
Cargo.lock generated
View file

@ -312,6 +312,15 @@ version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "approx"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "arrayref" name = "arrayref"
version = "0.3.7" version = "0.3.7"
@ -1145,6 +1154,7 @@ dependencies = [
"gpui2", "gpui2",
"itertools 0.10.5", "itertools 0.10.5",
"language2", "language2",
"outline2",
"project2", "project2",
"search2", "search2",
"settings2", "settings2",
@ -1743,7 +1753,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.30.0" version = "0.30.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -3162,6 +3172,12 @@ dependencies = [
"regex", "regex",
] ]
[[package]]
name = "fast-srgb8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -6263,6 +6279,28 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "palette"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc"
dependencies = [
"approx",
"fast-srgb8",
"palette_derive",
]
[[package]]
name = "palette_derive"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.37",
]
[[package]] [[package]]
name = "parity-tokio-ipc" name = "parity-tokio-ipc"
version = "0.9.0" version = "0.9.0"
@ -9696,6 +9734,7 @@ dependencies = [
"indexmap 1.9.3", "indexmap 1.9.3",
"json_comments", "json_comments",
"log", "log",
"palette",
"rust-embed", "rust-embed",
"serde", "serde",
"simplelog", "simplelog",
@ -10464,7 +10503,7 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter-vue" name = "tree-sitter-vue"
version = "0.0.1" version = "0.0.1"
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58" source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
dependencies = [ dependencies = [
"cc", "cc",
"tree-sitter", "tree-sitter",

View file

@ -205,7 +205,7 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
tree-sitter-lua = "0.0.14" tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
[patch.crates-io] [patch.crates-io]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,5 +17,8 @@
"file_name": "rose-pine-dawn.json", "file_name": "rose-pine-dawn.json",
"appearance": "light" "appearance": "light"
} }
] ],
"syntax": {
"function": ["entity.name"]
}
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -14,7 +14,7 @@ use ui::h_stack;
use util::ResultExt; use util::ResultExt;
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
actions!(ShowErrorMessage); actions!(activity_indicator, [ShowErrorMessage]);
const DOWNLOAD_ICON: &str = "icons/download.svg"; const DOWNLOAD_ICON: &str = "icons/download.svg";
const WARNING_ICON: &str = "icons/warning.svg"; const WARNING_ICON: &str = "icons/warning.svg";

View file

@ -12,23 +12,26 @@ use chrono::{DateTime, Local};
use collections::HashMap; use collections::HashMap;
use fs::Fs; use fs::Fs;
use futures::StreamExt; use futures::StreamExt;
use gpui::{actions, AppContext}; use gpui::{actions, AppContext, SharedString};
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
use util::paths::CONVERSATIONS_DIR; use util::paths::CONVERSATIONS_DIR;
actions!( actions!(
NewConversation, assistant,
Assist, [
Split, NewConversation,
CycleMessageRole, Assist,
QuoteSelection, Split,
ToggleFocus, CycleMessageRole,
ResetKey, QuoteSelection,
InlineAssist, ToggleFocus,
ToggleIncludeConversation, ResetKey,
ToggleRetrieveContext, InlineAssist,
ToggleIncludeConversation,
ToggleRetrieveContext,
]
); );
#[derive( #[derive(
@ -47,7 +50,7 @@ struct MessageMetadata {
enum MessageStatus { enum MessageStatus {
Pending, Pending,
Done, Done,
Error(Arc<str>), Error(SharedString),
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]

View file

@ -1628,8 +1628,9 @@ impl Conversation {
metadata.status = MessageStatus::Done; metadata.status = MessageStatus::Done;
} }
Err(error) => { Err(error) => {
metadata.status = metadata.status = MessageStatus::Error(SharedString::from(
MessageStatus::Error(error.to_string().trim().into()); error.to_string().trim().to_string(),
));
} }
} }
cx.notify(); cx.notify();
@ -2273,7 +2274,7 @@ impl ConversationEditor {
Some( Some(
div() div()
.id("error") .id("error")
.tooltip(move |cx| Tooltip::text(&error, cx)) .tooltip(move |cx| Tooltip::text(error.clone(), cx))
.child(IconElement::new(Icon::XCircle)), .child(IconElement::new(Icon::XCircle)),
) )
} else { } else {

View file

@ -26,10 +26,13 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
//todo!(remove CheckThatAutoUpdaterWorks) //todo!(remove CheckThatAutoUpdaterWorks)
actions!( actions!(
Check, auto_update,
DismissErrorMessage, [
ViewReleaseNotes, Check,
CheckThatAutoUpdaterWorks DismissErrorMessage,
ViewReleaseNotes,
CheckThatAutoUpdaterWorks
]
); );
#[derive(Serialize)] #[derive(Serialize)]
@ -85,15 +88,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
AutoUpdateSetting::register(cx); AutoUpdateSetting::register(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| { cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace workspace.register_action(|_, action: &Check, cx| check(action, cx));
.register_action(|_, action: &Check, cx| check(action, cx))
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
cx.spawn(|_, _cx| async move {
prompt.await.ok();
})
.detach();
});
// @nate - code to trigger update notification on launch // @nate - code to trigger update notification on launch
// workspace.show_notification(0, _cx, |cx| { // workspace.show_notification(0, _cx, |cx| {
@ -130,9 +125,15 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
} }
} }
pub fn check(_: &Check, cx: &mut AppContext) { pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
if let Some(updater) = AutoUpdater::get(cx) { if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx)); updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
&["Ok"],
));
} }
} }

View file

@ -19,7 +19,7 @@ search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" } theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" } workspace = { package = "workspace2", path = "../workspace2" }
# outline = { path = "../outline" } outline = { package = "outline2", path = "../outline2" }
itertools = "0.10" itertools = "0.10"
[dev-dependencies] [dev-dependencies]

View file

@ -1,13 +1,14 @@
use editor::Editor;
use gpui::{ use gpui::{
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
ViewContext, WeakView, ViewContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{prelude::*, ButtonLike, ButtonStyle, Label}; use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
use workspace::{ use workspace::{
item::{ItemEvent, ItemHandle}, item::{ItemEvent, ItemHandle},
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
}; };
pub enum Event { pub enum Event {
@ -18,16 +19,14 @@ pub struct Breadcrumbs {
pane_focused: bool, pane_focused: bool,
active_item: Option<Box<dyn ItemHandle>>, active_item: Option<Box<dyn ItemHandle>>,
subscription: Option<Subscription>, subscription: Option<Subscription>,
_workspace: WeakView<Workspace>,
} }
impl Breadcrumbs { impl Breadcrumbs {
pub fn new(workspace: &Workspace) -> Self { pub fn new() -> Self {
Self { Self {
pane_focused: false, pane_focused: false,
active_item: Default::default(), active_item: Default::default(),
subscription: Default::default(), subscription: Default::default(),
_workspace: workspace.weak_handle(),
} }
} }
} }
@ -62,97 +61,24 @@ impl Render for Breadcrumbs {
Label::new("").into_any_element() Label::new("").into_any_element()
}); });
let editor = active_item
.downcast::<Editor>()
.map(|editor| editor.downgrade());
element.child( element.child(
ButtonLike::new("toggle outline view") ButtonLike::new("toggle outline view")
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.child(h_stack().gap_1().children(breadcrumbs)) .child(h_stack().gap_1().children(breadcrumbs))
// We disable the button when it is not focused .on_click(move |_, cx| {
// due to ... @julia what was the reason again? if let Some(editor) = editor.as_ref().and_then(|editor| editor.upgrade()) {
.disabled(!self.pane_focused) outline::toggle(editor, &outline::Toggle, cx)
.on_click(move |_, _cx| { }
todo!("outline::toggle"); })
// this.update(cx, |this, cx| { .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
// if let Some(workspace) = this.workspace.upgrade() {
// workspace.update(cx, |_workspace, _cx| {
// outline::toggle(workspace, &Default::default(), cx)
// })
// }
// })
// .ok();
}),
) )
} }
} }
// 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 { impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item( fn set_active_pane_item(
&mut self, &mut self,

View file

@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
use db::RELEASE_CHANNEL; use db::RELEASE_CHANNEL;
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel, AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
WeakModel,
}; };
use rpc::{ use rpc::{
proto::{self, ChannelVisibility}, proto::{self, ChannelVisibility},
@ -46,7 +47,7 @@ pub struct ChannelStore {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Channel { pub struct Channel {
pub id: ChannelId, pub id: ChannelId,
pub name: String, pub name: SharedString,
pub visibility: proto::ChannelVisibility, pub visibility: proto::ChannelVisibility,
pub role: proto::ChannelRole, pub role: proto::ChannelRole,
pub unseen_note_version: Option<(u64, clock::Global)>, pub unseen_note_version: Option<(u64, clock::Global)>,
@ -895,14 +896,16 @@ impl ChannelStore {
.channel_invitations .channel_invitations
.binary_search_by_key(&channel.id, |c| c.id) .binary_search_by_key(&channel.id, |c| c.id)
{ {
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name, Ok(ix) => {
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
}
Err(ix) => self.channel_invitations.insert( Err(ix) => self.channel_invitations.insert(
ix, ix,
Arc::new(Channel { Arc::new(Channel {
id: channel.id, id: channel.id,
visibility: channel.visibility(), visibility: channel.visibility(),
role: channel.role(), role: channel.role(),
name: channel.name, name: channel.name.into(),
unseen_note_version: None, unseen_note_version: None,
unseen_message_id: None, unseen_message_id: None,
parent_path: channel.parent_path, parent_path: channel.parent_path,

View file

@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
existing_channel.visibility = channel_proto.visibility(); existing_channel.visibility = channel_proto.visibility();
existing_channel.role = channel_proto.role(); existing_channel.role = channel_proto.role();
existing_channel.name = channel_proto.name; existing_channel.name = channel_proto.name.into();
} else { } else {
self.channels_by_id.insert( self.channels_by_id.insert(
channel_proto.id, channel_proto.id,
@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
id: channel_proto.id, id: channel_proto.id,
visibility: channel_proto.visibility(), visibility: channel_proto.visibility(),
role: channel_proto.role(), role: channel_proto.role(),
name: channel_proto.name, name: channel_proto.name.into(),
unseen_note_version: None, unseen_note_version: None,
unseen_message_id: None, unseen_message_id: None,
parent_path: channel_proto.parent_path, parent_path: channel_proto.parent_path,
@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
let (parent_path, name) = channels_by_id let (parent_path, name) = channels_by_id
.get(&id) .get(&id)
.map_or((&[] as &[_], None), |channel| { .map_or((&[] as &[_], None), |channel| {
(channel.parent_path.as_slice(), Some(channel.name.as_str())) (channel.parent_path.as_slice(), Some(channel.name.as_ref()))
}); });
parent_path parent_path
.iter() .iter()
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str())) .filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
.chain(name) .chain(name)
} }

View file

@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
actions!(SignIn, SignOut, Reconnect); actions!(client, [SignIn, SignOut, Reconnect]);
pub fn init_settings(cx: &mut AppContext) { pub fn init_settings(cx: &mut AppContext) {
TelemetrySettings::register(cx); TelemetrySettings::register(cx);

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.30.0" version = "0.30.1"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -116,12 +116,13 @@ struct CreateUserResponse {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct Panic { struct Panic {
version: String, version: String,
release_channel: String,
text: String, text: String,
} }
#[instrument(skip(panic))] #[instrument(skip(panic))]
async fn trace_panic(panic: Json<Panic>) -> Result<()> { async fn trace_panic(panic: Json<Panic>) -> Result<()> {
tracing::error!(version = %panic.version, text = %panic.text, "panic report"); tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report");
Ok(()) Ok(())
} }

View file

@ -7,7 +7,7 @@ use call::ActiveCall;
use channel::{ChannelId, ChannelMembership, ChannelStore}; use channel::{ChannelId, ChannelMembership, ChannelStore};
use client::User; use client::User;
use futures::future::try_join_all; use futures::future::try_join_all;
use gpui::{BackgroundExecutor, Model, TestAppContext}; use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
use rpc::{ use rpc::{
proto::{self, ChannelRole}, proto::{self, ChannelRole},
RECEIVE_TIMEOUT, RECEIVE_TIMEOUT,
@ -46,13 +46,13 @@ async fn test_core_channels(
&[ &[
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".into(),
depth: 1, depth: 1,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
@ -92,7 +92,7 @@ async fn test_core_channels(
cx_b, cx_b,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
@ -140,13 +140,13 @@ async fn test_core_channels(
&[ &[
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
@ -168,19 +168,19 @@ async fn test_core_channels(
&[ &[
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
depth: 0, depth: 0,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
depth: 1, depth: 1,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".to_string(), name: "channel-c".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
depth: 2, depth: 2,
}, },
@ -211,19 +211,19 @@ async fn test_core_channels(
&[ &[
ExpectedChannel { ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_b_id, id: channel_b_id,
name: "channel-b".to_string(), name: "channel-b".into(),
depth: 1, depth: 1,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
id: channel_c_id, id: channel_c_id,
name: "channel-c".to_string(), name: "channel-c".into(),
depth: 2, depth: 2,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
@ -245,7 +245,7 @@ async fn test_core_channels(
cx_a, cx_a,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}], }],
@ -255,7 +255,7 @@ async fn test_core_channels(
cx_b, cx_b,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}], }],
@ -278,7 +278,7 @@ async fn test_core_channels(
cx_a, cx_a,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a".to_string(), name: "channel-a".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}], }],
@ -309,7 +309,7 @@ async fn test_core_channels(
cx_a, cx_a,
&[ExpectedChannel { &[ExpectedChannel {
id: channel_a_id, id: channel_a_id,
name: "channel-a-renamed".to_string(), name: "channel-a-renamed".into(),
depth: 0, depth: 0,
role: ChannelRole::Admin, role: ChannelRole::Admin,
}], }],
@ -418,7 +418,7 @@ async fn test_channel_room(
cx_b, cx_b,
&[ExpectedChannel { &[ExpectedChannel {
id: zed_id, id: zed_id,
name: "zed".to_string(), name: "zed".into(),
depth: 0, depth: 0,
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel { &[ExpectedChannel {
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".to_string(), name: "rust".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
); );
@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
&[ExpectedChannel { &[ExpectedChannel {
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust".to_string(), name: "rust".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
); );
@ -747,7 +747,7 @@ async fn test_channel_rename(
&[ExpectedChannel { &[ExpectedChannel {
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".to_string(), name: "rust-archive".into(),
role: ChannelRole::Admin, role: ChannelRole::Admin,
}], }],
); );
@ -759,7 +759,7 @@ async fn test_channel_rename(
&[ExpectedChannel { &[ExpectedChannel {
depth: 0, depth: 0,
id: rust_id, id: rust_id,
name: "rust-archive".to_string(), name: "rust-archive".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
); );
@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
&[ExpectedChannel { &[ExpectedChannel {
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}], }],
); );
@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
ExpectedChannel { ExpectedChannel {
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".into(),
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".to_string(), name: "subchannel".into(),
role: ChannelRole::Admin, role: ChannelRole::Admin,
}, },
], ],
@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
ExpectedChannel { ExpectedChannel {
depth: 0, depth: 0,
id: channel_id, id: channel_id,
name: "x".to_string(), name: "x".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: subchannel_id, id: subchannel_id,
name: "subchannel".to_string(), name: "subchannel".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}, },
], ],
@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel { ExpectedChannel {
depth: 0, depth: 0,
id: zed_channel, id: zed_channel,
name: "zed".to_string(), name: "zed".into(),
role: ChannelRole::Guest, role: ChannelRole::Guest,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: vim_channel, id: vim_channel,
name: "vim".to_string(), name: "vim".into(),
role: ChannelRole::Member, role: ChannelRole::Member,
}, },
], ],
@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
ExpectedChannel { ExpectedChannel {
depth: 0, depth: 0,
id: zed_channel, id: zed_channel,
name: "zed".to_string(), name: "zed".into(),
role: ChannelRole::Guest, role: ChannelRole::Guest,
}, },
ExpectedChannel { ExpectedChannel {
depth: 1, depth: 1,
id: vim_channel, id: vim_channel,
name: "vim".to_string(), name: "vim".into(),
role: ChannelRole::Guest, role: ChannelRole::Guest,
}, },
], ],
@ -1476,7 +1476,7 @@ async fn test_channel_moving(
struct ExpectedChannel { struct ExpectedChannel {
depth: usize, depth: usize,
id: ChannelId, id: ChannelId,
name: String, name: SharedString,
role: ChannelRole, role: ChannelRole,
} }
@ -1515,7 +1515,7 @@ fn assert_channels(
.ordered_channels() .ordered_channels()
.map(|(depth, channel)| ExpectedChannel { .map(|(depth, channel)| ExpectedChannel {
depth, depth,
name: channel.name.clone(), name: channel.name.clone().into(),
id: channel.id, id: channel.id,
role: channel.role, role: channel.role,
}) })

View file

@ -3,7 +3,7 @@ use crate::db::ChannelRole;
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan}; use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use gpui::{BackgroundExecutor, TestAppContext}; use gpui::{BackgroundExecutor, SharedString, TestAppContext};
use rand::prelude::*; use rand::prelude::*;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::{ use std::{
@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
enum ChannelBufferOperation { enum ChannelBufferOperation {
JoinChannelNotes { JoinChannelNotes {
channel_name: String, channel_name: SharedString,
}, },
LeaveChannelNotes { LeaveChannelNotes {
channel_name: String, channel_name: SharedString,
}, },
EditChannelNotes { EditChannelNotes {
channel_name: String, channel_name: SharedString,
edits: Vec<(Range<usize>, Arc<str>)>, edits: Vec<(Range<usize>, Arc<str>)>,
}, },
Noop, Noop,

View file

@ -26,7 +26,7 @@ use workspace::{
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
}; };
actions!(Deploy); actions!(collab, [Deploy]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
register_followable_item::<ChannelView>(cx) register_followable_item::<ChannelView>(cx)

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,14 @@ use client::UserId;
use collections::HashMap; use collections::HashMap;
use editor::{AnchorRangeExt, Editor}; use editor::{AnchorRangeExt, Editor};
use gpui::{ use gpui::{
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View, AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, WeakView,
}; };
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use project::search::SearchQuery; use project::search::SearchQuery;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use workspace::item::ItemHandle;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -19,8 +20,8 @@ lazy_static! {
} }
pub struct MessageEditor { pub struct MessageEditor {
pub editor: ViewHandle<Editor>, pub editor: View<Editor>,
channel_store: ModelHandle<ChannelStore>, channel_store: Model<ChannelStore>,
users: HashMap<String, UserId>, users: HashMap<String, UserId>,
mentions: Vec<UserId>, mentions: Vec<UserId>,
mentions_task: Option<Task<()>>, mentions_task: Option<Task<()>>,
@ -30,8 +31,8 @@ pub struct MessageEditor {
impl MessageEditor { impl MessageEditor {
pub fn new( pub fn new(
language_registry: Arc<LanguageRegistry>, language_registry: Arc<LanguageRegistry>,
channel_store: ModelHandle<ChannelStore>, channel_store: Model<ChannelStore>,
editor: ViewHandle<Editor>, editor: View<Editor>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
@ -48,15 +49,13 @@ impl MessageEditor {
cx.subscribe(&buffer, Self::on_buffer_event).detach(); cx.subscribe(&buffer, Self::on_buffer_event).detach();
let markdown = language_registry.language_for_name("Markdown"); let markdown = language_registry.language_for_name("Markdown");
cx.app_context() cx.spawn(|_, mut cx| async move {
.spawn(|mut cx| async move { let markdown = markdown.await?;
let markdown = markdown.await?; buffer.update(&mut cx, |buffer, cx| {
buffer.update(&mut cx, |buffer, cx| { buffer.set_language(Some(markdown), cx)
buffer.set_language(Some(markdown), cx)
});
anyhow::Ok(())
}) })
.detach_and_log_err(cx); })
.detach_and_log_err(cx);
Self { Self {
editor, editor,
@ -71,7 +70,7 @@ impl MessageEditor {
pub fn set_channel( pub fn set_channel(
&mut self, &mut self,
channel_id: u64, channel_id: u64,
channel_name: Option<String>, channel_name: Option<SharedString>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
@ -132,26 +131,28 @@ impl MessageEditor {
fn on_buffer_event( fn on_buffer_event(
&mut self, &mut self,
buffer: ModelHandle<Buffer>, buffer: Model<Buffer>,
event: &language::Event, event: &language::Event,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if let language::Event::Reparsed | language::Event::Edited = event { if let language::Event::Reparsed | language::Event::Edited = event {
let buffer = buffer.read(cx).snapshot(); let buffer = buffer.read(cx).snapshot();
self.mentions_task = Some(cx.spawn(|this, cx| async move { self.mentions_task = Some(cx.spawn(|this, cx| async move {
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await; cx.background_executor()
.timer(MENTIONS_DEBOUNCE_INTERVAL)
.await;
Self::find_mentions(this, buffer, cx).await; Self::find_mentions(this, buffer, cx).await;
})); }));
} }
} }
async fn find_mentions( async fn find_mentions(
this: WeakViewHandle<MessageEditor>, this: WeakView<MessageEditor>,
buffer: BufferSnapshot, buffer: BufferSnapshot,
mut cx: AsyncAppContext, mut cx: AsyncWindowContext,
) { ) {
let (buffer, ranges) = cx let (buffer, ranges) = cx
.background() .background_executor()
.spawn(async move { .spawn(async move {
let ranges = MENTIONS_SEARCH.search(&buffer, None).await; let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
(buffer, ranges) (buffer, ranges)
@ -180,11 +181,7 @@ impl MessageEditor {
} }
editor.clear_highlights::<Self>(cx); editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>( editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
anchor_ranges,
theme::current(cx).chat_panel.rich_text.mention_highlight,
cx,
)
}); });
this.mentions = mentioned_user_ids; this.mentions = mentioned_user_ids;
@ -192,21 +189,17 @@ impl MessageEditor {
}) })
.ok(); .ok();
} }
}
impl Entity for MessageEditor { pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
type Event = (); self.editor.read(cx).focus_handle(cx)
}
impl View for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
ChildView::new(&self.editor, cx).into_any()
} }
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) { impl Render for MessageEditor {
if cx.is_self_focused() { type Element = AnyView;
cx.focus(&self.editor);
} fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
self.editor.to_any()
} }
} }
@ -214,7 +207,7 @@ impl View for MessageEditor {
mod tests { mod tests {
use super::*; use super::*;
use client::{Client, User, UserStore}; use client::{Client, User, UserStore};
use gpui::{TestAppContext, WindowHandle}; use gpui::{Context as _, TestAppContext, VisualContext as _};
use language::{Language, LanguageConfig}; use language::{Language, LanguageConfig};
use rpc::proto; use rpc::proto;
use settings::SettingsStore; use settings::SettingsStore;
@ -222,8 +215,17 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_message_editor(cx: &mut TestAppContext) { async fn test_message_editor(cx: &mut TestAppContext) {
let editor = init_test(cx); let language_registry = init_test(cx);
let editor = editor.root(cx);
let (editor, cx) = cx.add_window_view(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.build_view(|cx| Editor::auto_height(4, cx)),
cx,
)
});
cx.executor().run_until_parked();
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
editor.set_members( editor.set_members(
@ -255,7 +257,7 @@ mod tests {
}); });
}); });
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL); cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false); let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
@ -269,15 +271,14 @@ mod tests {
}); });
} }
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> { fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
cx.foreground().forbid_parking();
cx.update(|cx| { cx.update(|cx| {
let http = FakeHttpClient::with_404_response(); let http = FakeHttpClient::with_404_response();
let client = Client::new(http.clone(), cx); let client = Client::new(http.clone(), cx);
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
cx.set_global(SettingsStore::test(cx)); let settings = SettingsStore::test(cx);
theme::init((), cx); cx.set_global(settings);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx); language::init(cx);
editor::init(cx); editor::init(cx);
client::init(&client, cx); client::init(&client, cx);
@ -292,16 +293,6 @@ mod tests {
}, },
Some(tree_sitter_markdown::language()), Some(tree_sitter_markdown::language()),
))); )));
language_registry
let editor = cx.add_window(|cx| {
MessageEditor::new(
language_registry,
ChannelStore::global(cx),
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
cx,
)
});
cx.foreground().run_until_parked();
editor
} }
} }

View file

@ -92,7 +92,7 @@ use theme::{ActiveTheme, ThemeSettings};
// channel_id: ChannelId, // channel_id: ChannelId,
// } // }
#[derive(Action, PartialEq, Debug, Clone, Serialize, Deserialize)] #[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct OpenChannelNotes { pub struct OpenChannelNotes {
pub channel_id: ChannelId, pub channel_id: ChannelId,
} }
@ -122,15 +122,20 @@ pub struct OpenChannelNotes {
// to: ChannelId, // to: ChannelId,
// } // }
impl_actions!(collab_panel, [OpenChannelNotes]);
actions!( actions!(
ToggleFocus, collab_panel,
Remove, [
Secondary, ToggleFocus,
CollapseSelectedChannel, Remove,
ExpandSelectedChannel, Secondary,
StartMoveChannel, CollapseSelectedChannel,
MoveSelected, ExpandSelectedChannel,
InsertSpace, StartMoveChannel,
MoveSelected,
InsertSpace,
]
); );
// impl_actions!( // impl_actions!(
@ -169,12 +174,12 @@ use editor::Editor;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action, actions, canvas, div, img, impl_actions, overlay, point, prelude::*, px, rems, serde_json,
AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div,
FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement,
MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render,
ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task, View,
VisualContext, WeakView, ViewContext, VisualContext, WeakView,
}; };
use project::{Fs, Project}; use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -192,6 +197,7 @@ use workspace::{
}; };
use crate::channel_view::ChannelView; use crate::channel_view::ChannelView;
use crate::chat_panel::ChatPanel;
use crate::{face_pile::FacePile, CollaborationPanelSettings}; use crate::{face_pile::FacePile, CollaborationPanelSettings};
use self::channel_modal::ChannelModal; use self::channel_modal::ChannelModal;
@ -852,7 +858,7 @@ impl CollabPanel {
.extend(channel_store.ordered_channels().enumerate().map( .extend(channel_store.ordered_channels().enumerate().map(
|(ix, (_, channel))| StringMatchCandidate { |(ix, (_, channel))| StringMatchCandidate {
id: ix, id: ix,
string: channel.name.clone(), string: channel.name.clone().into(),
char_bag: channel.name.chars().collect(), char_bag: channel.name.chars().collect(),
}, },
)); ));
@ -2102,14 +2108,13 @@ impl CollabPanel {
}; };
cx.window_context().defer(move |cx| { cx.window_context().defer(move |cx| {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
todo!(); if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) {
// if let Some(panel) = workspace.focus_panel::<ChatPanel>(cx) { panel.update(cx, |panel, cx| {
// panel.update(cx, |panel, cx| { panel
// panel .select_channel(channel_id, None, cx)
// .select_channel(channel_id, None, cx) .detach_and_log_err(cx);
// .detach_and_log_err(cx); });
// }); }
// }
}); });
}); });
} }
@ -2262,7 +2267,7 @@ impl CollabPanel {
} }
}; };
Some(channel.name.as_str()) Some(channel.name.as_ref())
}); });
if let Some(name) = channel_name { if let Some(name) = channel_name {
@ -2603,9 +2608,14 @@ impl CollabPanel {
Color::Default Color::Default
} else { } else {
Color::Muted Color::Muted
})
.on_click(cx.listener(move |this, _, cx| {
this.join_channel_chat(channel_id, cx)
}))
.tooltip(|cx| {
Tooltip::text("Open channel chat", cx)
}), }),
) ),
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
) )
.child( .child(
div() div()

View file

@ -13,12 +13,16 @@ use picker::{Picker, PickerDelegate};
use std::sync::Arc; use std::sync::Arc;
use ui::prelude::*; use ui::prelude::*;
use util::TryFutureExt; use util::TryFutureExt;
use workspace::ModalView;
actions!( actions!(
SelectNextControl, channel_modal,
ToggleMode, [
ToggleMemberAdmin, SelectNextControl,
RemoveMember ToggleMode,
ToggleMemberAdmin,
RemoveMember
]
); );
// pub fn init(cx: &mut AppContext) { // pub fn init(cx: &mut AppContext) {
@ -140,6 +144,7 @@ impl ChannelModal {
} }
impl EventEmitter<DismissEvent> for ChannelModal {} impl EventEmitter<DismissEvent> for ChannelModal {}
impl ModalView for ChannelModal {}
impl FocusableView for ChannelModal { impl FocusableView for ChannelModal {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {

View file

@ -9,6 +9,7 @@ use std::sync::Arc;
use theme::ActiveTheme as _; use theme::ActiveTheme as _;
use ui::prelude::*; use ui::prelude::*;
use util::{ResultExt as _, TryFutureExt}; use util::{ResultExt as _, TryFutureExt};
use workspace::ModalView;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
//Picker::<ContactFinderDelegate>::init(cx); //Picker::<ContactFinderDelegate>::init(cx);
@ -95,6 +96,7 @@ pub struct ContactFinderDelegate {
} }
impl EventEmitter<DismissEvent> for ContactFinder {} impl EventEmitter<DismissEvent> for ContactFinder {}
impl ModalView for ContactFinder {}
impl FocusableView for ContactFinder { impl FocusableView for ContactFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ use std::{rc::Rc, sync::Arc};
use call::{report_call_event_for_room, ActiveCall, Room}; use call::{report_call_event_for_room, ActiveCall, Room};
pub use collab_panel::CollabPanel; pub use collab_panel::CollabPanel;
pub use collab_titlebar_item::CollabTitlebarItem; pub use collab_titlebar_item::CollabTitlebarItem;
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
use gpui::{ use gpui::{
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds, actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
WindowKind, WindowOptions, WindowKind, WindowOptions,
@ -23,7 +24,10 @@ use settings::Settings;
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall); actions!(
collab,
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx); CollaborationPanelSettings::register(cx);
@ -34,7 +38,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
collab_titlebar_item::init(cx); collab_titlebar_item::init(cx);
collab_panel::init(cx); collab_panel::init(cx);
channel_view::init(cx); channel_view::init(cx);
// chat_panel::init(cx); chat_panel::init(cx);
notifications::init(&app_state, cx); notifications::init(&app_state, cx);
// cx.add_global_action(toggle_screen_sharing); // cx.add_global_action(toggle_screen_sharing);
@ -157,6 +161,6 @@ fn notification_window_options(
// .into_any() // .into_any()
// } // }
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool { fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>() cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
// } }

View file

@ -1,8 +1,9 @@
use gpui::{ use gpui::{
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext, div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
WindowContext,
}; };
#[derive(Default)] #[derive(Default, IntoElement)]
pub struct FacePile { pub struct FacePile {
pub faces: Vec<AnyElement>, pub faces: Vec<AnyElement>,
} }
@ -15,64 +16,15 @@ impl RenderOnce for FacePile {
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
let isnt_last = ix < player_count - 1; let isnt_last = ix < player_count - 1;
div().when(isnt_last, |div| div.neg_mr_1()).child(player) div()
.z_index((player_count - ix) as u32)
.when(isnt_last, |div| div.neg_mr_1())
.child(player)
}); });
div().p_1().flex().items_center().children(player_list) div().p_1().flex().items_center().children(player_list)
} }
} }
// impl Element for FacePile {
// type State = ();
// fn layout(
// &mut self,
// state: Option<Self::State>,
// cx: &mut WindowContext,
// ) -> (LayoutId, Self::State) {
// let mut width = 0.;
// let mut max_height = 0.;
// let mut faces = Vec::with_capacity(self.faces.len());
// for face in &mut self.faces {
// let layout = face.layout(cx);
// width += layout.x();
// max_height = f32::max(max_height, layout.y());
// faces.push(layout);
// }
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
// (cx.request_layout(&Style::default(), faces), ())
// // (
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
// // (),
// // ))
// }
// fn paint(
// &mut self,
// bounds: RectF,
// visible_bounds: RectF,
// _layout: &mut Self::LayoutState,
// view: &mut V,
// cx: &mut ViewContext<V>,
// ) -> Self::PaintState {
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
// let origin_y = bounds.upper_right().y();
// let mut origin_x = bounds.upper_right().x();
// for face in self.faces.iter_mut().rev() {
// let size = face.size();
// origin_x -= size.x();
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
// cx.scene().push_layer(None);
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
// cx.scene().pop_layer();
// origin_x += self.overlap;
// }
// ()
// }
// }
impl Extend<AnyElement> for FacePile { impl Extend<AnyElement> for FacePile {
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) { fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
self.faces.extend(children); self.faces.extend(children);

View file

@ -3,9 +3,9 @@ use std::sync::Arc;
use workspace::AppState; use workspace::AppState;
pub mod incoming_call_notification; pub mod incoming_call_notification;
// pub mod project_shared_notification; pub mod project_shared_notification;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
incoming_call_notification::init(app_state, cx); incoming_call_notification::init(app_state, cx);
//project_shared_notification::init(app_state, cx); project_shared_notification::init(app_state, cx);
} }

View file

@ -33,8 +33,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
height: px(64.), height: px(64.),
}; };
for window in unique_screens { for screen in unique_screens {
let options = notification_window_options(window, window_size); let options = notification_window_options(screen, window_size);
let window = cx let window = cx
.open_window(options, |cx| { .open_window(options, |cx| {
cx.build_view(|_| { cx.build_view(|_| {
@ -47,15 +47,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
.unwrap(); .unwrap();
notification_windows.push(window); notification_windows.push(window);
} }
// for screen in cx.platform().screens() {
// let window = cx
// .add_window(notification_window_options(screen, window_size), |_| {
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
// });
// notification_windows.push(window);
// }
} }
} }
}) })
@ -84,21 +75,22 @@ impl IncomingCallNotificationState {
let active_call = ActiveCall::global(cx); let active_call = ActiveCall::global(cx);
if accept { if accept {
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
let caller_user_id = self.call.calling_user.id;
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
let app_state = self.app_state.clone(); let app_state = self.app_state.clone();
let cx: &mut AppContext = cx; let cx: &mut AppContext = cx;
cx.spawn(|cx| async move { cx.spawn(|cx| async move {
join.await?; join.await?;
if let Some(_project_id) = initial_project_id { if let Some(project_id) = initial_project_id {
cx.update(|_cx| { cx.update(|cx| {
if let Some(_app_state) = app_state.upgrade() { if let Some(app_state) = app_state.upgrade() {
// workspace::join_remote_project( workspace::join_remote_project(
// project_id, project_id,
// caller_user_id, caller_user_id,
// app_state, app_state,
// cx, cx,
// ) )
// .detach_and_log_err(cx); .detach_and_log_err(cx);
} }
}) })
.log_err(); .log_err();
@ -138,135 +130,25 @@ impl IncomingCallNotification {
))) )))
.child(self.render_buttons(cx)), .child(self.render_buttons(cx)),
) )
// let theme = &theme::current(cx).incoming_call_notification;
// let default_project = proto::ParticipantProject::default();
// let initial_project = self
// .call
// .initial_project
// .as_ref()
// .unwrap_or(&default_project);
// Flex::row()
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
// Image::from_data(avatar)
// .with_style(theme.caller_avatar)
// .aligned()
// }))
// .with_child(
// Flex::column()
// .with_child(
// Label::new(
// self.call.calling_user.github_login.clone(),
// theme.caller_username.text.clone(),
// )
// .contained()
// .with_style(theme.caller_username.container),
// )
// .with_child(
// Label::new(
// format!(
// "is sharing a project in Zed{}",
// if initial_project.worktree_root_names.is_empty() {
// ""
// } else {
// ":"
// }
// ),
// theme.caller_message.text.clone(),
// )
// .contained()
// .with_style(theme.caller_message.container),
// )
// .with_children(if initial_project.worktree_root_names.is_empty() {
// None
// } else {
// Some(
// Label::new(
// initial_project.worktree_root_names.join(", "),
// theme.worktree_roots.text.clone(),
// )
// .contained()
// .with_style(theme.worktree_roots.container),
// )
// })
// .contained()
// .with_style(theme.caller_metadata)
// .aligned(),
// )
// .contained()
// .with_style(theme.caller_container)
// .flex(1., true)
// .into_any()
} }
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element { fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
h_stack() h_stack()
.child( .child(Button::new("accept", "Accept").render(cx).on_click({
Button::new("accept", "Accept") let state = self.state.clone();
.render(cx) move |_, cx| state.respond(true, cx)
// .bg(green()) }))
.on_click({ .child(Button::new("decline", "Decline").render(cx).on_click({
let state = self.state.clone(); let state = self.state.clone();
move |_, cx| state.respond(true, cx) move |_, cx| state.respond(false, cx)
}), }))
)
.child(
Button::new("decline", "Decline")
.render(cx)
// .bg(red())
.on_click({
let state = self.state.clone();
move |_, cx| state.respond(false, cx)
}),
)
// enum Accept {}
// enum Decline {}
// let theme = theme::current(cx);
// Flex::column()
// .with_child(
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
// let theme = &theme.incoming_call_notification;
// Label::new("Accept", theme.accept_button.text.clone())
// .aligned()
// .contained()
// .with_style(theme.accept_button.container)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, this, cx| {
// this.respond(true, cx);
// })
// .flex(1., true),
// )
// .with_child(
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
// let theme = &theme.incoming_call_notification;
// Label::new("Decline", theme.decline_button.text.clone())
// .aligned()
// .contained()
// .with_style(theme.decline_button.container)
// })
// .with_cursor_style(CursorStyle::PointingHand)
// .on_click(MouseButton::Left, |_, this, cx| {
// this.respond(false, cx);
// })
// .flex(1., true),
// )
// .constrained()
// .with_width(theme.incoming_call_notification.button_width)
// .into_any()
} }
} }
impl Render for IncomingCallNotification { impl Render for IncomingCallNotification {
type Element = Div; type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().bg(red()).flex_none().child(self.render_caller(cx)) div().bg(red()).flex_none().child(self.render_caller(cx))
// Flex::row()
// .with_child()
// .with_child(self.render_buttons(cx))
// .contained()
// .with_background_color(background)
// .expanded()
// .into_any()
} }
} }

View file

@ -3,12 +3,11 @@ use call::{room, ActiveCall};
use client::User; use client::User;
use collections::HashMap; use collections::HashMap;
use gpui::{ use gpui::{
elements::*, px, AppContext, Div, Element, ParentElement, Render, RenderOnce, Size, Styled, ViewContext,
geometry::vector::vec2f, VisualContext,
platform::{CursorStyle, MouseButton},
AppContext, Entity, View, ViewContext,
}; };
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use ui::{h_stack, v_stack, Avatar, Button, Clickable, Label};
use workspace::AppState; use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@ -21,38 +20,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
project_id, project_id,
worktree_root_names, worktree_root_names,
} => { } => {
let theme = &theme::current(cx).project_shared_notification; let window_size = Size {
let window_size = vec2f(theme.window_width, theme.window_height); width: px(380.),
height: px(64.),
};
for screen in cx.platform().screens() { for screen in cx.displays() {
let window = let options = notification_window_options(screen, window_size);
cx.add_window(notification_window_options(screen, window_size), |_| { let window = cx.open_window(options, |cx| {
cx.build_view(|_| {
ProjectSharedNotification::new( ProjectSharedNotification::new(
owner.clone(), owner.clone(),
*project_id, *project_id,
worktree_root_names.clone(), worktree_root_names.clone(),
app_state.clone(), app_state.clone(),
) )
}); })
});
notification_windows notification_windows
.entry(*project_id) .entry(*project_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.push(window); .push(window);
} }
} }
room::Event::RemoteProjectUnshared { project_id } room::Event::RemoteProjectUnshared { project_id }
| room::Event::RemoteProjectJoined { project_id } | room::Event::RemoteProjectJoined { project_id }
| room::Event::RemoteProjectInvitationDiscarded { project_id } => { | room::Event::RemoteProjectInvitationDiscarded { project_id } => {
if let Some(windows) = notification_windows.remove(&project_id) { if let Some(windows) = notification_windows.remove(&project_id) {
for window in windows { for window in windows {
window.remove(cx); window
.update(cx, |_, cx| {
// todo!()
cx.remove_window();
})
.ok();
} }
} }
} }
room::Event::Left => { room::Event::Left => {
for (_, windows) in notification_windows.drain() { for (_, windows) in notification_windows.drain() {
for window in windows { for window in windows {
window.remove(cx); window
.update(cx, |_, cx| {
// todo!()
cx.remove_window();
})
.ok();
} }
} }
} }
@ -102,116 +117,60 @@ impl ProjectSharedNotification {
} }
} }
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_owner(&self) -> impl Element {
let theme = &theme::current(cx).project_shared_notification; h_stack()
Flex::row() .children(
.with_children(self.owner.avatar.clone().map(|avatar| { self.owner
Image::from_data(avatar) .avatar
.with_style(theme.owner_avatar) .clone()
.aligned() .map(|avatar| Avatar::data(avatar.clone())),
})) )
.with_child( .child(
Flex::column() v_stack()
.with_child( .child(Label::new(self.owner.github_login.clone()))
Label::new( .child(Label::new(format!(
self.owner.github_login.clone(), "is sharing a project in Zed{}",
theme.owner_username.text.clone(), if self.worktree_root_names.is_empty() {
) ""
.contained() } else {
.with_style(theme.owner_username.container), ":"
) }
.with_child( )))
Label::new( .children(if self.worktree_root_names.is_empty() {
format!(
"is sharing a project in Zed{}",
if self.worktree_root_names.is_empty() {
""
} else {
":"
}
),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container),
)
.with_children(if self.worktree_root_names.is_empty() {
None None
} else { } else {
Some( Some(Label::new(self.worktree_root_names.join(", ")))
Label::new( }),
self.worktree_root_names.join(", "),
theme.worktree_roots.text.clone(),
)
.contained()
.with_style(theme.worktree_roots.container),
)
})
.contained()
.with_style(theme.owner_metadata)
.aligned(),
) )
.contained()
.with_style(theme.owner_container)
.flex(1., true)
.into_any()
} }
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> { fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
enum Open {} let this = cx.view().clone();
enum Dismiss {} v_stack()
.child(Button::new("open", "Open").render(cx).on_click({
let theme = theme::current(cx); let this = this.clone();
Flex::column() move |_, cx| {
.with_child( this.update(cx, |this, cx| this.join(cx));
MouseEventHandler::new::<Open, _>(0, cx, |_, _| { }
let theme = &theme.project_shared_notification; }))
Label::new("Open", theme.open_button.text.clone()) .child(
.aligned() Button::new("dismiss", "Dismiss")
.contained() .render(cx)
.with_style(theme.open_button.container) .on_click(move |_, cx| {
}) this.update(cx, |this, cx| this.dismiss(cx));
.with_cursor_style(CursorStyle::PointingHand) }),
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
.flex(1., true),
) )
.with_child(
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
let theme = &theme.project_shared_notification;
Label::new("Dismiss", theme.dismiss_button.text.clone())
.aligned()
.contained()
.with_style(theme.dismiss_button.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, this, cx| {
this.dismiss(cx);
})
.flex(1., true),
)
.constrained()
.with_width(theme.project_shared_notification.button_width)
.into_any()
} }
} }
impl Entity for ProjectSharedNotification { impl Render for ProjectSharedNotification {
type Event = (); type Element = Div;
}
impl View for ProjectSharedNotification { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
fn ui_name() -> &'static str { h_stack()
"ProjectSharedNotification" .size_full()
} .bg(gpui::red())
.child(self.render_owner())
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> { .child(self.render_buttons(cx))
let background = theme::current(cx).project_shared_notification.background;
Flex::row()
.with_child(self.render_owner(cx))
.with_child(self.render_buttons(cx))
.contained()
.with_background_color(background)
.expanded()
.into_any()
} }
} }

View file

@ -7,7 +7,7 @@ use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
@ -16,16 +16,18 @@ use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt, ResultExt,
}; };
use workspace::Workspace; use workspace::{ModalView, Workspace};
use zed_actions::OpenZedURL; use zed_actions::OpenZedURL;
actions!(Toggle); actions!(command_palette, [Toggle]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.set_global(HitCounts::default()); cx.set_global(HitCounts::default());
cx.observe_new_views(CommandPalette::register).detach(); cx.observe_new_views(CommandPalette::register).detach();
} }
impl ModalView for CommandPalette {}
pub struct CommandPalette { pub struct CommandPalette {
picker: View<Picker<CommandPaletteDelegate>>, picker: View<Picker<CommandPaletteDelegate>>,
} }
@ -47,7 +49,7 @@ impl CommandPalette {
.available_actions() .available_actions()
.into_iter() .into_iter()
.filter_map(|action| { .filter_map(|action| {
let name = gpui::remove_the_2(action.name()); let name = action.name();
let namespace = name.split("::").next().unwrap_or("malformed action name"); let namespace = name.split("::").next().unwrap_or("malformed action name");
if filter.is_some_and(|f| { if filter.is_some_and(|f| {
f.hidden_namespaces.contains(namespace) f.hidden_namespaces.contains(namespace)
@ -59,7 +61,6 @@ impl CommandPalette {
Some(Command { Some(Command {
name: humanize_action_name(&name), name: humanize_action_name(&name),
action, action,
keystrokes: vec![], // todo!()
}) })
}) })
.collect(); .collect();
@ -108,7 +109,6 @@ pub struct CommandPaletteDelegate {
struct Command { struct Command {
name: String, name: String,
action: Box<dyn Action>, action: Box<dyn Action>,
keystrokes: Vec<Keystroke>,
} }
impl Clone for Command { impl Clone for Command {
@ -116,7 +116,6 @@ impl Clone for Command {
Self { Self {
name: self.name.clone(), name: self.name.clone(),
action: self.action.boxed_clone(), action: self.action.boxed_clone(),
keystrokes: self.keystrokes.clone(),
} }
} }
} }
@ -227,6 +226,7 @@ impl PickerDelegate for CommandPaletteDelegate {
}) })
} }
} }
if let Some(CommandInterceptResult { if let Some(CommandInterceptResult {
action, action,
string, string,
@ -242,7 +242,6 @@ impl PickerDelegate for CommandPaletteDelegate {
commands.push(Command { commands.push(Command {
name: string.clone(), name: string.clone(),
action, action,
keystrokes: vec![],
}); });
matches.insert( matches.insert(
0, 0,
@ -254,6 +253,7 @@ impl PickerDelegate for CommandPaletteDelegate {
}, },
) )
} }
picker picker
.update(&mut cx, |picker, _| { .update(&mut cx, |picker, _| {
let delegate = &mut picker.delegate; let delegate = &mut picker.delegate;
@ -283,6 +283,8 @@ impl PickerDelegate for CommandPaletteDelegate {
} }
let action_ix = self.matches[self.selected_ix].candidate_id; let action_ix = self.matches[self.selected_ix].candidate_id;
let command = self.commands.swap_remove(action_ix); let command = self.commands.swap_remove(action_ix);
self.matches.clear();
self.commands.clear();
cx.update_global(|hit_counts: &mut HitCounts, _| { cx.update_global(|hit_counts: &mut HitCounts, _| {
*hit_counts.0.entry(command.name).or_default() += 1; *hit_counts.0.entry(command.name).or_default() += 1;
}); });
@ -298,13 +300,8 @@ impl PickerDelegate for CommandPaletteDelegate {
selected: bool, selected: bool,
cx: &mut ViewContext<Picker<Self>>, cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> { ) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else { let r#match = self.matches.get(ix)?;
return None; let command = self.commands.get(r#match.candidate_id)?;
};
let Some(command) = self.commands.get(r#match.candidate_id) else {
return None;
};
Some( Some(
ListItem::new(ix).inset(true).selected(selected).child( ListItem::new(ix).inset(true).selected(selected).child(
h_stack() h_stack()
@ -352,8 +349,7 @@ impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command") f.debug_struct("Command")
.field("name", &self.name) .field("name", &self.name)
.field("keystrokes", &self.keystrokes) .finish_non_exhaustive()
.finish()
} }
} }

View file

@ -34,12 +34,15 @@ use util::{
}; };
actions!( actions!(
Suggest, copilot,
NextSuggestion, [
PreviousSuggestion, Suggest,
Reinstall, NextSuggestion,
SignIn, PreviousSuggestion,
SignOut Reinstall,
SignIn,
SignOut
]
); );
pub fn init( pub fn init(

View file

@ -43,7 +43,7 @@ use workspace::{
ItemNavHistory, Pane, ToolbarItemLocation, Workspace, ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
}; };
actions!(Deploy, ToggleWarnings); actions!(diagnostics, [Deploy, ToggleWarnings]);
const CONTEXT_LINE_COUNT: u32 = 1; const CONTEXT_LINE_COUNT: u32 = 1;

View file

@ -12,6 +12,7 @@ mod link_go_to_definition;
mod mouse_context_menu; mod mouse_context_menu;
pub mod movement; pub mod movement;
mod persistence; mod persistence;
mod rust_analyzer_ext;
pub mod scroll; pub mod scroll;
pub mod selections_collection; pub mod selections_collection;
@ -300,6 +301,7 @@ actions!(
DeleteToEndOfLine, DeleteToEndOfLine,
CutToEndOfLine, CutToEndOfLine,
DuplicateLine, DuplicateLine,
ExpandMacroRecursively,
MoveLineUp, MoveLineUp,
MoveLineDown, MoveLineDown,
JoinLines, JoinLines,
@ -425,6 +427,8 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
init_settings(cx); init_settings(cx);
rust_analyzer_ext::apply_related_actions(cx);
cx.add_action(Editor::new_file); cx.add_action(Editor::new_file);
cx.add_action(Editor::new_file_in_direction); cx.add_action(Editor::new_file_in_direction);
cx.add_action(Editor::cancel); cx.add_action(Editor::cancel);

View file

@ -0,0 +1,98 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{AppContext, Task, ViewContext};
use language::Language;
use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{Editor, ExpandMacroRecursively};
pub fn apply_related_actions(cx: &mut AppContext) {
cx.add_async_action(expand_macro_recursively);
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
cx: &mut ViewContext<'_, '_, Editor>,
) -> Option<Task<anyhow::Result<()>>> {
if editor.selections.count() == 0 {
return None;
}
let project = editor.project.as_ref()?;
let workspace = editor.workspace(cx)?;
let multibuffer = editor.buffer().read(cx);
let (trigger_anchor, rust_language, server_to_query, buffer) = editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !is_rust_language(&rust_language) {
return None;
}
Some((trigger_anchor, rust_language, buffer))
})
.find_map(|(trigger_anchor, rust_language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.into_iter()
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == "rust-analyzer" {
Some((
trigger_anchor,
Arc::clone(&rust_language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})?;
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let expand_macro_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
ExpandMacro { position },
cx,
)
});
Some(cx.spawn(|_, mut cx| async move {
let macro_expansion = expand_macro_task.await.context("expand macro")?;
if macro_expansion.is_empty() {
log::info!("Empty macro expansion for position {position:?}");
return Ok(());
}
let buffer = project.update(&mut cx, |project, cx| {
project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx)
})?;
workspace.update(&mut cx, |workspace, cx| {
let buffer = cx.add_model(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
});
workspace.add_item(
Box::new(cx.add_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
cx,
);
});
anyhow::Ok(())
}))
}
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}

View file

@ -13,6 +13,7 @@ mod link_go_to_definition;
mod mouse_context_menu; mod mouse_context_menu;
pub mod movement; pub mod movement;
mod persistence; mod persistence;
mod rust_analyzer_ext;
pub mod scroll; pub mod scroll;
pub mod selections_collection; pub mod selections_collection;
@ -39,8 +40,8 @@ use futures::FutureExt;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display; use git::diff_hunk_to_display;
use gpui::{ use gpui::{
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action,
AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures,
FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model,
MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText,
@ -107,7 +108,7 @@ use ui::{
use ui::{prelude::*, IconSize}; use ui::{prelude::*, IconSize};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
item::{ItemEvent, ItemHandle}, item::{Item, ItemEvent, ItemHandle},
searchable::SearchEvent, searchable::SearchEvent,
ItemNavHistory, Pane, SplitDirection, ViewId, Workspace, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace,
}; };
@ -185,82 +186,101 @@ pub fn render_parsed_markdown(
}) })
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectNext { pub struct SelectNext {
#[serde(default)] #[serde(default)]
pub replace_newest: bool, pub replace_newest: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectPrevious { pub struct SelectPrevious {
#[serde(default)] #[serde(default)]
pub replace_newest: bool, pub replace_newest: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectAllMatches { pub struct SelectAllMatches {
#[serde(default)] #[serde(default)]
pub replace_newest: bool, pub replace_newest: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToBeginningOfLine { pub struct SelectToBeginningOfLine {
#[serde(default)] #[serde(default)]
stop_at_soft_wraps: bool, stop_at_soft_wraps: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageUp { pub struct MovePageUp {
#[serde(default)] #[serde(default)]
center_cursor: bool, center_cursor: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct MovePageDown { pub struct MovePageDown {
#[serde(default)] #[serde(default)]
center_cursor: bool, center_cursor: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct SelectToEndOfLine { pub struct SelectToEndOfLine {
#[serde(default)] #[serde(default)]
stop_at_soft_wraps: bool, stop_at_soft_wraps: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleCodeActions { pub struct ToggleCodeActions {
#[serde(default)] #[serde(default)]
pub deployed_from_indicator: bool, pub deployed_from_indicator: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCompletion { pub struct ConfirmCompletion {
#[serde(default)] #[serde(default)]
pub item_ix: Option<usize>, pub item_ix: Option<usize>,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ConfirmCodeAction { pub struct ConfirmCodeAction {
#[serde(default)] #[serde(default)]
pub item_ix: Option<usize>, pub item_ix: Option<usize>,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct ToggleComments { pub struct ToggleComments {
#[serde(default)] #[serde(default)]
pub advance_downwards: bool, pub advance_downwards: bool,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct FoldAt { pub struct FoldAt {
pub buffer_row: u32, pub buffer_row: u32,
} }
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize, Default)]
pub struct UnfoldAt { pub struct UnfoldAt {
pub buffer_row: u32, pub buffer_row: u32,
} }
impl_actions!(
editor,
[
SelectNext,
SelectPrevious,
SelectAllMatches,
SelectToBeginningOfLine,
MovePageUp,
MovePageDown,
SelectToEndOfLine,
ToggleCodeActions,
ConfirmCompletion,
ConfirmCodeAction,
ToggleComments,
FoldAt,
UnfoldAt
]
);
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum InlayId { pub enum InlayId {
Suggestion(usize), Suggestion(usize),
@ -277,121 +297,125 @@ impl InlayId {
} }
actions!( actions!(
AddSelectionAbove, editor,
AddSelectionBelow, [
Backspace, AddSelectionAbove,
Cancel, AddSelectionBelow,
ConfirmRename, Backspace,
ContextMenuFirst, Cancel,
ContextMenuLast, ConfirmRename,
ContextMenuNext, ContextMenuFirst,
ContextMenuPrev, ContextMenuLast,
ConvertToKebabCase, ContextMenuNext,
ConvertToLowerCamelCase, ContextMenuPrev,
ConvertToLowerCase, ConvertToKebabCase,
ConvertToSnakeCase, ConvertToLowerCamelCase,
ConvertToTitleCase, ConvertToLowerCase,
ConvertToUpperCamelCase, ConvertToSnakeCase,
ConvertToUpperCase, ConvertToTitleCase,
Copy, ConvertToUpperCamelCase,
CopyHighlightJson, ConvertToUpperCase,
CopyPath, Copy,
CopyRelativePath, CopyHighlightJson,
Cut, CopyPath,
CutToEndOfLine, CopyRelativePath,
Delete, Cut,
DeleteLine, CutToEndOfLine,
DeleteToBeginningOfLine, Delete,
DeleteToEndOfLine, DeleteLine,
DeleteToNextSubwordEnd, DeleteToBeginningOfLine,
DeleteToNextWordEnd, DeleteToEndOfLine,
DeleteToPreviousSubwordStart, DeleteToNextSubwordEnd,
DeleteToPreviousWordStart, DeleteToNextWordEnd,
DuplicateLine, DeleteToPreviousSubwordStart,
FindAllReferences, DeleteToPreviousWordStart,
Fold, DuplicateLine,
FoldSelectedRanges, ExpandMacroRecursively,
Format, FindAllReferences,
GoToDefinition, Fold,
GoToDefinitionSplit, FoldSelectedRanges,
GoToDiagnostic, Format,
GoToHunk, GoToDefinition,
GoToPrevDiagnostic, GoToDefinitionSplit,
GoToPrevHunk, GoToDiagnostic,
GoToTypeDefinition, GoToHunk,
GoToTypeDefinitionSplit, GoToPrevDiagnostic,
HalfPageDown, GoToPrevHunk,
HalfPageUp, GoToTypeDefinition,
Hover, GoToTypeDefinitionSplit,
Indent, HalfPageDown,
JoinLines, HalfPageUp,
LineDown, Hover,
LineUp, Indent,
MoveDown, JoinLines,
MoveLeft, LineDown,
MoveLineDown, LineUp,
MoveLineUp, MoveDown,
MoveRight, MoveLeft,
MoveToBeginning, MoveLineDown,
MoveToBeginningOfLine, MoveLineUp,
MoveToEnclosingBracket, MoveRight,
MoveToEnd, MoveToBeginning,
MoveToEndOfLine, MoveToBeginningOfLine,
MoveToEndOfParagraph, MoveToEnclosingBracket,
MoveToNextSubwordEnd, MoveToEnd,
MoveToNextWordEnd, MoveToEndOfLine,
MoveToPreviousSubwordStart, MoveToEndOfParagraph,
MoveToPreviousWordStart, MoveToNextSubwordEnd,
MoveToStartOfParagraph, MoveToNextWordEnd,
MoveUp, MoveToPreviousSubwordStart,
Newline, MoveToPreviousWordStart,
NewlineAbove, MoveToStartOfParagraph,
NewlineBelow, MoveUp,
NextScreen, Newline,
OpenExcerpts, NewlineAbove,
Outdent, NewlineBelow,
PageDown, NextScreen,
PageUp, OpenExcerpts,
Paste, Outdent,
Redo, PageDown,
RedoSelection, PageUp,
Rename, Paste,
RestartLanguageServer, Redo,
RevealInFinder, RedoSelection,
ReverseLines, Rename,
ScrollCursorBottom, RestartLanguageServer,
ScrollCursorCenter, RevealInFinder,
ScrollCursorTop, ReverseLines,
SelectAll, ScrollCursorBottom,
SelectDown, ScrollCursorCenter,
SelectLargerSyntaxNode, ScrollCursorTop,
SelectLeft, SelectAll,
SelectLine, SelectDown,
SelectRight, SelectLargerSyntaxNode,
SelectSmallerSyntaxNode, SelectLeft,
SelectToBeginning, SelectLine,
SelectToEnd, SelectRight,
SelectToEndOfParagraph, SelectSmallerSyntaxNode,
SelectToNextSubwordEnd, SelectToBeginning,
SelectToNextWordEnd, SelectToEnd,
SelectToPreviousSubwordStart, SelectToEndOfParagraph,
SelectToPreviousWordStart, SelectToNextSubwordEnd,
SelectToStartOfParagraph, SelectToNextWordEnd,
SelectUp, SelectToPreviousSubwordStart,
ShowCharacterPalette, SelectToPreviousWordStart,
ShowCompletions, SelectToStartOfParagraph,
ShuffleLines, SelectUp,
SortLinesCaseInsensitive, ShowCharacterPalette,
SortLinesCaseSensitive, ShowCompletions,
SplitSelectionIntoLines, ShuffleLines,
Tab, SortLinesCaseInsensitive,
TabPrev, SortLinesCaseSensitive,
ToggleInlayHints, SplitSelectionIntoLines,
ToggleSoftWrap, Tab,
Transpose, TabPrev,
Undo, ToggleInlayHints,
UndoSelection, ToggleSoftWrap,
UnfoldLines, Transpose,
Undo,
UndoSelection,
UnfoldLines,
]
); );
enum DocumentHighlightRead {} enum DocumentHighlightRead {}
@ -9319,7 +9343,6 @@ impl Render for Editor {
scrollbar_width: px(12.), scrollbar_width: px(12.),
syntax: cx.theme().syntax().clone(), syntax: cx.theme().syntax().clone(),
diagnostic_style: cx.theme().diagnostic_style(), diagnostic_style: cx.theme().diagnostic_style(),
// TODO kb find `HighlightStyle` usages
// todo!("what about the rest of the highlight style parts?") // todo!("what about the rest of the highlight style parts?")
inlays_style: HighlightStyle { inlays_style: HighlightStyle {
color: Some(cx.theme().status().hint), color: Some(cx.theme().status().hint),

View file

@ -32,7 +32,7 @@ use gpui::{
Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, Style, Styled, TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting; use language::{language_settings::ShowWhitespaceSetting, Language};
use multi_buffer::Anchor; use multi_buffer::Anchor;
use project::{ use project::{
project_settings::{GitGutterSetting, ProjectSettings}, project_settings::{GitGutterSetting, ProjectSettings},
@ -135,11 +135,13 @@ impl EditorElement {
fn register_actions(&self, cx: &mut WindowContext) { fn register_actions(&self, cx: &mut WindowContext) {
let view = &self.editor; let view = &self.editor;
self.editor.update(cx, |editor, cx| { view.update(cx, |editor, cx| {
for action in editor.editor_actions.iter() { for action in editor.editor_actions.iter() {
(action)(cx) (action)(cx)
} }
}); });
crate::rust_analyzer_ext::apply_related_actions(view, cx);
register_action(view, cx, Editor::move_left); register_action(view, cx, Editor::move_left);
register_action(view, cx, Editor::move_right); register_action(view, cx, Editor::move_right);
register_action(view, cx, Editor::move_down); register_action(view, cx, Editor::move_down);
@ -385,17 +387,17 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>, gutter_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder, stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) {
let mut click_count = event.click_count; let mut click_count = event.click_count;
let modifiers = event.modifiers; let modifiers = event.modifiers;
if gutter_bounds.contains_point(&event.position) { if gutter_bounds.contains_point(&event.position) {
click_count = 3; // Simulate triple-click when clicking the gutter to select lines click_count = 3; // Simulate triple-click when clicking the gutter to select lines
} else if !text_bounds.contains_point(&event.position) { } else if !text_bounds.contains_point(&event.position) {
return false; return;
} }
if !cx.was_top_layer(&event.position, stacking_order) { if !cx.was_top_layer(&event.position, stacking_order) {
return false; return;
} }
let point_for_position = position_map.point_for_position(text_bounds, event.position); let point_for_position = position_map.point_for_position(text_bounds, event.position);
@ -427,7 +429,7 @@ impl EditorElement {
); );
} }
true cx.stop_propagation();
} }
fn mouse_right_down( fn mouse_right_down(
@ -436,9 +438,9 @@ impl EditorElement {
position_map: &PositionMap, position_map: &PositionMap,
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) {
if !text_bounds.contains_point(&event.position) { if !text_bounds.contains_point(&event.position) {
return false; return;
} }
let point_for_position = position_map.point_for_position(text_bounds, event.position); let point_for_position = position_map.point_for_position(text_bounds, event.position);
mouse_context_menu::deploy_context_menu( mouse_context_menu::deploy_context_menu(
@ -447,7 +449,7 @@ impl EditorElement {
point_for_position.previous_valid, point_for_position.previous_valid,
cx, cx,
); );
true cx.stop_propagation();
} }
fn mouse_up( fn mouse_up(
@ -457,7 +459,7 @@ impl EditorElement {
text_bounds: Bounds<Pixels>, text_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder, stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) {
let end_selection = editor.has_pending_selection(); let end_selection = editor.has_pending_selection();
let pending_nonempty_selections = editor.has_pending_nonempty_selection(); let pending_nonempty_selections = editor.has_pending_nonempty_selection();
@ -479,10 +481,10 @@ impl EditorElement {
go_to_fetched_definition(editor, point, split, cx); go_to_fetched_definition(editor, point, split, cx);
} }
return true; cx.stop_propagation();
} else if end_selection {
cx.stop_propagation();
} }
end_selection
} }
fn mouse_moved( fn mouse_moved(
@ -493,7 +495,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>, gutter_bounds: Bounds<Pixels>,
stacking_order: &StackingOrder, stacking_order: &StackingOrder,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) {
let modifiers = event.modifiers; let modifiers = event.modifiers;
if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) {
let point_for_position = position_map.point_for_position(text_bounds, event.position); let point_for_position = position_map.point_for_position(text_bounds, event.position);
@ -562,11 +564,13 @@ impl EditorElement {
} }
} }
true cx.stop_propagation();
} else { } else {
update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx); update_go_to_definition_link(editor, None, modifiers.command, modifiers.shift, cx);
hover_at(editor, None, cx); hover_at(editor, None, cx);
gutter_hovered && was_top if gutter_hovered && was_top {
cx.stop_propagation();
}
} }
} }
@ -576,9 +580,9 @@ impl EditorElement {
position_map: &PositionMap, position_map: &PositionMap,
bounds: &InteractiveBounds, bounds: &InteractiveBounds,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> bool { ) {
if !bounds.visibly_contains(&event.position, cx) { if !bounds.visibly_contains(&event.position, cx) {
return false; return;
} }
let line_height = position_map.line_height; let line_height = position_map.line_height;
@ -602,8 +606,7 @@ impl EditorElement {
let y = f32::from((scroll_position.y * line_height - delta.y) / line_height); let y = f32::from((scroll_position.y * line_height - delta.y) / line_height);
let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max);
editor.scroll(scroll_position, axis, cx); editor.scroll(scroll_position, axis, cx);
cx.stop_propagation();
true
} }
fn paint_background( fn paint_background(
@ -749,44 +752,47 @@ impl EditorElement {
} }
} }
for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { cx.with_z_index(1, |cx| {
if let Some(mut fold_indicator) = fold_indicator { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
let mut fold_indicator = fold_indicator.into_any_element(); if let Some(mut fold_indicator) = fold_indicator {
let mut fold_indicator = fold_indicator.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_padding,
ix as f32 * line_height - (scroll_top % line_height),
);
let centering_offset = point(
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width)
/ 2.,
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
fold_indicator.draw(origin, available_space, cx);
}
}
if let Some(indicator) = layout.code_actions_indicator.take() {
let mut button = indicator.button.into_any_element();
let available_space = size( let available_space = size(
AvailableSpace::MinContent, AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55), AvailableSpace::Definite(line_height),
); );
let fold_indicator_size = fold_indicator.measure(available_space, cx); let indicator_size = button.measure(available_space, cx);
let position = point( let mut x = Pixels::ZERO;
bounds.size.width - layout.gutter_padding, let mut y = indicator.row as f32 * line_height - scroll_top;
ix as f32 * line_height - (scroll_top % line_height), // Center indicator.
); x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
let centering_offset = point( y += (line_height - indicator_size.height) / 2.;
(layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) / 2.,
(line_height - fold_indicator_size.height) / 2., button.draw(bounds.origin + point(x, y), available_space, cx);
);
let origin = bounds.origin + position + centering_offset;
fold_indicator.draw(origin, available_space, cx);
} }
} });
if let Some(indicator) = layout.code_actions_indicator.take() {
let mut button = indicator.button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
let indicator_size = button.measure(available_space, cx);
let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator.
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.;
button.draw(bounds.origin + point(x, y), available_space, cx);
}
} }
fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) { fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
@ -824,8 +830,8 @@ impl EditorElement {
}; };
let color = match status { let color = match status {
DiffHunkStatus::Added => gpui::green(), // todo!("use the appropriate color") DiffHunkStatus::Added => cx.theme().status().created,
DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the appropriate color") DiffHunkStatus::Modified => cx.theme().status().modified,
//TODO: This rendering is entirely a horrible hack //TODO: This rendering is entirely a horrible hack
DiffHunkStatus::Removed => { DiffHunkStatus::Removed => {
@ -842,7 +848,7 @@ impl EditorElement {
cx.paint_quad( cx.paint_quad(
highlight_bounds, highlight_bounds,
Corners::all(1. * line_height), Corners::all(1. * line_height),
gpui::red(), // todo!("use the right color") cx.theme().status().deleted,
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
); );
@ -1230,203 +1236,216 @@ impl EditorElement {
bounds.upper_right().x - self.style.scrollbar_width bounds.upper_right().x - self.style.scrollbar_width
} }
// fn paint_scrollbar( fn paint_scrollbar(
// &mut self, &mut self,
// bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
// layout: &mut LayoutState, layout: &mut LayoutState,
// editor: &Editor, cx: &mut WindowContext,
// cx: &mut ViewContext<Editor>, ) {
// ) { if layout.mode != EditorMode::Full {
// enum ScrollbarMouseHandlers {} return;
// if layout.mode != EditorMode::Full { }
// return;
// }
// let style = &self.style.theme.scrollbar; let top = bounds.origin.y;
let bottom = bounds.lower_left().y;
let right = bounds.lower_right().x;
let left = self.scrollbar_left(&bounds);
let row_range = layout.scrollbar_row_range.clone();
let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
// let top = bounds.min_y; let mut height = bounds.size.height;
// let bottom = bounds.max_y; let mut first_row_y_offset = px(0.0);
// let right = bounds.max_x;
// let left = self.scrollbar_left(&bounds);
// let row_range = &layout.scrollbar_row_range;
// let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
// let mut height = bounds.height(); // Impose a minimum height on the scrollbar thumb
// let mut first_row_y_offset = 0.0; let row_height = height / max_row;
let min_thumb_height = layout.position_map.line_height;
let thumb_height = (row_range.end - row_range.start) * row_height;
if thumb_height < min_thumb_height {
first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
height -= min_thumb_height - thumb_height;
}
// // Impose a minimum height on the scrollbar thumb let y_for_row = |row: f32| -> Pixels { top + first_row_y_offset + row * row_height };
// let row_height = height / max_row;
// let min_thumb_height =
// style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
// let thumb_height = (row_range.end - row_range.start) * row_height;
// if thumb_height < min_thumb_height {
// first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
// height -= min_thumb_height - thumb_height;
// }
// let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height }; let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
let track_bounds = Bounds::from_corners(point(left, top), point(right, bottom));
let thumb_bounds = Bounds::from_corners(point(left, thumb_top), point(right, thumb_bottom));
// let thumb_top = y_for_row(row_range.start) - first_row_y_offset; if layout.show_scrollbars {
// let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; cx.paint_quad(
// let track_bounds = Bounds::<Pixels>::from_points(point(left, top), point(right, bottom)); track_bounds,
// let thumb_bounds = Bounds::<Pixels>::from_points(point(left, thumb_top), point(right, thumb_bottom)); Corners::default(),
gpui::blue(), // todo!("style.track.background_color")
Edges::default(), // todo!("style.track.border")
transparent_black(), // todo!("style.track.border")
);
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
if layout.is_singleton && scrollbar_settings.selections {
let start_anchor = Anchor::min();
let end_anchor = Anchor::max();
let background_ranges = self
.editor
.read(cx)
.background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
start_anchor..end_anchor,
&layout.position_map.snapshot,
50000,
);
for range in background_ranges {
let start_y = y_for_row(range.start().row() as f32);
let mut end_y = y_for_row(range.end().row() as f32);
if end_y - start_y < px(1.) {
end_y = start_y + px(1.);
}
let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
cx.paint_quad(
bounds,
Corners::default(),
gpui::yellow(), // todo!("theme.editor.scrollbar")
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
gpui::green(), // todo!("style.thumb.border.color")
);
}
}
// if layout.show_scrollbars { if layout.is_singleton && scrollbar_settings.git_diff {
// cx.paint_quad(Quad { for hunk in layout
// bounds: track_bounds, .position_map
// border: style.track.border.into(), .snapshot
// background: style.track.background_color, .buffer_snapshot
// ..Default::default() .git_diff_hunks_in_range(0..(max_row.floor() as u32))
// }); {
// let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar; let start_display = Point::new(hunk.buffer_range.start, 0)
// let theme = theme::current(cx); .to_display_point(&layout.position_map.snapshot.display_snapshot);
// let scrollbar_theme = &theme.editor.scrollbar; let end_display = Point::new(hunk.buffer_range.end, 0)
// if layout.is_singleton && scrollbar_settings.selections { .to_display_point(&layout.position_map.snapshot.display_snapshot);
// let start_anchor = Anchor::min(); let start_y = y_for_row(start_display.row() as f32);
// let end_anchor = Anchor::max; let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
// let color = scrollbar_theme.selections; y_for_row((end_display.row() + 1) as f32)
// let border = Border { } else {
// width: 1., y_for_row((end_display.row()) as f32)
// color: style.thumb.border.color, };
// overlay: false,
// top: false,
// right: true,
// bottom: false,
// left: true,
// };
// let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
// let start_y = y_for_row(start.row() as f32);
// let mut end_y = y_for_row(end.row() as f32);
// if end_y - start_y < 1. {
// end_y = start_y + 1.;
// }
// let bounds = Bounds::<Pixels>::from_points(point(left, start_y), point(right, end_y));
// cx.paint_quad(Quad { if end_y - start_y < px(1.) {
// bounds, end_y = start_y + px(1.);
// background: Some(color), }
// border: border.into(), let bounds = Bounds::from_corners(point(left, start_y), point(right, end_y));
// corner_radii: style.thumb.corner_radii.into(),
// })
// };
// let background_ranges = editor
// .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
// start_anchor..end_anchor,
// &layout.position_map.snapshot,
// 50000,
// );
// for row in background_ranges {
// let start = row.start();
// let end = row.end();
// push_region(*start, *end);
// }
// }
// if layout.is_singleton && scrollbar_settings.git_diff { let color = match hunk.status() {
// let diff_style = scrollbar_theme.git.clone(); DiffHunkStatus::Added => gpui::green(), // todo!("use the right color")
// for hunk in layout DiffHunkStatus::Modified => gpui::yellow(), // todo!("use the right color")
// .position_map DiffHunkStatus::Removed => gpui::red(), // todo!("use the right color")
// .snapshot };
// .buffer_snapshot cx.paint_quad(
// .git_diff_hunks_in_range(0..(max_row.floor() as u32)) bounds,
// { Corners::default(),
// let start_display = Point::new(hunk.buffer_range.start, 0) color,
// .to_display_point(&layout.position_map.snapshot.display_snapshot); Edges {
// let end_display = Point::new(hunk.buffer_range.end, 0) top: Pixels::ZERO,
// .to_display_point(&layout.position_map.snapshot.display_snapshot); right: px(1.),
// let start_y = y_for_row(start_display.row() as f32); bottom: Pixels::ZERO,
// let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end { left: px(1.),
// y_for_row((end_display.row() + 1) as f32) },
// } else { gpui::green(), // todo!("style.thumb.border.color")
// y_for_row((end_display.row()) as f32) );
// }; }
}
// if end_y - start_y < 1. { cx.paint_quad(
// end_y = start_y + 1.; thumb_bounds,
// } Corners::default(),
// let bounds = Bounds::<Pixels>::from_points(point(left, start_y), point(right, end_y)); gpui::black(), // todo!("style.thumb.background_color")
Edges {
top: Pixels::ZERO,
right: px(1.),
bottom: Pixels::ZERO,
left: px(1.),
},
gpui::green(), // todo!("style.thumb.border.color")
);
}
// let color = match hunk.status() { let mouse_position = cx.mouse_position();
// DiffHunkStatus::Added => diff_style.inserted, if track_bounds.contains_point(&mouse_position) {
// DiffHunkStatus::Modified => diff_style.modified, cx.set_cursor_style(CursorStyle::Arrow);
// DiffHunkStatus::Removed => diff_style.deleted, }
// };
// let border = Border { cx.on_mouse_event({
// width: 1., let editor = self.editor.clone();
// color: style.thumb.border.color, move |event: &MouseMoveEvent, phase, cx| {
// overlay: false, if phase == DispatchPhase::Capture {
// top: false, return;
// right: true, }
// bottom: false,
// left: true,
// };
// cx.paint_quad(Quad { editor.update(cx, |editor, cx| {
// bounds, if event.pressed_button == Some(MouseButton::Left)
// background: Some(color), && editor.scroll_manager.is_dragging_scrollbar()
// border: border.into(), {
// corner_radii: style.thumb.corner_radii.into(), let y = mouse_position.y;
// }) let new_y = event.position.y;
// } if thumb_top < y && y < thumb_bottom {
// } let mut position = editor.scroll_position(cx);
position.y += (new_y - y) * (max_row as f32) / height;
if position.y < 0.0 {
position.y = 0.0;
}
editor.set_scroll_position(position, cx);
}
cx.stop_propagation();
} else {
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
if track_bounds.contains_point(&event.position) {
editor.scroll_manager.show_scrollbar(cx);
}
}
})
}
});
// cx.paint_quad(Quad { if self.editor.read(cx).scroll_manager.is_dragging_scrollbar() {
// bounds: thumb_bounds, cx.on_mouse_event({
// border: style.thumb.border.into(), let editor = self.editor.clone();
// background: style.thumb.background_color, move |event: &MouseUpEvent, phase, cx| {
// corner_radii: style.thumb.corner_radii.into(), editor.update(cx, |editor, cx| {
// }); editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
// } cx.stop_propagation();
});
}
});
} else {
cx.on_mouse_event({
let editor = self.editor.clone();
move |event: &MouseDownEvent, phase, cx| {
editor.update(cx, |editor, cx| {
if track_bounds.contains_point(&event.position) {
editor.scroll_manager.set_is_dragging_scrollbar(true, cx);
// cx.scene().push_cursor_region(CursorRegion { let y = event.position.y;
// bounds: track_bounds, if y < thumb_top || thumb_bottom < y {
// style: CursorStyle::Arrow, let center_row =
// }); ((y - top) * max_row as f32 / height).round() as u32;
// let region_id = cx.view_id(); let top_row = center_row
// cx.scene().push_mouse_region( .saturating_sub((row_range.end - row_range.start) as u32 / 2);
// MouseRegion::new::<ScrollbarMouseHandlers>(region_id, region_id, track_bounds) let mut position = editor.scroll_position(cx);
// .on_move(move |event, editor: &mut Editor, cx| { position.y = top_row as f32;
// if event.pressed_button.is_none() { editor.set_scroll_position(position, cx);
// editor.scroll_manager.show_scrollbar(cx); } else {
// } editor.scroll_manager.show_scrollbar(cx);
// }) }
// .on_down(MouseButton::Left, {
// let row_range = row_range.clone();
// move |event, editor: &mut Editor, cx| {
// let y = event.position.y;
// if y < thumb_top || thumb_bottom < y {
// let center_row = ((y - top) * max_row as f32 / height).round() as u32;
// let top_row = center_row
// .saturating_sub((row_range.end - row_range.start) as u32 / 2);
// let mut position = editor.scroll_position(cx);
// position.set_y(top_row as f32);
// editor.set_scroll_position(position, cx);
// } else {
// editor.scroll_manager.show_scrollbar(cx);
// }
// }
// })
// .on_drag(MouseButton::Left, {
// move |event, editor: &mut Editor, cx| {
// if event.end {
// return;
// }
// let y = event.prev_mouse_position.y; cx.stop_propagation();
// let new_y = event.position.y; }
// if thumb_top < y && y < thumb_bottom { });
// let mut position = editor.scroll_position(cx); }
// position.set_y(position.y + (new_y - y) * (max_row as f32) / height); });
// if position.y < 0.0 { }
// position.set_y(0.); }
// }
// editor.set_scroll_position(position, cx);
// }
// }
// }),
// );
// }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn paint_highlighted_range( fn paint_highlighted_range(
@ -2452,12 +2471,9 @@ impl EditorElement {
return; return;
} }
let handled = editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
Self::scroll(editor, event, &position_map, &interactive_bounds, cx) Self::scroll(editor, event, &position_map, &interactive_bounds, cx)
}); });
if handled {
cx.stop_propagation();
}
} }
}); });
@ -2471,7 +2487,7 @@ impl EditorElement {
return; return;
} }
let handled = match event.button { match event.button {
MouseButton::Left => editor.update(cx, |editor, cx| { MouseButton::Left => editor.update(cx, |editor, cx| {
Self::mouse_left_down( Self::mouse_left_down(
editor, editor,
@ -2486,12 +2502,8 @@ impl EditorElement {
MouseButton::Right => editor.update(cx, |editor, cx| { MouseButton::Right => editor.update(cx, |editor, cx| {
Self::mouse_right_down(editor, event, &position_map, text_bounds, cx) Self::mouse_right_down(editor, event, &position_map, text_bounds, cx)
}), }),
_ => false, _ => {}
}; };
if handled {
cx.stop_propagation()
}
} }
}); });
@ -2501,7 +2513,7 @@ impl EditorElement {
let stacking_order = cx.stacking_order().clone(); let stacking_order = cx.stacking_order().clone();
move |event: &MouseUpEvent, phase, cx| { move |event: &MouseUpEvent, phase, cx| {
let handled = editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
Self::mouse_up( Self::mouse_up(
editor, editor,
event, event,
@ -2511,10 +2523,6 @@ impl EditorElement {
cx, cx,
) )
}); });
if handled {
cx.stop_propagation()
}
} }
}); });
cx.on_mouse_event({ cx.on_mouse_event({
@ -2527,7 +2535,7 @@ impl EditorElement {
return; return;
} }
let stop_propogating = editor.update(cx, |editor, cx| { editor.update(cx, |editor, cx| {
Self::mouse_moved( Self::mouse_moved(
editor, editor,
event, event,
@ -2538,10 +2546,6 @@ impl EditorElement {
cx, cx,
) )
}); });
if stop_propogating {
cx.stop_propagation()
}
} }
}); });
} }
@ -2840,9 +2844,11 @@ impl Element for EditorElement {
cx.with_z_index(1, |cx| { cx.with_z_index(1, |cx| {
cx.with_element_id(Some("editor_blocks"), |cx| { cx.with_element_id(Some("editor_blocks"), |cx| {
self.paint_blocks(bounds, &mut layout, cx); self.paint_blocks(bounds, &mut layout, cx);
}) });
}) })
} }
cx.with_z_index(2, |cx| self.paint_scrollbar(bounds, &mut layout, cx));
}); });
}); });
}) })
@ -2944,7 +2950,7 @@ impl PositionMap {
) -> PointForPosition { ) -> PointForPosition {
let scroll_position = self.snapshot.scroll_position(); let scroll_position = self.snapshot.scroll_position();
let position = position - text_bounds.origin; let position = position - text_bounds.origin;
let y = position.y.max(px(0.)).min(self.size.width); let y = position.y.max(px(0.)).min(self.size.height);
let x = position.x + (scroll_position.x * self.em_width); let x = position.x + (scroll_position.x * self.em_width);
let row = (f32::from(y / self.line_height) + scroll_position.y) as u32; let row = (f32::from(y / self.line_height) + scroll_position.y) as u32;

View file

@ -26,7 +26,7 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.);
pub const HOVER_POPOVER_GAP: Pixels = px(10.); pub const HOVER_POPOVER_GAP: Pixels = px(10.);
actions!(Hover); actions!(editor, [Hover]);
/// Bindable action which uses the most recent selection head to trigger a hover /// Bindable action which uses the most recent selection head to trigger a hover
pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) { pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {

View file

@ -0,0 +1,119 @@
use std::sync::Arc;
use anyhow::Context as _;
use gpui::{Context, Model, View, ViewContext, VisualContext, WindowContext};
use language::Language;
use multi_buffer::MultiBuffer;
use project::lsp_ext_command::ExpandMacro;
use text::ToPointUtf16;
use crate::{element::register_action, Editor, ExpandMacroRecursively};
pub fn apply_related_actions(editor: &View<Editor>, cx: &mut WindowContext) {
let is_rust_related = editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.all_buffers()
.iter()
.any(|b| match b.read(cx).language() {
Some(l) => is_rust_language(l),
None => false,
})
});
if is_rust_related {
register_action(editor, cx, expand_macro_recursively);
}
}
pub fn expand_macro_recursively(
editor: &mut Editor,
_: &ExpandMacroRecursively,
cx: &mut ViewContext<'_, Editor>,
) {
if editor.selections.count() == 0 {
return;
}
let Some(project) = &editor.project else {
return;
};
let Some(workspace) = editor.workspace() else {
return;
};
let multibuffer = editor.buffer().read(cx);
let Some((trigger_anchor, rust_language, server_to_query, buffer)) = editor
.selections
.disjoint_anchors()
.into_iter()
.filter(|selection| selection.start == selection.end)
.filter_map(|selection| Some((selection.start.buffer_id?, selection.start)))
.filter_map(|(buffer_id, trigger_anchor)| {
let buffer = multibuffer.buffer(buffer_id)?;
let rust_language = buffer.read(cx).language_at(trigger_anchor.text_anchor)?;
if !is_rust_language(&rust_language) {
return None;
}
Some((trigger_anchor, rust_language, buffer))
})
.find_map(|(trigger_anchor, rust_language, buffer)| {
project
.read(cx)
.language_servers_for_buffer(buffer.read(cx), cx)
.into_iter()
.find_map(|(adapter, server)| {
if adapter.name.0.as_ref() == "rust-analyzer" {
Some((
trigger_anchor,
Arc::clone(&rust_language),
server.server_id(),
buffer.clone(),
))
} else {
None
}
})
})
else {
return;
};
let project = project.clone();
let buffer_snapshot = buffer.read(cx).snapshot();
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
let expand_macro_task = project.update(cx, |project, cx| {
project.request_lsp(
buffer,
project::LanguageServerToQuery::Other(server_to_query),
ExpandMacro { position },
cx,
)
});
cx.spawn(|editor, mut cx| async move {
let macro_expansion = expand_macro_task.await.context("expand macro")?;
if macro_expansion.is_empty() {
log::info!("Empty macro expansion for position {position:?}");
return Ok(());
}
let buffer = project.update(&mut cx, |project, cx| {
project.create_buffer(&macro_expansion.expansion, Some(rust_language), cx)
})??;
workspace.update(&mut cx, |workspace, cx| {
let buffer = cx.build_model(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(macro_expansion.name)
});
workspace.add_item(
Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
cx,
);
})
})
.detach_and_log_err(cx);
}
fn is_rust_language(language: &Language) -> bool {
language.name().as_ref() == "Rust"
}

View file

@ -136,6 +136,7 @@ pub struct ScrollManager {
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>, last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool, show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>, hide_scrollbar_task: Option<Task<()>>,
dragging_scrollbar: bool,
visible_line_count: Option<f32>, visible_line_count: Option<f32>,
} }
@ -148,6 +149,7 @@ impl ScrollManager {
autoscroll_request: None, autoscroll_request: None,
show_scrollbars: true, show_scrollbars: true,
hide_scrollbar_task: None, hide_scrollbar_task: None,
dragging_scrollbar: false,
last_autoscroll: None, last_autoscroll: None,
visible_line_count: None, visible_line_count: None,
} }
@ -278,6 +280,17 @@ impl ScrollManager {
self.autoscroll_request.is_some() self.autoscroll_request.is_some()
} }
pub fn is_dragging_scrollbar(&self) -> bool {
self.dragging_scrollbar
}
pub fn set_is_dragging_scrollbar(&mut self, dragging: bool, cx: &mut ViewContext<Editor>) {
if dragging != self.dragging_scrollbar {
self.dragging_scrollbar = dragging;
cx.notify();
}
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool { pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x { if max < self.anchor.offset.x {
self.anchor.offset.x = max; self.anchor.offset.x = max;

View file

@ -32,7 +32,7 @@ impl Render for DeployFeedbackButton {
IconButton::new("give-feedback", Icon::Envelope) IconButton::new("give-feedback", Icon::Envelope)
.style(ui::ButtonStyle::Subtle) .style(ui::ButtonStyle::Subtle)
.selected(is_open) .selected(is_open)
.tooltip(|cx| Tooltip::text("Give Feedback", cx)) .tooltip(|cx| Tooltip::text("Share Feedback", cx))
.on_click(|_, cx| { .on_click(|_, cx| {
cx.dispatch_action(Box::new(GiveFeedback)); cx.dispatch_action(Box::new(GiveFeedback));
}) })

View file

@ -5,15 +5,18 @@ use workspace::Workspace;
pub mod deploy_feedback_button; pub mod deploy_feedback_button;
pub mod feedback_modal; pub mod feedback_modal;
actions!(GiveFeedback, SubmitFeedback); actions!(feedback, [GiveFeedback, SubmitFeedback]);
mod system_specs; mod system_specs;
actions!( actions!(
CopySystemSpecsIntoClipboard, zed,
FileBugReport, [
RequestFeature, CopySystemSpecsIntoClipboard,
OpenZedCommunityRepo FileBugReport,
RequestFeature,
OpenZedCommunityRepo
]
); );
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {

View file

@ -1,28 +1,38 @@
use std::{ops::RangeInclusive, sync::Arc}; use std::{ops::RangeInclusive, sync::Arc};
use anyhow::bail; use anyhow::{anyhow, bail};
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
div, red, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
FocusableView, Model, PromptLevel, Render, Task, View, ViewContext, Model, PromptLevel, Render, Task, View, ViewContext,
}; };
use isahc::Request; use isahc::Request;
use language::Buffer; use language::Buffer;
use project::Project; use project::Project;
use regex::Regex; use regex::Regex;
use serde_derive::Serialize; use serde_derive::Serialize;
use ui::{prelude::*, Button, ButtonStyle, Label, Tooltip}; use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::{ModalView, Workspace};
use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo}; use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
// For UI testing purposes
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
// Temporary, until tests are in place
#[cfg(debug_assertions)]
const DEV_MODE: bool = true;
#[cfg(not(debug_assertions))]
const DEV_MODE: bool = false;
const DATABASE_KEY_NAME: &str = "email_address"; const DATABASE_KEY_NAME: &str = "email_address";
const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b"; const EMAIL_REGEX: &str = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b";
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000; const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str = const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
"Feedback failed to submit, see error log for details."; "Feedback failed to submit, see error log for details.";
@ -41,8 +51,9 @@ pub struct FeedbackModal {
system_specs: SystemSpecs, system_specs: SystemSpecs,
feedback_editor: View<Editor>, feedback_editor: View<Editor>,
email_address_editor: View<Editor>, email_address_editor: View<Editor>,
character_count: usize, awaiting_submission: bool,
pending_submission: bool, user_submitted: bool,
character_count: i32,
} }
impl FocusableView for FeedbackModal { impl FocusableView for FeedbackModal {
@ -52,6 +63,25 @@ impl FocusableView for FeedbackModal {
} }
impl EventEmitter<DismissEvent> for FeedbackModal {} impl EventEmitter<DismissEvent> for FeedbackModal {}
impl ModalView for FeedbackModal {
fn dismiss(&mut self, cx: &mut ViewContext<Self>) -> Task<bool> {
if self.user_submitted {
self.set_user_submitted(false, cx);
return cx.spawn(|_, _| async { true });
}
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
if !has_feedback {
return cx.spawn(|_, _| async { true });
}
let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
cx.spawn(|_, _| async { answer.await.ok() == Some(0) })
}
}
impl FeedbackModal { impl FeedbackModal {
pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let _handle = cx.view().downgrade(); let _handle = cx.view().downgrade();
@ -104,6 +134,11 @@ impl FeedbackModal {
let feedback_editor = cx.build_view(|cx| { let feedback_editor = cx.build_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_placeholder_text(
"You can use markdown to organize your feedback wiht add code and links, or organize feedback.",
cx,
);
// editor.set_show_gutter(false, cx);
editor.set_vertical_scroll_margin(5, cx); editor.set_vertical_scroll_margin(5, cx);
editor editor
}); });
@ -119,7 +154,7 @@ impl FeedbackModal {
.as_singleton() .as_singleton()
.expect("Feedback editor is never a multi-buffer") .expect("Feedback editor is never a multi-buffer")
.read(cx) .read(cx)
.len(); .len() as i32;
cx.notify(); cx.notify();
} }
_ => {} _ => {}
@ -131,7 +166,8 @@ impl FeedbackModal {
system_specs: system_specs.clone(), system_specs: system_specs.clone(),
feedback_editor, feedback_editor,
email_address_editor, email_address_editor,
pending_submission: false, awaiting_submission: false,
user_submitted: false,
character_count: 0, character_count: 0,
} }
} }
@ -163,37 +199,53 @@ impl FeedbackModal {
} }
}; };
this.update(&mut cx, |feedback_editor, cx| { this.update(&mut cx, |this, cx| {
feedback_editor.set_pending_submission(true, cx); this.set_awaiting_submission(true, cx);
}) })
.log_err(); .log_err();
if let Err(error) = let res =
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await;
{
log::error!("{}", error); match res {
this.update(&mut cx, |feedback_editor, cx| { Ok(_) => {
let prompt = cx.prompt( this.update(&mut cx, |this, cx| {
PromptLevel::Critical, this.set_user_submitted(true, cx);
FEEDBACK_SUBMISSION_ERROR_TEXT, cx.emit(DismissEvent)
&["OK"],
);
cx.spawn(|_, _cx| async move {
prompt.await.ok();
}) })
.detach(); .ok();
feedback_editor.set_pending_submission(false, cx); }
}) Err(error) => {
.log_err(); log::error!("{}", error);
this.update(&mut cx, |this, cx| {
let prompt = cx.prompt(
PromptLevel::Critical,
FEEDBACK_SUBMISSION_ERROR_TEXT,
&["OK"],
);
cx.spawn(|_, _cx| async move {
prompt.await.ok();
})
.detach();
this.set_awaiting_submission(false, cx);
})
.log_err();
}
} }
} }
}) })
.detach(); .detach();
Task::ready(Ok(())) Task::ready(Ok(()))
} }
fn set_pending_submission(&mut self, pending_submission: bool, cx: &mut ViewContext<Self>) { fn set_awaiting_submission(&mut self, awaiting_submission: bool, cx: &mut ViewContext<Self>) {
self.pending_submission = pending_submission; self.awaiting_submission = awaiting_submission;
cx.notify();
}
fn set_user_submitted(&mut self, user_submitted: bool, cx: &mut ViewContext<Self>) {
self.user_submitted = user_submitted;
cx.notify(); cx.notify();
} }
@ -203,6 +255,14 @@ impl FeedbackModal {
zed_client: Arc<Client>, zed_client: Arc<Client>,
system_specs: SystemSpecs, system_specs: SystemSpecs,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
if DEV_MODE {
if SEND_SUCCESS_IN_DEV_MODE {
return Ok(());
} else {
return Err(anyhow!("Error submitting feedback"));
}
}
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL); let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
let telemetry = zed_client.telemetry(); let telemetry = zed_client.telemetry();
let metrics_id = telemetry.metrics_id(); let metrics_id = telemetry.metrics_id();
@ -233,11 +293,8 @@ impl FeedbackModal {
} }
// TODO: Escape button calls dismiss // TODO: Escape button calls dismiss
// TODO: Should do same as hitting cancel / clicking outside of modal
// Close immediately if no text in field
// Ask to close if text in the field
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent); cx.emit(DismissEvent)
} }
} }
@ -251,132 +308,128 @@ impl Render for FeedbackModal {
}; };
let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count); let valid_character_count = FEEDBACK_CHAR_LIMIT.contains(&self.character_count);
let characters_remaining =
if valid_character_count || self.character_count > *FEEDBACK_CHAR_LIMIT.end() {
*FEEDBACK_CHAR_LIMIT.end() as i32 - self.character_count as i32
} else {
self.character_count as i32 - *FEEDBACK_CHAR_LIMIT.start() as i32
};
let allow_submission = let allow_submission =
valid_character_count && valid_email_address && !self.pending_submission; valid_character_count && valid_email_address && !self.awaiting_submission;
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some(); let submit_button_text = if self.awaiting_submission {
"Submitting..."
let submit_button_text = if self.pending_submission {
"Sending..."
} else { } else {
"Send Feedback" "Submit"
}; };
let dismiss = cx.listener(|_, _, cx| {
cx.emit(DismissEvent);
});
// TODO: get the "are you sure you want to dismiss?" prompt here working
let dismiss_prompt = cx.listener(|_, _, _| {
// let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]);
// cx.spawn(|_, _| async move {
// let answer = answer.await.ok();
// if answer == Some(0) {
// cx.emit(DismissEvent);
// }
// })
// .detach();
});
let open_community_repo = let open_community_repo =
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
// TODO: Nate UI pass // Moved this here because providing it inline breaks rustfmt
let provide_an_email_address =
"Provide an email address if you want us to be able to reply.";
v_stack() v_stack()
.elevation_3(cx) .elevation_3(cx)
.key_context("GiveFeedback") .key_context("GiveFeedback")
.on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::cancel))
.min_w(rems(40.)) .min_w(rems(40.))
.max_w(rems(96.)) .max_w(rems(96.))
.border() .h(rems(32.))
.border_color(red()) .p_4()
.h(rems(40.)) .gap_4()
.p_2() .child(v_stack().child(
.gap_2() // TODO: Add Headline component to `ui2`
div().text_xl().child("Share Feedback"),
))
.child( .child(
v_stack().child( Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
div() format!(
.size_full() "Feedback must be at least {} characters.",
.child(Label::new("Give Feedback").color(Color::Default)) FEEDBACK_CHAR_LIMIT.start()
.child(Label::new("This editor supports markdown").color(Color::Muted)), )
), } else {
format!(
"Characters: {}",
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
)
})
.color(if valid_character_count {
Color::Success
} else {
Color::Error
}),
) )
.child( .child(
div() div()
.flex_1() .flex_1()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
.p_2()
.border() .border()
.rounded_md()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()), .child(self.feedback_editor.clone()),
) )
.child(
div().child(
Label::new(format!(
"Characters: {}",
characters_remaining
))
.when_else(
valid_character_count,
|this| this.color(Color::Success),
|this| this.color(Color::Error)
)
),
)
.child( .child(
div() div()
.bg(cx.theme().colors().editor_background) .child(
.border() h_stack()
.border_color(cx.theme().colors().border) .bg(cx.theme().colors().editor_background)
.child(self.email_address_editor.clone()) .p_2()
) .border()
.child( .rounded_md()
h_stack() .border_color(cx.theme().colors().border)
.justify_between() .child(self.email_address_editor.clone()),
.gap_1()
.child(Button::new("community_repo", "Community Repo")
.style(ButtonStyle::Filled)
.color(Color::Muted)
.on_click(open_community_repo)
) )
.child(h_stack().justify_between().gap_1() .child(
.child( h_stack()
Button::new("cancel_feedback", "Cancel") .justify_between()
.style(ButtonStyle::Subtle) .gap_1()
.color(Color::Muted) .child(
// TODO: replicate this logic when clicking outside the modal Button::new("community_repo", "Community Repo")
// TODO: Will require somehow overriding the modal dismal default behavior .style(ButtonStyle::Transparent)
.when_else( .icon(Icon::ExternalLink)
has_feedback, .icon_position(IconPosition::End)
|this| this.on_click(dismiss_prompt), .icon_size(IconSize::Small)
|this| this.on_click(dismiss) .on_click(open_community_repo),
) )
) .child(
.child( h_stack()
Button::new("send_feedback", submit_button_text) .gap_1()
.color(Color::Accent) .child(
.style(ButtonStyle::Filled) Button::new("cancel_feedback", "Cancel")
// TODO: Ensure that while submitting, "Sending..." is shown and disable the button .style(ButtonStyle::Subtle)
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Send Feedback", and re-enable button .color(Color::Muted)
// TODO: If submit is successful, close the modal .on_click(cx.listener(move |_, _, cx| {
.on_click(cx.listener(|this, _, cx| { cx.spawn(|this, mut cx| async move {
let _ = this.submit(cx); this.update(&mut cx, |_, cx| {
})) cx.emit(DismissEvent)
.tooltip(|cx| { })
Tooltip::with_meta( .ok();
"Submit feedback to the Zed team.", })
None, .detach();
"Provide an email address if you want us to be able to reply.", })),
cx,
) )
}) .child(
.when(!allow_submission, |this| this.disabled(true)) Button::new("send_feedback", submit_button_text)
), .color(Color::Accent)
) .style(ButtonStyle::Filled)
// TODO: Ensure that while submitting, "Sending..." is shown and disable the button
// TODO: If submit errors: show popup with error, don't close modal, set text back to "Submit", and re-enable button
.on_click(cx.listener(|this, _, cx| {
this.submit(cx).detach();
}))
.tooltip(move |cx| {
Tooltip::with_meta(
"Submit feedback to the Zed team.",
None,
provide_an_email_address,
cx,
)
})
.when(!allow_submission, |this| this.disabled(true)),
),
),
),
) )
} }
} }
// TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
// TODO: Testing of various button states, dismissal prompts, etc.

View file

@ -17,9 +17,11 @@ use std::{
use text::Point; use text::Point;
use ui::{prelude::*, HighlightedLabel, ListItem}; use ui::{prelude::*, HighlightedLabel, ListItem};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace; use workspace::{ModalView, Workspace};
actions!(Toggle); actions!(file_finder, [Toggle]);
impl ModalView for FileFinder {}
pub struct FileFinder { pub struct FileFinder {
picker: View<Picker<FileFinderDelegate>>, picker: View<Picker<FileFinderDelegate>>,

View file

@ -8,8 +8,9 @@ use text::{Bias, Point};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{h_stack, prelude::*, v_stack, Label}; use ui::{h_stack, prelude::*, v_stack, Label};
use util::paths::FILE_ROW_COLUMN_DELIMITER; use util::paths::FILE_ROW_COLUMN_DELIMITER;
use workspace::ModalView;
actions!(Toggle); actions!(go_to_line, [Toggle]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(GoToLine::register).detach(); cx.observe_new_views(GoToLine::register).detach();
@ -23,6 +24,8 @@ pub struct GoToLine {
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl ModalView for GoToLine {}
impl FocusableView for GoToLine { impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.line_editor.focus_handle(cx) self.line_editor.focus_handle(cx)

View file

@ -22,7 +22,7 @@ Actions are frequently unit structs, for which we have a macro. The above could
```rust ```rust
mod menu { mod menu {
actions!(MoveUp, MoveDown); actions!(gpui, [MoveUp, MoveDown]);
} }
``` ```

View file

@ -3,34 +3,33 @@ use anyhow::{anyhow, Context, Result};
use collections::HashMap; use collections::HashMap;
pub use no_action::NoAction; pub use no_action::NoAction;
use serde_json::json; use serde_json::json;
use std::{ use std::any::{Any, TypeId};
any::{Any, TypeId},
ops::Deref,
};
/// Actions are used to implement keyboard-driven UI. /// Actions are used to implement keyboard-driven UI.
/// When you declare an action, you can bind keys to the action in the keymap and /// When you declare an action, you can bind keys to the action in the keymap and
/// listeners for that action in the element tree. /// listeners for that action in the element tree.
/// ///
/// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct /// To declare a list of simple actions, you can use the actions! macro, which defines a simple unit struct
/// action for each listed action name. /// action for each listed action name in the given namespace.
/// ```rust /// ```rust
/// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline); /// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ``` /// ```
/// More complex data types can also be actions. If you annotate your type with the action derive macro /// More complex data types can also be actions, providing they implement Clone, PartialEq,
/// it will be implemented and registered automatically. /// and serde_derive::Deserialize.
/// Use `impl_actions!` to automatically implement the action in the given namespace.
/// ``` /// ```
/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)] /// #[derive(Clone, PartialEq, serde_derive::Deserialize)]
/// pub struct SelectNext { /// pub struct SelectNext {
/// pub replace_newest: bool, /// pub replace_newest: bool,
/// } /// }
/// impl_actions!(editor, [SelectNext]);
/// ```
/// ///
/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]` /// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
/// macro, which only generates the code needed to register your action before `main`. /// macro, which only generates the code needed to register your action before `main`.
/// ///
/// ``` /// ```
/// #[gpui::register_action] /// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone)]
/// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::fmt::Debug)]
/// pub struct Paste { /// pub struct Paste {
/// pub content: SharedString, /// pub content: SharedString,
/// } /// }
@ -38,6 +37,7 @@ use std::{
/// impl gpui::Action for Paste { /// impl gpui::Action for Paste {
/// ///... /// ///...
/// } /// }
/// register_action!(Paste);
/// ``` /// ```
pub trait Action: 'static { pub trait Action: 'static {
fn boxed_clone(&self) -> Box<dyn Action>; fn boxed_clone(&self) -> Box<dyn Action>;
@ -56,7 +56,7 @@ pub trait Action: 'static {
impl std::fmt::Debug for dyn Action { impl std::fmt::Debug for dyn Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("dyn Action") f.debug_struct("dyn Action")
.field("type_name", &self.name()) .field("name", &self.name())
.finish() .finish()
} }
} }
@ -115,7 +115,7 @@ impl ActionRegistry {
for builder in __GPUI_ACTIONS { for builder in __GPUI_ACTIONS {
let action = builder(); let action = builder();
//todo(remove) //todo(remove)
let name: SharedString = remove_the_2(action.name).into(); let name: SharedString = action.name.into();
self.builders_by_name.insert(name.clone(), action.build); self.builders_by_name.insert(name.clone(), action.build);
self.names_by_type_id.insert(action.type_id, name.clone()); self.names_by_type_id.insert(action.type_id, name.clone());
self.all_names.push(name); self.all_names.push(name);
@ -139,11 +139,9 @@ impl ActionRegistry {
name: &str, name: &str,
params: Option<serde_json::Value>, params: Option<serde_json::Value>,
) -> Result<Box<dyn Action>> { ) -> Result<Box<dyn Action>> {
//todo(remove)
let name = remove_the_2(name);
let build_action = self let build_action = self
.builders_by_name .builders_by_name
.get(name.deref()) .get(name)
.ok_or_else(|| anyhow!("no action type registered for {}", name))?; .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
(build_action)(params.unwrap_or_else(|| json!({}))) (build_action)(params.unwrap_or_else(|| json!({})))
.with_context(|| format!("Attempting to build action {}", name)) .with_context(|| format!("Attempting to build action {}", name))
@ -155,36 +153,88 @@ impl ActionRegistry {
} }
/// Defines unit structs that can be used as actions. /// Defines unit structs that can be used as actions.
/// To use more complex data types as actions, annotate your type with the #[action] macro. /// To use more complex data types as actions, use `impl_actions!`
#[macro_export] #[macro_export]
macro_rules! actions { macro_rules! actions {
() => {}; ($namespace:path, [ $($name:ident),* $(,)? ]) => {
$(
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize)]
#[serde(crate = "gpui::serde")]
pub struct $name;
( $name:ident ) => { gpui::__impl_action!($namespace, $name,
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)] fn build(_: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
#[serde(crate = "gpui::serde")] Ok(Box::new(Self))
pub struct $name; }
}; );
( $name:ident, $($rest:tt)* ) => { gpui::register_action!($name);
actions!($name); )*
actions!($($rest)*);
}; };
} }
//todo!(remove) /// Implements the Action trait for any struct that implements Clone, Default, PartialEq, and serde_deserialize::Deserialize
pub fn remove_the_2(action_name: &str) -> String { #[macro_export]
let mut separator_matches = action_name.rmatch_indices("::"); macro_rules! impl_actions {
separator_matches.next().unwrap(); ($namespace:path, [ $($name:ident),* $(,)? ]) => {
let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2); $(
// todo!() remove the 2 replacement when migration is done gpui::__impl_action!($namespace, $name,
action_name[name_start_ix..] fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
.replace("2::", "::") Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
.to_string() }
);
)*
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __impl_action {
($namespace:path, $name:ident, $build:item) => {
impl gpui::Action for $name {
fn name(&self) -> &'static str
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
// todo!() why is this needed in addition to name?
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
concat!(
stringify!($namespace),
"::",
stringify!($name),
)
}
$build
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
};
} }
mod no_action { mod no_action {
use crate as gpui; use crate as gpui;
actions!(NoAction); actions!(zed, [NoAction]);
} }

View file

@ -13,6 +13,7 @@ use smallvec::SmallVec;
use smol::future::FutureExt; use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub use test_context::*; pub use test_context::*;
use time::UtcOffset;
use crate::{ use crate::{
current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any, current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any,
@ -536,6 +537,10 @@ impl AppContext {
self.platform.restart() self.platform.restart()
} }
pub fn local_timezone(&self) -> UtcOffset {
self.platform.local_timezone()
}
pub(crate) fn push_effect(&mut self, effect: Effect) { pub(crate) fn push_effect(&mut self, effect: Effect) {
match &effect { match &effect {
Effect::Notify { emitter } => { Effect::Notify { emitter } => {
@ -1110,6 +1115,10 @@ impl AppContext {
} }
} }
} }
pub fn has_active_drag(&self) -> bool {
self.active_drag.is_some()
}
} }
impl Context for AppContext { impl Context for AppContext {

View file

@ -69,24 +69,6 @@ pub trait IntoElement: Sized {
self.map(|this| if condition { then(this) } else { this }) self.map(|this| if condition { then(this) } else { this })
} }
fn when_else(
self,
condition: bool,
then: impl FnOnce(Self) -> Self,
otherwise: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| {
if condition {
then(this)
} else {
otherwise(this)
}
})
}
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where where
Self: Sized, Self: Sized,

View file

@ -0,0 +1,496 @@
use crate::{
px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels,
Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext,
};
use collections::VecDeque;
use refineable::Refineable as _;
use std::{cell::RefCell, ops::Range, rc::Rc};
use sum_tree::{Bias, SumTree};
pub fn list(state: ListState) -> List {
List {
state,
style: StyleRefinement::default(),
}
}
pub struct List {
state: ListState,
style: StyleRefinement,
}
#[derive(Clone)]
pub struct ListState(Rc<RefCell<StateInner>>);
struct StateInner {
last_layout_width: Option<Pixels>,
render_item: Box<dyn FnMut(usize, &mut WindowContext) -> AnyElement>,
items: SumTree<ListItem>,
logical_scroll_top: Option<ListOffset>,
alignment: ListAlignment,
overdraw: Pixels,
#[allow(clippy::type_complexity)]
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut WindowContext)>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ListAlignment {
Top,
Bottom,
}
pub struct ListScrollEvent {
pub visible_range: Range<usize>,
pub count: usize,
}
#[derive(Clone)]
enum ListItem {
Unrendered,
Rendered { height: Pixels },
}
#[derive(Clone, Debug, Default, PartialEq)]
struct ListItemSummary {
count: usize,
rendered_count: usize,
unrendered_count: usize,
height: Pixels,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct Count(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct RenderedCount(usize);
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
struct UnrenderedCount(usize);
#[derive(Clone, Debug, Default)]
struct Height(Pixels);
impl ListState {
pub fn new<F>(
element_count: usize,
orientation: ListAlignment,
overdraw: Pixels,
render_item: F,
) -> Self
where
F: 'static + FnMut(usize, &mut WindowContext) -> AnyElement,
{
let mut items = SumTree::new();
items.extend((0..element_count).map(|_| ListItem::Unrendered), &());
Self(Rc::new(RefCell::new(StateInner {
last_layout_width: None,
render_item: Box::new(render_item),
items,
logical_scroll_top: None,
alignment: orientation,
overdraw,
scroll_handler: None,
})))
}
pub fn reset(&self, element_count: usize) {
let state = &mut *self.0.borrow_mut();
state.logical_scroll_top = None;
state.items = SumTree::new();
state
.items
.extend((0..element_count).map(|_| ListItem::Unrendered), &());
}
pub fn item_count(&self) -> usize {
self.0.borrow().items.summary().count
}
pub fn splice(&self, old_range: Range<usize>, count: usize) {
let state = &mut *self.0.borrow_mut();
if let Some(ListOffset {
item_ix,
offset_in_item,
}) = state.logical_scroll_top.as_mut()
{
if old_range.contains(item_ix) {
*item_ix = old_range.start;
*offset_in_item = px(0.);
} else if old_range.end <= *item_ix {
*item_ix = *item_ix - (old_range.end - old_range.start) + count;
}
}
let mut old_heights = state.items.cursor::<Count>();
let mut new_heights = old_heights.slice(&Count(old_range.start), Bias::Right, &());
old_heights.seek_forward(&Count(old_range.end), Bias::Right, &());
new_heights.extend((0..count).map(|_| ListItem::Unrendered), &());
new_heights.append(old_heights.suffix(&()), &());
drop(old_heights);
state.items = new_heights;
}
pub fn set_scroll_handler(
&self,
handler: impl FnMut(&ListScrollEvent, &mut WindowContext) + 'static,
) {
self.0.borrow_mut().scroll_handler = Some(Box::new(handler))
}
pub fn logical_scroll_top(&self) -> ListOffset {
self.0.borrow().logical_scroll_top()
}
pub fn scroll_to(&self, mut scroll_top: ListOffset) {
let state = &mut *self.0.borrow_mut();
let item_count = state.items.summary().count;
if scroll_top.item_ix >= item_count {
scroll_top.item_ix = item_count;
scroll_top.offset_in_item = px(0.);
}
state.logical_scroll_top = Some(scroll_top);
}
}
impl StateInner {
fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range<usize> {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
let start_y = cursor.start().height + scroll_top.offset_in_item;
cursor.seek_forward(&Height(start_y + height), Bias::Left, &());
scroll_top.item_ix..cursor.start().count + 1
}
fn scroll(
&mut self,
scroll_top: &ListOffset,
height: Pixels,
delta: Point<Pixels>,
cx: &mut WindowContext,
) {
let scroll_max = (self.items.summary().height - height).max(px(0.));
let new_scroll_top = (self.scroll_top(scroll_top) - delta.y)
.max(px(0.))
.min(scroll_max);
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
self.logical_scroll_top = None;
} else {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
let item_ix = cursor.start().count;
let offset_in_item = new_scroll_top - cursor.start().height;
self.logical_scroll_top = Some(ListOffset {
item_ix,
offset_in_item,
});
}
if self.scroll_handler.is_some() {
let visible_range = self.visible_range(height, scroll_top);
self.scroll_handler.as_mut().unwrap()(
&ListScrollEvent {
visible_range,
count: self.items.summary().count,
},
cx,
);
}
cx.notify();
}
fn logical_scroll_top(&self) -> ListOffset {
self.logical_scroll_top
.unwrap_or_else(|| match self.alignment {
ListAlignment::Top => ListOffset {
item_ix: 0,
offset_in_item: px(0.),
},
ListAlignment::Bottom => ListOffset {
item_ix: self.items.summary().count,
offset_in_item: px(0.),
},
})
}
fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels {
let mut cursor = self.items.cursor::<ListItemSummary>();
cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &());
cursor.start().height + logical_scroll_top.offset_in_item
}
}
impl std::fmt::Debug for ListItem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unrendered => write!(f, "Unrendered"),
Self::Rendered { height, .. } => {
f.debug_struct("Rendered").field("height", height).finish()
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ListOffset {
pub item_ix: usize,
pub offset_in_item: Pixels,
}
impl Element for List {
type State = ();
fn layout(
&mut self,
_state: Option<Self::State>,
cx: &mut crate::WindowContext,
) -> (crate::LayoutId, Self::State) {
let mut style = Style::default();
style.refine(&self.style);
let layout_id = cx.with_text_style(style.text_style().cloned(), |cx| {
cx.request_layout(&style, None)
});
(layout_id, ())
}
fn paint(
self,
bounds: crate::Bounds<crate::Pixels>,
_state: &mut Self::State,
cx: &mut crate::WindowContext,
) {
let state = &mut *self.state.0.borrow_mut();
// If the width of the list has changed, invalidate all cached item heights
if state.last_layout_width != Some(bounds.size.width) {
state.items = SumTree::from_iter(
(0..state.items.summary().count).map(|_| ListItem::Unrendered),
&(),
)
}
let old_items = state.items.clone();
let mut measured_items = VecDeque::new();
let mut item_elements = VecDeque::new();
let mut rendered_height = px(0.);
let mut scroll_top = state.logical_scroll_top();
let available_item_space = Size {
width: AvailableSpace::Definite(bounds.size.width),
height: AvailableSpace::MinContent,
};
// Render items after the scroll top, including those in the trailing overdraw
let mut cursor = old_items.cursor::<Count>();
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
for (ix, item) in cursor.by_ref().enumerate() {
let visible_height = rendered_height - scroll_top.offset_in_item;
if visible_height >= bounds.size.height + state.overdraw {
break;
}
// Use the previously cached height if available
let mut height = if let ListItem::Rendered { height } = item {
Some(*height)
} else {
None
};
// If we're within the visible area or the height wasn't cached, render and measure the item's element
if visible_height < bounds.size.height || height.is_none() {
let mut element = (state.render_item)(scroll_top.item_ix + ix, cx);
let element_size = element.measure(available_item_space, cx);
height = Some(element_size.height);
if visible_height < bounds.size.height {
item_elements.push_back(element);
}
}
let height = height.unwrap();
rendered_height += height;
measured_items.push_back(ListItem::Rendered { height });
}
// Prepare to start walking upward from the item at the scroll top.
cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &());
// If the rendered items do not fill the visible region, then adjust
// the scroll top upward.
if rendered_height - scroll_top.offset_in_item < bounds.size.height {
while rendered_height < bounds.size.height {
cursor.prev(&());
if cursor.item().is_some() {
let mut element = (state.render_item)(cursor.start().0, cx);
let element_size = element.measure(available_item_space, cx);
rendered_height += element_size.height;
measured_items.push_front(ListItem::Rendered {
height: element_size.height,
});
item_elements.push_front(element)
} else {
break;
}
}
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - bounds.size.height,
};
match state.alignment {
ListAlignment::Top => {
scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.));
state.logical_scroll_top = Some(scroll_top);
}
ListAlignment::Bottom => {
scroll_top = ListOffset {
item_ix: cursor.start().0,
offset_in_item: rendered_height - bounds.size.height,
};
state.logical_scroll_top = None;
}
};
}
// Measure items in the leading overdraw
let mut leading_overdraw = scroll_top.offset_in_item;
while leading_overdraw < state.overdraw {
cursor.prev(&());
if let Some(item) = cursor.item() {
let height = if let ListItem::Rendered { height } = item {
*height
} else {
let mut element = (state.render_item)(cursor.start().0, cx);
element.measure(available_item_space, cx).height
};
leading_overdraw += height;
measured_items.push_front(ListItem::Rendered { height });
} else {
break;
}
}
let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len());
let mut cursor = old_items.cursor::<Count>();
let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &());
new_items.extend(measured_items, &());
cursor.seek(&Count(measured_range.end), Bias::Right, &());
new_items.append(cursor.suffix(&()), &());
// Paint the visible items
let mut item_origin = bounds.origin;
item_origin.y -= scroll_top.offset_in_item;
for mut item_element in item_elements {
let item_height = item_element.measure(available_item_space, cx).height;
item_element.draw(item_origin, available_item_space, cx);
item_origin.y += item_height;
}
state.items = new_items;
state.last_layout_width = Some(bounds.size.width);
let list_state = self.state.clone();
let height = bounds.size.height;
cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
list_state.0.borrow_mut().scroll(
&scroll_top,
height,
event.delta.pixel_delta(px(20.)),
cx,
)
}
});
}
}
impl IntoElement for List {
type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
fn into_element(self) -> Self::Element {
self
}
}
impl Styled for List {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl sum_tree::Item for ListItem {
type Summary = ListItemSummary;
fn summary(&self) -> Self::Summary {
match self {
ListItem::Unrendered => ListItemSummary {
count: 1,
rendered_count: 0,
unrendered_count: 1,
height: px(0.),
},
ListItem::Rendered { height } => ListItemSummary {
count: 1,
rendered_count: 1,
unrendered_count: 0,
height: *height,
},
}
}
}
impl sum_tree::Summary for ListItemSummary {
type Context = ();
fn add_summary(&mut self, summary: &Self, _: &()) {
self.count += summary.count;
self.rendered_count += summary.rendered_count;
self.unrendered_count += summary.unrendered_count;
self.height += summary.height;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Count {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for RenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.rendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for UnrenderedCount {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.unrendered_count;
}
}
impl<'a> sum_tree::Dimension<'a, ListItemSummary> for Height {
fn add_summary(&mut self, summary: &'a ListItemSummary, _: &()) {
self.0 += summary.height;
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Count {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.count).unwrap()
}
}
impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height {
fn cmp(&self, other: &ListItemSummary, _: &()) -> std::cmp::Ordering {
self.0.partial_cmp(&other.height).unwrap()
}
}

View file

@ -1,6 +1,7 @@
mod canvas; mod canvas;
mod div; mod div;
mod img; mod img;
mod list;
mod overlay; mod overlay;
mod svg; mod svg;
mod text; mod text;
@ -9,6 +10,7 @@ mod uniform_list;
pub use canvas::*; pub use canvas::*;
pub use div::*; pub use div::*;
pub use img::*; pub use img::*;
pub use list::*;
pub use overlay::*; pub use overlay::*;
pub use svg::*; pub use svg::*;
pub use text::*; pub use text::*;

View file

@ -131,7 +131,7 @@ impl Element for UniformList {
} }
}); });
let height = match available_space.height { let height = match available_space.height {
AvailableSpace::Definite(x) => desired_height.min(x), AvailableSpace::Definite(height) => desired_height.min(height),
AvailableSpace::MinContent | AvailableSpace::MaxContent => { AvailableSpace::MinContent | AvailableSpace::MaxContent => {
desired_height desired_height
} }

View file

@ -1,6 +1,7 @@
#[macro_use] #[macro_use]
mod action; mod action;
mod app; mod app;
mod assets; mod assets;
mod color; mod color;
mod element; mod element;
@ -15,6 +16,7 @@ mod keymap;
mod platform; mod platform;
pub mod prelude; pub mod prelude;
mod scene; mod scene;
mod shared_string;
mod style; mod style;
mod styled; mod styled;
mod subscription; mod subscription;
@ -57,6 +59,7 @@ pub use scene::*;
pub use serde; pub use serde;
pub use serde_derive; pub use serde_derive;
pub use serde_json; pub use serde_json;
pub use shared_string::*;
pub use smallvec; pub use smallvec;
pub use smol::Timer; pub use smol::Timer;
pub use style::*; pub use style::*;
@ -71,10 +74,9 @@ pub use util::arc_cow::ArcCow;
pub use view::*; pub use view::*;
pub use window::*; pub use window::*;
use derive_more::{Deref, DerefMut};
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
borrow::{Borrow, BorrowMut}, borrow::BorrowMut,
}; };
use taffy::TaffyLayoutEngine; use taffy::TaffyLayoutEngine;
@ -209,42 +211,3 @@ impl<T> Flatten<T> for Result<T> {
self self
} }
} }
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);
impl Default for SharedString {
fn default() -> Self {
Self(ArcCow::Owned("".into()))
}
}
impl AsRef<str> for SharedString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for SharedString {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl std::fmt::Debug for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Display for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_ref())
}
}
impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
fn from(value: T) -> Self {
Self(value.into())
}
}

View file

@ -193,6 +193,12 @@ impl Deref for MouseExitEvent {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl ExternalPaths {
pub fn paths(&self) -> &[PathBuf] {
&self.0
}
}
impl Render for ExternalPaths { impl Render for ExternalPaths {
type Element = Div; type Element = Div;
@ -296,7 +302,7 @@ mod test {
focus_handle: FocusHandle, focus_handle: FocusHandle,
} }
actions!(TestAction); actions!(test, [TestAction]);
impl Render for TestView { impl Render for TestView {
type Element = Stateful<Div>; type Element = Stateful<Div>;

View file

@ -149,13 +149,19 @@ impl DispatchTree {
} }
pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> { pub fn available_actions(&self, target: DispatchNodeId) -> Vec<Box<dyn Action>> {
let mut actions = Vec::new(); let mut actions = Vec::<Box<dyn Action>>::new();
for node_id in self.dispatch_path(target) { for node_id in self.dispatch_path(target) {
let node = &self.nodes[node_id.0]; let node = &self.nodes[node_id.0];
for DispatchActionListener { action_type, .. } in &node.action_listeners { for DispatchActionListener { action_type, .. } in &node.action_listeners {
// Intentionally silence these errors without logging. if let Err(ix) = actions.binary_search_by_key(action_type, |a| a.as_any().type_id())
// If an action cannot be built by default, it's not available. {
actions.extend(self.action_registry.build_action_type(action_type).ok()); // Intentionally silence these errors without logging.
// If an action cannot be built by default, it's not available.
let action = self.action_registry.build_action_type(action_type).ok();
if let Some(action) = action {
actions.insert(ix, action);
}
}
} }
} }
actions actions

View file

@ -293,11 +293,13 @@ mod tests {
#[test] #[test]
fn test_actions_definition() { fn test_actions_definition() {
{ {
actions!(A, B, C, D, E, F, G); actions!(test, [A, B, C, D, E, F, G]);
} }
{ {
actions!( actions!(
test,
[
A, A,
B, B,
C, C,
@ -305,6 +307,7 @@ mod tests {
E, E,
F, F,
G, // Don't wrap, test the trailing comma G, // Don't wrap, test the trailing comma
]
); );
} }
} }

View file

@ -325,7 +325,7 @@ impl MetalRenderer {
.entry(tile.texture_id) .entry(tile.texture_id)
.or_insert(Vec::new()) .or_insert(Vec::new())
.extend(path.vertices.iter().map(|vertex| PathVertex { .extend(path.vertices.iter().map(|vertex| PathVertex {
xy_position: vertex.xy_position - path.bounds.origin xy_position: vertex.xy_position - clipped_bounds.origin
+ tile.bounds.origin.map(Into::into), + tile.bounds.origin.map(Into::into),
st_position: vertex.st_position, st_position: vertex.st_position,
content_mask: ContentMask { content_mask: ContentMask {
@ -544,9 +544,10 @@ impl MetalRenderer {
if let Some((path, tile)) = paths_and_tiles.peek() { if let Some((path, tile)) = paths_and_tiles.peek() {
if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) { if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) {
prev_texture_id = Some(tile.texture_id); prev_texture_id = Some(tile.texture_id);
let origin = path.bounds.intersect(&path.content_mask.bounds).origin;
sprites.push(PathSprite { sprites.push(PathSprite {
bounds: Bounds { bounds: Bounds {
origin: path.bounds.origin.map(|p| p.floor()), origin: origin.map(|p| p.floor()),
size: tile.bounds.size.map(Into::into), size: tile.bounds.size.map(Into::into),
}, },
color: path.color, color: path.color,

View file

@ -0,0 +1,101 @@
use derive_more::{Deref, DerefMut};
use serde::{Deserialize, Serialize};
use std::{borrow::Borrow, sync::Arc};
use util::arc_cow::ArcCow;
#[derive(Deref, DerefMut, Eq, PartialEq, Hash, Clone)]
pub struct SharedString(ArcCow<'static, str>);
impl Default for SharedString {
fn default() -> Self {
Self(ArcCow::Owned("".into()))
}
}
impl AsRef<str> for SharedString {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for SharedString {
fn borrow(&self) -> &str {
self.as_ref()
}
}
impl std::fmt::Debug for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl std::fmt::Display for SharedString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_ref())
}
}
impl PartialEq<String> for SharedString {
fn eq(&self, other: &String) -> bool {
self.as_ref() == other
}
}
impl PartialEq<SharedString> for String {
fn eq(&self, other: &SharedString) -> bool {
self == other.as_ref()
}
}
impl PartialEq<str> for SharedString {
fn eq(&self, other: &str) -> bool {
self.as_ref() == other
}
}
impl<'a> PartialEq<&'a str> for SharedString {
fn eq(&self, other: &&'a str) -> bool {
self.as_ref() == *other
}
}
impl Into<Arc<str>> for SharedString {
fn into(self) -> Arc<str> {
match self.0 {
ArcCow::Borrowed(borrowed) => Arc::from(borrowed),
ArcCow::Owned(owned) => owned.clone(),
}
}
}
impl<T: Into<ArcCow<'static, str>>> From<T> for SharedString {
fn from(value: T) -> Self {
Self(value.into())
}
}
impl Into<String> for SharedString {
fn into(self) -> String {
self.0.to_string()
}
}
impl Serialize for SharedString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> Deserialize<'de> for SharedString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(SharedString::from(s))
}
}

View file

@ -245,6 +245,13 @@ pub trait Styled: Sized {
self self
} }
/// Sets the flex direction of the element to `column-reverse`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#column-reverse)
fn flex_col_reverse(mut self) -> Self {
self.style().flex_direction = Some(FlexDirection::ColumnReverse);
self
}
/// Sets the flex direction of the element to `row`. /// Sets the flex direction of the element to `row`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#row) /// [Docs](https://tailwindcss.com/docs/flex-direction#row)
fn flex_row(mut self) -> Self { fn flex_row(mut self) -> Self {
@ -252,6 +259,13 @@ pub trait Styled: Sized {
self self
} }
/// Sets the flex direction of the element to `row-reverse`.
/// [Docs](https://tailwindcss.com/docs/flex-direction#row-reverse)
fn flex_row_reverse(mut self) -> Self {
self.style().flex_direction = Some(FlexDirection::RowReverse);
self
}
/// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size. /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
/// [Docs](https://tailwindcss.com/docs/flex#flex-1) /// [Docs](https://tailwindcss.com/docs/flex#flex-1)
fn flex_1(mut self) -> Self { fn flex_1(mut self) -> Self {

View file

@ -4,12 +4,12 @@ use crate::{
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model,
ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, Path,
MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point,
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet,
Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, Underline, Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext,
UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use collections::HashMap; use collections::HashMap;
@ -1269,10 +1269,9 @@ impl<'a> WindowContext<'a> {
cursor_offset: position, cursor_offset: position,
}); });
} }
InputEvent::MouseDown(MouseDownEvent { InputEvent::MouseMove(MouseMoveEvent {
position, position,
button: MouseButton::Left, pressed_button: Some(MouseButton::Left),
click_count: 1,
modifiers: Modifiers::default(), modifiers: Modifiers::default(),
}) })
} }
@ -1285,6 +1284,7 @@ impl<'a> WindowContext<'a> {
}) })
} }
FileDropEvent::Submit { position } => { FileDropEvent::Submit { position } => {
self.activate(true);
self.window.mouse_position = position; self.window.mouse_position = position;
InputEvent::MouseUp(MouseUpEvent { InputEvent::MouseUp(MouseUpEvent {
button: MouseButton::Left, button: MouseButton::Left,
@ -2839,3 +2839,9 @@ impl From<(&'static str, usize)> for ElementId {
ElementId::NamedInteger(name.into(), id) ElementId::NamedInteger(name.into(), id)
} }
} }
impl From<(&'static str, u64)> for ElementId {
fn from((name, id): (&'static str, u64)) -> Self {
ElementId::NamedInteger(name.into(), id as usize)
}
}

View file

@ -1,16 +1,23 @@
use gpui2::{actions, impl_actions};
use gpui2_macros::register_action;
use serde_derive::Deserialize; use serde_derive::Deserialize;
#[test] #[test]
fn test_derive() { fn test_action_macros() {
use gpui2 as gpui; use gpui2 as gpui;
#[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)] actions!(test, [TestAction]);
#[derive(PartialEq, Clone, Deserialize)]
struct AnotherTestAction; struct AnotherTestAction;
#[gpui2_macros::register_action] impl_actions!(test, [AnotherTestAction]);
#[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)] #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
struct RegisterableAction {} struct RegisterableAction {}
register_action!(RegisterableAction);
impl gpui::Action for RegisterableAction { impl gpui::Action for RegisterableAction {
fn boxed_clone(&self) -> Box<dyn gpui::Action> { fn boxed_clone(&self) -> Box<dyn gpui::Action> {
todo!() todo!()

View file

@ -1,96 +0,0 @@
// Input:
//
// #[action]
// struct Foo {
// bar: String,
// }
// Output:
//
// #[gpui::register_action]
// #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
// struct Foo {
// bar: String,
// }
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Error};
use crate::register_action::register_action;
pub fn action(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
if input.generics.lt_token.is_some() {
return Error::new(name.span(), "Actions must be a concrete type")
.into_compile_error()
.into();
}
let is_unit_struct = match input.data {
syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
syn::Data::Enum(_) => false,
syn::Data::Union(_) => false,
};
let build_impl = if is_unit_struct {
quote! {
Ok(std::boxed::Box::new(Self {}))
}
} else {
quote! {
Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
}
};
let register_action = register_action(&name);
let output = quote! {
const _: fn() = || {
fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> + ::std::cmp::PartialEq + ::std::clone::Clone>() {}
assert_impl::<#name>();
};
impl gpui::Action for #name {
fn name(&self) -> &'static str
{
::std::any::type_name::<#name>()
}
fn debug_name() -> &'static str
where
Self: ::std::marker::Sized
{
::std::any::type_name::<#name>()
}
fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
where
Self: ::std::marker::Sized {
#build_impl
}
fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
action
.as_any()
.downcast_ref::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> std::boxed::Box<dyn gpui::Action> {
::std::boxed::Box::new(self.clone())
}
fn as_any(&self) -> &dyn ::std::any::Any {
self
}
}
#register_action
};
TokenStream::from(output)
}

View file

@ -1,4 +1,3 @@
mod action;
mod derive_into_element; mod derive_into_element;
mod register_action; mod register_action;
mod style_helpers; mod style_helpers;
@ -6,14 +5,9 @@ mod test;
use proc_macro::TokenStream; use proc_macro::TokenStream;
#[proc_macro_derive(Action)] #[proc_macro]
pub fn action(input: TokenStream) -> TokenStream { pub fn register_action(ident: TokenStream) -> TokenStream {
action::action(input) register_action::register_action_macro(ident)
}
#[proc_macro_attribute]
pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
register_action::register_action_macro(attr, item)
} }
#[proc_macro_derive(IntoElement)] #[proc_macro_derive(IntoElement)]

View file

@ -14,47 +14,13 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Ident; use proc_macro2::Ident;
use quote::{format_ident, quote}; use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput, Error}; use syn::parse_macro_input;
pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn register_action_macro(ident: TokenStream) -> TokenStream {
let input = parse_macro_input!(item as DeriveInput); let name = parse_macro_input!(ident as Ident);
let registration = register_action(&input.ident); let registration = register_action(&name);
let has_action_derive = input
.attrs
.iter()
.find(|attr| {
(|| {
let meta = attr.parse_meta().ok()?;
meta.path().is_ident("derive").then(|| match meta {
syn::Meta::Path(_) => None,
syn::Meta::NameValue(_) => None,
syn::Meta::List(list) => list
.nested
.iter()
.find(|list| match list {
syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
syn::NestedMeta::Lit(_) => false,
})
.map(|_| true),
})?
})()
.unwrap_or(false)
})
.is_some();
if has_action_derive {
return Error::new(
input.ident.span(),
"The Action derive macro has already registered this action",
)
.into_compile_error()
.into();
}
TokenStream::from(quote! { TokenStream::from(quote! {
#input
#registration #registration
}) })
} }
@ -78,7 +44,7 @@ pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
#[doc(hidden)] #[doc(hidden)]
fn #action_builder_fn_name() -> gpui::ActionData { fn #action_builder_fn_name() -> gpui::ActionData {
gpui::ActionData { gpui::ActionData {
name: ::std::any::type_name::<#type_name>(), name: <#type_name as gpui::Action>::debug_name(),
type_id: ::std::any::TypeId::of::<#type_name>(), type_id: ::std::any::TypeId::of::<#type_name>(),
build: <#type_name as gpui::Action>::build, build: <#type_name as gpui::Action>::build,
} }

View file

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

View file

@ -14,9 +14,9 @@ use project::Project;
use std::sync::Arc; use std::sync::Arc;
use ui::{prelude::*, HighlightedLabel, ListItem}; use ui::{prelude::*, HighlightedLabel, ListItem};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::{ModalView, Workspace};
actions!(Toggle); actions!(language_selector, [Toggle]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(LanguageSelector::register).detach(); cx.observe_new_views(LanguageSelector::register).detach();
@ -81,6 +81,7 @@ impl FocusableView for LanguageSelector {
} }
impl EventEmitter<DismissEvent> for LanguageSelector {} impl EventEmitter<DismissEvent> for LanguageSelector {}
impl ModalView for LanguageSelector {}
pub struct LanguageSelectorDelegate { pub struct LanguageSelectorDelegate {
language_selector: WeakView<LanguageSelector>, language_selector: WeakView<LanguageSelector>,

View file

@ -1,17 +1,15 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use futures::StreamExt; use futures::StreamExt;
use gpui::{Action, KeyBinding}; use gpui::{actions, KeyBinding};
use live_kit_client2::{ use live_kit_client2::{
LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
}; };
use live_kit_server::token::{self, VideoGrant}; use live_kit_server::token::{self, VideoGrant};
use log::LevelFilter; use log::LevelFilter;
use serde_derive::Deserialize;
use simplelog::SimpleLogger; use simplelog::SimpleLogger;
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)] actions!(live_kit_client, [Quit]);
struct Quit;
fn main() { fn main() {
SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger");

View file

@ -10,12 +10,15 @@ use gpui::actions;
pub fn init() {} pub fn init() {}
actions!( actions!(
Cancel, menu,
Confirm, [
SecondaryConfirm, Cancel,
SelectPrev, Confirm,
SelectNext, SecondaryConfirm,
SelectFirst, SelectPrev,
SelectLast, SelectNext,
ShowContextMenu SelectFirst,
SelectLast,
ShowContextMenu
]
); );

View file

@ -1,6 +1,6 @@
use editor::{ use editor::{
display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt,
DisplayPoint, Editor, ToPoint, DisplayPoint, Editor, EditorMode, ToPoint,
}; };
use fuzzy::StringMatch; use fuzzy::StringMatch;
use gpui::{ use gpui::{
@ -20,29 +20,26 @@ use std::{
use theme::{color_alpha, ActiveTheme, ThemeSettings}; use theme::{color_alpha, ActiveTheme, ThemeSettings};
use ui::{prelude::*, ListItem}; use ui::{prelude::*, ListItem};
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::ModalView;
actions!(Toggle); actions!(outline, [Toggle]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(OutlineView::register).detach(); cx.observe_new_views(OutlineView::register).detach();
} }
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) { pub fn toggle(editor: View<Editor>, _: &Toggle, cx: &mut WindowContext) {
if let Some(editor) = workspace let outline = editor
.active_item(cx) .read(cx)
.and_then(|item| item.downcast::<Editor>()) .buffer()
{ .read(cx)
let outline = editor .snapshot(cx)
.read(cx) .outline(Some(&cx.theme().syntax()));
.buffer()
.read(cx)
.snapshot(cx)
.outline(Some(&cx.theme().syntax()));
if let Some(outline) = outline { if let Some((workspace, outline)) = editor.read(cx).workspace().zip(outline) {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx)); workspace.toggle_modal(cx, |cx| OutlineView::new(outline, editor, cx));
} })
} }
} }
@ -57,6 +54,7 @@ impl FocusableView for OutlineView {
} }
impl EventEmitter<DismissEvent> for OutlineView {} impl EventEmitter<DismissEvent> for OutlineView {}
impl ModalView for OutlineView {}
impl Render for OutlineView { impl Render for OutlineView {
type Element = Div; type Element = Div;
@ -67,8 +65,15 @@ impl Render for OutlineView {
} }
impl OutlineView { impl OutlineView {
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) { fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
workspace.register_action(toggle); if editor.mode() == EditorMode::Full {
let handle = cx.view().downgrade();
editor.register_action(move |action, cx| {
if let Some(editor) = handle.upgrade() {
toggle(editor, action, cx);
}
});
}
} }
fn new( fn new(
@ -238,6 +243,7 @@ impl PickerDelegate for OutlineViewDelegate {
s.select_ranges([position..position]) s.select_ranges([position..position])
}); });
active_editor.highlight_rows(None); active_editor.highlight_rows(None);
active_editor.focus(cx);
} }
}); });

View file

@ -33,7 +33,7 @@ pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
} }
#[async_trait(?Send)] #[async_trait(?Send)]
pub(crate) trait LspCommand: 'static + Sized { pub trait LspCommand: 'static + Sized {
type Response: 'static + Default + Send; type Response: 'static + Default + Send;
type LspRequest: 'static + Send + lsp::request::Request; type LspRequest: 'static + Send + lsp::request::Request;
type ProtoRequest: 'static + Send + proto::RequestMessage; type ProtoRequest: 'static + Send + proto::RequestMessage;

View file

@ -0,0 +1,137 @@
use std::{path::Path, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use gpui::{AppContext, AsyncAppContext, ModelHandle};
use language::{point_to_lsp, proto::deserialize_anchor, Buffer};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use text::{PointUtf16, ToPointUtf16};
use crate::{lsp_command::LspCommand, Project};
pub enum LspExpandMacro {}
impl lsp::request::Request for LspExpandMacro {
type Params = ExpandMacroParams;
type Result = Option<ExpandedMacro>;
const METHOD: &'static str = "rust-analyzer/expandMacro";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandMacroParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: lsp::Position,
}
#[derive(Default, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandedMacro {
pub name: String,
pub expansion: String,
}
impl ExpandedMacro {
pub fn is_empty(&self) -> bool {
self.name.is_empty() && self.expansion.is_empty()
}
}
pub struct ExpandMacro {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for ExpandMacro {
type Response = ExpandedMacro;
type LspRequest = LspExpandMacro;
type ProtoRequest = proto::LspExtExpandMacro;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> ExpandMacroParams {
ExpandMacroParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
}
}
async fn response_from_lsp(
self,
message: Option<ExpandedMacro>,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(message
.map(|message| ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
proto::LspExtExpandMacro {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
async fn from_proto(
message: Self::ProtoRequest,
_: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
mut cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer)),
})
}
fn response_to_proto(
response: ExpandedMacro,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtExpandMacroResponse {
proto::LspExtExpandMacroResponse {
name: response.name,
expansion: response.expansion,
}
}
async fn response_from_proto(
self,
message: proto::LspExtExpandMacroResponse,
_: ModelHandle<Project>,
_: ModelHandle<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
}
fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> u64 {
message.buffer_id
}
}

View file

@ -1,5 +1,6 @@
mod ignore; mod ignore;
mod lsp_command; pub mod lsp_command;
pub mod lsp_ext_command;
mod prettier_support; mod prettier_support;
pub mod project_settings; pub mod project_settings;
pub mod search; pub mod search;
@ -174,7 +175,7 @@ struct DelayedDebounced {
cancel_channel: Option<oneshot::Sender<()>>, cancel_channel: Option<oneshot::Sender<()>>,
} }
enum LanguageServerToQuery { pub enum LanguageServerToQuery {
Primary, Primary,
Other(LanguageServerId), Other(LanguageServerId),
} }
@ -626,6 +627,7 @@ impl Project {
client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base); client.add_model_message_handler(Self::handle_update_diff_base);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
} }
pub fn local( pub fn local(
@ -5863,7 +5865,7 @@ impl Project {
.await; .await;
} }
fn request_lsp<R: LspCommand>( pub fn request_lsp<R: LspCommand>(
&self, &self,
buffer_handle: ModelHandle<Buffer>, buffer_handle: ModelHandle<Buffer>,
server: LanguageServerToQuery, server: LanguageServerToQuery,

View file

@ -33,7 +33,7 @@ pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
} }
#[async_trait(?Send)] #[async_trait(?Send)]
pub(crate) trait LspCommand: 'static + Sized + Send { pub trait LspCommand: 'static + Sized + Send {
type Response: 'static + Default + Send; type Response: 'static + Default + Send;
type LspRequest: 'static + Send + lsp::request::Request; type LspRequest: 'static + Send + lsp::request::Request;
type ProtoRequest: 'static + Send + proto::RequestMessage; type ProtoRequest: 'static + Send + proto::RequestMessage;

View file

@ -0,0 +1,137 @@
use std::{path::Path, sync::Arc};
use anyhow::Context;
use async_trait::async_trait;
use gpui::{AppContext, AsyncAppContext, Model};
use language::{point_to_lsp, proto::deserialize_anchor, Buffer};
use lsp::{LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use text::{PointUtf16, ToPointUtf16};
use crate::{lsp_command::LspCommand, Project};
pub enum LspExpandMacro {}
impl lsp::request::Request for LspExpandMacro {
type Params = ExpandMacroParams;
type Result = Option<ExpandedMacro>;
const METHOD: &'static str = "rust-analyzer/expandMacro";
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandMacroParams {
pub text_document: lsp::TextDocumentIdentifier,
pub position: lsp::Position,
}
#[derive(Default, Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExpandedMacro {
pub name: String,
pub expansion: String,
}
impl ExpandedMacro {
pub fn is_empty(&self) -> bool {
self.name.is_empty() && self.expansion.is_empty()
}
}
pub struct ExpandMacro {
pub position: PointUtf16,
}
#[async_trait(?Send)]
impl LspCommand for ExpandMacro {
type Response = ExpandedMacro;
type LspRequest = LspExpandMacro;
type ProtoRequest = proto::LspExtExpandMacro;
fn to_lsp(
&self,
path: &Path,
_: &Buffer,
_: &Arc<LanguageServer>,
_: &AppContext,
) -> ExpandMacroParams {
ExpandMacroParams {
text_document: lsp::TextDocumentIdentifier {
uri: lsp::Url::from_file_path(path).unwrap(),
},
position: point_to_lsp(self.position),
}
}
async fn response_from_lsp(
self,
message: Option<ExpandedMacro>,
_: Model<Project>,
_: Model<Buffer>,
_: LanguageServerId,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(message
.map(|message| ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
.unwrap_or_default())
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro {
proto::LspExtExpandMacro {
project_id,
buffer_id: buffer.remote_id(),
position: Some(language::proto::serialize_anchor(
&buffer.anchor_before(self.position),
)),
}
}
async fn from_proto(
message: Self::ProtoRequest,
_: Model<Project>,
buffer: Model<Buffer>,
mut cx: AsyncAppContext,
) -> anyhow::Result<Self> {
let position = message
.position
.and_then(deserialize_anchor)
.context("invalid position")?;
Ok(Self {
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
})
}
fn response_to_proto(
response: ExpandedMacro,
_: &mut Project,
_: PeerId,
_: &clock::Global,
_: &mut AppContext,
) -> proto::LspExtExpandMacroResponse {
proto::LspExtExpandMacroResponse {
name: response.name,
expansion: response.expansion,
}
}
async fn response_from_proto(
self,
message: proto::LspExtExpandMacroResponse,
_: Model<Project>,
_: Model<Buffer>,
_: AsyncAppContext,
) -> anyhow::Result<ExpandedMacro> {
Ok(ExpandedMacro {
name: message.name,
expansion: message.expansion,
})
}
fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> u64 {
message.buffer_id
}
}

View file

@ -1,5 +1,6 @@
mod ignore; mod ignore;
mod lsp_command; pub mod lsp_command;
pub mod lsp_ext_command;
mod prettier_support; mod prettier_support;
pub mod project_settings; pub mod project_settings;
pub mod search; pub mod search;
@ -172,7 +173,7 @@ struct DelayedDebounced {
cancel_channel: Option<oneshot::Sender<()>>, cancel_channel: Option<oneshot::Sender<()>>,
} }
enum LanguageServerToQuery { pub enum LanguageServerToQuery {
Primary, Primary,
Other(LanguageServerId), Other(LanguageServerId),
} }
@ -623,6 +624,7 @@ impl Project {
client.add_model_request_handler(Self::handle_open_buffer_by_path); client.add_model_request_handler(Self::handle_open_buffer_by_path);
client.add_model_request_handler(Self::handle_save_buffer); client.add_model_request_handler(Self::handle_save_buffer);
client.add_model_message_handler(Self::handle_update_diff_base); client.add_model_message_handler(Self::handle_update_diff_base);
client.add_model_request_handler(Self::handle_lsp_command::<lsp_ext_command::ExpandMacro>);
} }
pub fn local( pub fn local(
@ -5933,7 +5935,7 @@ impl Project {
.await; .await;
} }
fn request_lsp<R: LspCommand>( pub fn request_lsp<R: LspCommand>(
&self, &self,
buffer_handle: Model<Buffer>, buffer_handle: Model<Buffer>,
server: LanguageServerToQuery, server: LanguageServerToQuery,

View file

@ -103,23 +103,26 @@ pub struct EntryDetails {
} }
actions!( actions!(
ExpandSelectedEntry, project_panel,
CollapseSelectedEntry, [
CollapseAllEntries, ExpandSelectedEntry,
NewDirectory, CollapseSelectedEntry,
NewFile, CollapseAllEntries,
Copy, NewDirectory,
CopyPath, NewFile,
CopyRelativePath, Copy,
RevealInFinder, CopyPath,
OpenInTerminal, CopyRelativePath,
Cut, RevealInFinder,
Paste, OpenInTerminal,
Delete, Cut,
Rename, Paste,
Open, Delete,
ToggleFocus, Rename,
NewSearchInDirectory, Open,
ToggleFocus,
NewSearchInDirectory,
]
); );
pub fn init_settings(cx: &mut AppContext) { pub fn init_settings(cx: &mut AppContext) {

View file

@ -0,0 +1 @@
gpui::actions!(projects, [OpenRecent]);

View file

@ -1,9 +1,10 @@
mod highlighted_workspace_location; mod highlighted_workspace_location;
mod projects;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, View,
View, ViewContext, WeakView, ViewContext, WeakView,
}; };
use highlighted_workspace_location::HighlightedWorkspaceLocation; use highlighted_workspace_location::HighlightedWorkspaceLocation;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
@ -12,11 +13,11 @@ use std::sync::Arc;
use ui::{prelude::*, ListItem}; use ui::{prelude::*, ListItem};
use util::paths::PathExt; use util::paths::PathExt;
use workspace::{ use workspace::{
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, notifications::simple_message_notification::MessageNotification, ModalView, Workspace,
WORKSPACE_DB, WorkspaceLocation, WORKSPACE_DB,
}; };
actions!(OpenRecent); pub use projects::OpenRecent;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach(); cx.observe_new_views(RecentProjects::register).detach();
@ -26,6 +27,8 @@ pub struct RecentProjects {
picker: View<Picker<RecentProjectsDelegate>>, picker: View<Picker<RecentProjectsDelegate>>,
} }
impl ModalView for RecentProjects {}
impl RecentProjects { impl RecentProjects {
fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self { fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self {
Self { Self {

View file

@ -1,13 +1,16 @@
use std::{ops::Range, sync::Arc};
use anyhow::bail;
use futures::FutureExt; use futures::FutureExt;
use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; use gpui::{
AnyElement, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, IntoElement,
SharedString, StyledText, UnderlineStyle, WindowContext,
};
use language::{HighlightId, Language, LanguageRegistry}; use language::{HighlightId, Language, LanguageRegistry};
use std::{ops::Range, sync::Arc};
use theme::ActiveTheme;
use util::RangeExt; use util::RangeExt;
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum Highlight { pub enum Highlight {
Code,
Id(HighlightId), Id(HighlightId),
Highlight(HighlightStyle), Highlight(HighlightStyle),
Mention, Mention,
@ -28,24 +31,10 @@ impl From<HighlightId> for Highlight {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct RichText { pub struct RichText {
pub text: String, pub text: SharedString,
pub highlights: Vec<(Range<usize>, Highlight)>, pub highlights: Vec<(Range<usize>, Highlight)>,
pub region_ranges: Vec<Range<usize>>, pub link_ranges: Vec<Range<usize>>,
pub regions: Vec<RenderedRegion>, pub link_urls: Arc<[String]>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackgroundKind {
Code,
/// A mention background for non-self user.
Mention,
SelfMention,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenderedRegion {
pub background_kind: Option<BackgroundKind>,
pub link_url: Option<String>,
} }
/// Allows one to specify extra links to the rendered markdown, which can be used /// Allows one to specify extra links to the rendered markdown, which can be used
@ -56,94 +45,71 @@ pub struct Mention {
} }
impl RichText { impl RichText {
pub fn element( pub fn element(&self, id: ElementId, cx: &mut WindowContext) -> AnyElement {
&self, let theme = cx.theme();
// syntax: Arc<SyntaxTheme>, let code_background = theme.colors().surface_background;
// style: RichTextStyle,
// cx: &mut ViewContext<V>,
) -> AnyElement {
todo!();
// let mut region_id = 0; InteractiveText::new(
// let view_id = cx.view_id(); id,
StyledText::new(self.text.clone()).with_highlights(
// let regions = self.regions.clone(); &cx.text_style(),
self.highlights.iter().map(|(range, highlight)| {
// enum Markdown {} (
// Text::new(self.text.clone(), style.text.clone()) range.clone(),
// .with_highlights( match highlight {
// self.highlights Highlight::Code => HighlightStyle {
// .iter() background_color: Some(code_background),
// .filter_map(|(range, highlight)| { ..Default::default()
// let style = match highlight { },
// Highlight::Id(id) => id.style(&syntax)?, Highlight::Id(id) => HighlightStyle {
// Highlight::Highlight(style) => style.clone(), background_color: Some(code_background),
// Highlight::Mention => style.mention_highlight, ..id.style(&theme.syntax()).unwrap_or_default()
// Highlight::SelfMention => style.self_mention_highlight, },
// }; Highlight::Highlight(highlight) => *highlight,
// Some((range.clone(), style)) Highlight::Mention => HighlightStyle {
// }) font_weight: Some(FontWeight::BOLD),
// .collect::<Vec<_>>(), ..Default::default()
// ) },
// .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { Highlight::SelfMention => HighlightStyle {
// region_id += 1; font_weight: Some(FontWeight::BOLD),
// let region = regions[ix].clone(); ..Default::default()
// if let Some(url) = region.link_url { },
// cx.scene().push_cursor_region(CursorRegion { },
// bounds, )
// style: CursorStyle::PointingHand, }),
// }); ),
// cx.scene().push_mouse_region( )
// MouseRegion::new::<Markdown>(view_id, region_id, bounds) .on_click(self.link_ranges.clone(), {
// .on_click::<V, _>(MouseButton::Left, move |_, _, cx| { let link_urls = self.link_urls.clone();
// cx.platform().open_url(&url) move |ix, cx| cx.open_url(&link_urls[ix])
// }), })
// ); .into_any_element()
// }
// if let Some(region_kind) = &region.background_kind {
// let background = match region_kind {
// BackgroundKind::Code => style.code_background,
// BackgroundKind::Mention => style.mention_background,
// BackgroundKind::SelfMention => style.self_mention_background,
// };
// if background.is_some() {
// cx.scene().push_quad(gpui::Quad {
// bounds,
// background,
// border: Default::default(),
// corner_radii: (2.0).into(),
// });
// }
// }
// })
// .with_soft_wrap(true)
// .into_any()
} }
pub fn add_mention( // pub fn add_mention(
&mut self, // &mut self,
range: Range<usize>, // range: Range<usize>,
is_current_user: bool, // is_current_user: bool,
mention_style: HighlightStyle, // mention_style: HighlightStyle,
) -> anyhow::Result<()> { // ) -> anyhow::Result<()> {
if range.end > self.text.len() { // if range.end > self.text.len() {
bail!( // bail!(
"Mention in range {range:?} is outside of bounds for a message of length {}", // "Mention in range {range:?} is outside of bounds for a message of length {}",
self.text.len() // self.text.len()
); // );
} // }
if is_current_user { // if is_current_user {
self.region_ranges.push(range.clone()); // self.region_ranges.push(range.clone());
self.regions.push(RenderedRegion { // self.regions.push(RenderedRegion {
background_kind: Some(BackgroundKind::Mention), // background_kind: Some(BackgroundKind::Mention),
link_url: None, // link_url: None,
}); // });
} // }
self.highlights // self.highlights
.push((range, Highlight::Highlight(mention_style))); // .push((range, Highlight::Highlight(mention_style)));
Ok(()) // Ok(())
} // }
} }
pub fn render_markdown_mut( pub fn render_markdown_mut(
@ -151,7 +117,10 @@ pub fn render_markdown_mut(
mut mentions: &[Mention], mut mentions: &[Mention],
language_registry: &Arc<LanguageRegistry>, language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>, language: Option<&Arc<Language>>,
data: &mut RichText, text: &mut String,
highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>,
link_urls: &mut Vec<String>,
) { ) {
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
@ -163,18 +132,18 @@ pub fn render_markdown_mut(
let options = Options::all(); let options = Options::all();
for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() { for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() {
let prev_len = data.text.len(); let prev_len = text.len();
match event { match event {
Event::Text(t) => { Event::Text(t) => {
if let Some(language) = &current_language { if let Some(language) = &current_language {
render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); render_code(text, highlights, t.as_ref(), language);
} else { } else {
if let Some(mention) = mentions.first() { if let Some(mention) = mentions.first() {
if source_range.contains_inclusive(&mention.range) { if source_range.contains_inclusive(&mention.range) {
mentions = &mentions[1..]; mentions = &mentions[1..];
let range = (prev_len + mention.range.start - source_range.start) let range = (prev_len + mention.range.start - source_range.start)
..(prev_len + mention.range.end - source_range.start); ..(prev_len + mention.range.end - source_range.start);
data.highlights.push(( highlights.push((
range.clone(), range.clone(),
if mention.is_self_mention { if mention.is_self_mention {
Highlight::SelfMention Highlight::SelfMention
@ -182,19 +151,10 @@ pub fn render_markdown_mut(
Highlight::Mention Highlight::Mention
}, },
)); ));
data.region_ranges.push(range);
data.regions.push(RenderedRegion {
background_kind: Some(if mention.is_self_mention {
BackgroundKind::SelfMention
} else {
BackgroundKind::Mention
}),
link_url: None,
});
} }
} }
data.text.push_str(t.as_ref()); text.push_str(t.as_ref());
let mut style = HighlightStyle::default(); let mut style = HighlightStyle::default();
if bold_depth > 0 { if bold_depth > 0 {
style.font_weight = Some(FontWeight::BOLD); style.font_weight = Some(FontWeight::BOLD);
@ -203,11 +163,8 @@ pub fn render_markdown_mut(
style.font_style = Some(FontStyle::Italic); style.font_style = Some(FontStyle::Italic);
} }
if let Some(link_url) = link_url.clone() { if let Some(link_url) = link_url.clone() {
data.region_ranges.push(prev_len..data.text.len()); link_ranges.push(prev_len..text.len());
data.regions.push(RenderedRegion { link_urls.push(link_url);
link_url: Some(link_url),
background_kind: None,
});
style.underline = Some(UnderlineStyle { style.underline = Some(UnderlineStyle {
thickness: 1.0.into(), thickness: 1.0.into(),
..Default::default() ..Default::default()
@ -216,27 +173,25 @@ pub fn render_markdown_mut(
if style != HighlightStyle::default() { if style != HighlightStyle::default() {
let mut new_highlight = true; let mut new_highlight = true;
if let Some((last_range, last_style)) = data.highlights.last_mut() { if let Some((last_range, last_style)) = highlights.last_mut() {
if last_range.end == prev_len if last_range.end == prev_len
&& last_style == &Highlight::Highlight(style) && last_style == &Highlight::Highlight(style)
{ {
last_range.end = data.text.len(); last_range.end = text.len();
new_highlight = false; new_highlight = false;
} }
} }
if new_highlight { if new_highlight {
data.highlights highlights.push((prev_len..text.len(), Highlight::Highlight(style)));
.push((prev_len..data.text.len(), Highlight::Highlight(style)));
} }
} }
} }
} }
Event::Code(t) => { Event::Code(t) => {
data.text.push_str(t.as_ref()); text.push_str(t.as_ref());
data.region_ranges.push(prev_len..data.text.len());
if link_url.is_some() { if link_url.is_some() {
data.highlights.push(( highlights.push((
prev_len..data.text.len(), prev_len..text.len(),
Highlight::Highlight(HighlightStyle { Highlight::Highlight(HighlightStyle {
underline: Some(UnderlineStyle { underline: Some(UnderlineStyle {
thickness: 1.0.into(), thickness: 1.0.into(),
@ -246,19 +201,19 @@ pub fn render_markdown_mut(
}), }),
)); ));
} }
data.regions.push(RenderedRegion { if let Some(link_url) = link_url.clone() {
background_kind: Some(BackgroundKind::Code), link_ranges.push(prev_len..text.len());
link_url: link_url.clone(), link_urls.push(link_url);
}); }
} }
Event::Start(tag) => match tag { Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading(_, _, _) => { Tag::Heading(_, _, _) => {
new_paragraph(&mut data.text, &mut list_stack); new_paragraph(text, &mut list_stack);
bold_depth += 1; bold_depth += 1;
} }
Tag::CodeBlock(kind) => { Tag::CodeBlock(kind) => {
new_paragraph(&mut data.text, &mut list_stack); new_paragraph(text, &mut list_stack);
current_language = if let CodeBlockKind::Fenced(language) = kind { current_language = if let CodeBlockKind::Fenced(language) = kind {
language_registry language_registry
.language_for_name(language.as_ref()) .language_for_name(language.as_ref())
@ -278,18 +233,18 @@ pub fn render_markdown_mut(
let len = list_stack.len(); let len = list_stack.len();
if let Some((list_number, has_content)) = list_stack.last_mut() { if let Some((list_number, has_content)) = list_stack.last_mut() {
*has_content = false; *has_content = false;
if !data.text.is_empty() && !data.text.ends_with('\n') { if !text.is_empty() && !text.ends_with('\n') {
data.text.push('\n'); text.push('\n');
} }
for _ in 0..len - 1 { for _ in 0..len - 1 {
data.text.push_str(" "); text.push_str(" ");
} }
if let Some(number) = list_number { if let Some(number) = list_number {
data.text.push_str(&format!("{}. ", number)); text.push_str(&format!("{}. ", number));
*number += 1; *number += 1;
*has_content = false; *has_content = false;
} else { } else {
data.text.push_str("- "); text.push_str("- ");
} }
} }
} }
@ -304,8 +259,8 @@ pub fn render_markdown_mut(
Tag::List(_) => drop(list_stack.pop()), Tag::List(_) => drop(list_stack.pop()),
_ => {} _ => {}
}, },
Event::HardBreak => data.text.push('\n'), Event::HardBreak => text.push('\n'),
Event::SoftBreak => data.text.push(' '), Event::SoftBreak => text.push(' '),
_ => {} _ => {}
} }
} }
@ -317,18 +272,35 @@ pub fn render_markdown(
language_registry: &Arc<LanguageRegistry>, language_registry: &Arc<LanguageRegistry>,
language: Option<&Arc<Language>>, language: Option<&Arc<Language>>,
) -> RichText { ) -> RichText {
let mut data = RichText { // let mut data = RichText {
text: Default::default(), // text: Default::default(),
highlights: Default::default(), // highlights: Default::default(),
region_ranges: Default::default(), // region_ranges: Default::default(),
regions: Default::default(), // regions: Default::default(),
}; // };
render_markdown_mut(&block, mentions, language_registry, language, &mut data); let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_markdown_mut(
&block,
mentions,
language_registry,
language,
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
);
text.truncate(text.trim_end().len());
data.text = data.text.trim().to_string(); RichText {
text: SharedString::from(text),
data link_urls: link_urls.into(),
link_ranges,
highlights,
}
} }
pub fn render_code( pub fn render_code(
@ -339,11 +311,19 @@ pub fn render_code(
) { ) {
let prev_len = text.len(); let prev_len = text.len();
text.push_str(content); text.push_str(content);
let mut offset = 0;
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
if range.start > offset {
highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
}
highlights.push(( highlights.push((
prev_len + range.start..prev_len + range.end, prev_len + range.start..prev_len + range.end,
Highlight::Id(highlight_id), Highlight::Id(highlight_id),
)); ));
offset = range.end;
}
if offset < content.len() {
highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
} }
} }

View file

@ -178,7 +178,9 @@ message Envelope {
GetNotifications get_notifications = 150; GetNotifications get_notifications = 150;
GetNotificationsResponse get_notifications_response = 151; GetNotificationsResponse get_notifications_response = 151;
DeleteNotification delete_notification = 152; DeleteNotification delete_notification = 152;
MarkNotificationRead mark_notification_read = 153; // Current max MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max
} }
} }
@ -1619,3 +1621,14 @@ message Notification {
bool is_read = 6; bool is_read = 6;
optional bool response = 7; optional bool response = 7;
} }
message LspExtExpandMacro {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message LspExtExpandMacroResponse {
string name = 1;
string expansion = 2;
}

View file

@ -280,6 +280,8 @@ messages!(
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground), (UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground), (UsersResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
); );
request_messages!( request_messages!(
@ -363,6 +365,7 @@ request_messages!(
(UpdateParticipantLocation, Ack), (UpdateParticipantLocation, Ack),
(UpdateProject, Ack), (UpdateProject, Ack),
(UpdateWorktree, Ack), (UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
); );
entity_messages!( entity_messages!(
@ -415,6 +418,7 @@ entity_messages!(
UpdateProjectCollaborator, UpdateProjectCollaborator,
UpdateWorktree, UpdateWorktree,
UpdateWorktreeSettings, UpdateWorktreeSettings,
LspExtExpandMacro,
); );
entity_messages!( entity_messages!(

View file

@ -178,7 +178,9 @@ message Envelope {
GetNotifications get_notifications = 150; GetNotifications get_notifications = 150;
GetNotificationsResponse get_notifications_response = 151; GetNotificationsResponse get_notifications_response = 151;
DeleteNotification delete_notification = 152; DeleteNotification delete_notification = 152;
MarkNotificationRead mark_notification_read = 153; // Current max MarkNotificationRead mark_notification_read = 153;
LspExtExpandMacro lsp_ext_expand_macro = 154;
LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max
} }
} }
@ -1619,3 +1621,14 @@ message Notification {
bool is_read = 6; bool is_read = 6;
optional bool response = 7; optional bool response = 7;
} }
message LspExtExpandMacro {
uint64 project_id = 1;
uint64 buffer_id = 2;
Anchor position = 3;
}
message LspExtExpandMacroResponse {
string name = 1;
string expansion = 2;
}

View file

@ -280,6 +280,8 @@ messages!(
(UpdateWorktree, Foreground), (UpdateWorktree, Foreground),
(UpdateWorktreeSettings, Foreground), (UpdateWorktreeSettings, Foreground),
(UsersResponse, Foreground), (UsersResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
); );
request_messages!( request_messages!(
@ -363,6 +365,7 @@ request_messages!(
(UpdateParticipantLocation, Ack), (UpdateParticipantLocation, Ack),
(UpdateProject, Ack), (UpdateProject, Ack),
(UpdateWorktree, Ack), (UpdateWorktree, Ack),
(LspExtExpandMacro, LspExtExpandMacroResponse),
); );
entity_messages!( entity_messages!(
@ -415,6 +418,7 @@ entity_messages!(
UpdateProjectCollaborator, UpdateProjectCollaborator,
UpdateWorktree, UpdateWorktree,
UpdateWorktreeSettings, UpdateWorktreeSettings,
LspExtExpandMacro,
); );
entity_messages!( entity_messages!(

View file

@ -1075,8 +1075,7 @@ impl ProjectSearchView {
}); });
} }
// Re-activate the most recently activated search or the most recent if it has been closed. // Add another search tab to the workspace.
// If no search exists in the workspace, create a new one.
fn deploy( fn deploy(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &workspace::NewSearch, _: &workspace::NewSearch,
@ -1087,19 +1086,6 @@ impl ProjectSearchView {
state.0.retain(|project, _| project.is_upgradable(cx)) state.0.retain(|project, _| project.is_upgradable(cx))
}); });
let active_search = cx
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade());
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearchView>(cx)
.find(|search| search == active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
let query = workspace.active_item(cx).and_then(|item| { let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?; let editor = item.act_as::<Editor>(cx)?;
let query = editor.query_suggestion(cx); let query = editor.query_suggestion(cx);
@ -1110,28 +1096,22 @@ impl ProjectSearchView {
} }
}); });
let search = if let Some(existing) = existing { let settings = cx
workspace.activate_item(&existing, cx); .global::<ActiveSettings>()
existing .0
.get(&workspace.project().downgrade());
let settings = if let Some(settings) = settings {
Some(settings.clone())
} else { } else {
let settings = cx None
.global::<ActiveSettings>()
.0
.get(&workspace.project().downgrade());
let settings = if let Some(settings) = settings {
Some(settings.clone())
} else {
None
};
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(view.clone()), cx);
view
}; };
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(search.clone()), cx);
search.update(cx, |search, cx| { search.update(cx, |search, cx| {
if let Some(query) = query { if let Some(query) = query {
search.set_query(&query, cx); search.set_query(&query, cx);
@ -2306,29 +2286,86 @@ pub mod tests {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
}); });
deterministic.run_until_parked();
let Some(search_view_2) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
let search_view_id_2 = search_view_2.id();
assert_ne!(
search_view_2, search_view,
"New search view should be open after `workspace::NewSearch` event"
);
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
assert_eq!( assert_eq!(
search_view search_view
.results_editor .results_editor
.update(cx, |editor, cx| editor.display_text(cx)), .update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row" "Results of the first search view should not update too"
); );
assert!( assert!(
search_view.query_editor.is_focused(cx), !search_view.query_editor.is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row" "Focus should be moved away from the first search view"
);
});
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2.query_editor.read(cx).text(cx),
"two",
"New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
);
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"",
"No search results should be in the 2nd view yet, as we did not spawn a search for it"
);
assert!(
search_view_2.query_editor.is_focused(cx),
"Focus should be moved into query editor fo the new window"
);
});
search_view_2.update(cx, |search_view_2, cx| {
search_view_2
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
search_view_2.search(cx);
});
deterministic.run_until_parked();
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst FOUR: usize = one::ONE + three::THREE;",
"New search view with the updated query should have new search results"
);
assert!(
search_view_2.results_editor.is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
); );
}); });
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
}) })
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| { search_view_2.update(cx, |search_view_2, cx| {
assert!( assert!(
search_view.results_editor.is_focused(cx), search_view_2.results_editor.is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event", "Search view with matching query should switch focus to the results editor after the toggle focus event",
); );
}); });

View file

@ -10,15 +10,15 @@ use collections::HashMap;
use editor::{Editor, EditorMode}; use editor::{Editor, EditorMode};
use futures::channel::oneshot; use futures::channel::oneshot;
use gpui::{ use gpui::{
actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView, actions, div, impl_actions, red, Action, AppContext, Div, EventEmitter, FocusableView,
InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task, InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, Styled,
View, ViewContext, VisualContext as _, WeakView, WindowContext, Subscription, Task, View, ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use project::search::SearchQuery; use project::search::SearchQuery;
use serde::Deserialize; use serde::Deserialize;
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use ui::{h_stack, Clickable, Icon, IconButton, IconElement}; use ui::{h_stack, ButtonCommon, Clickable, Icon, IconButton, IconElement, Tooltip};
use util::ResultExt; use util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,
@ -26,12 +26,14 @@ use workspace::{
ToolbarItemLocation, ToolbarItemView, ToolbarItemLocation, ToolbarItemView,
}; };
#[derive(PartialEq, Clone, Deserialize, Default, Action)] #[derive(PartialEq, Clone, Deserialize)]
pub struct Deploy { pub struct Deploy {
pub focus: bool, pub focus: bool,
} }
actions!(Dismiss, FocusEditor); impl_actions!(buffer_search, [Deploy]);
actions!(buffer_search, [Dismiss, FocusEditor]);
pub enum Event { pub enum Event {
UpdateLocation, UpdateLocation,
@ -131,13 +133,7 @@ impl Render for BufferSearchBar {
let search_button_for_mode = |mode| { let search_button_for_mode = |mode| {
let is_active = self.current_mode == mode; let is_active = self.current_mode == mode;
render_search_mode_button( render_search_mode_button(mode, is_active)
mode,
is_active,
cx.listener(move |this, _, cx| {
this.activate_search_mode(mode, cx);
}),
)
}; };
let search_option_button = |option| { let search_option_button = |option| {
let is_active = self.search_options.contains(option); let is_active = self.search_options.contains(option);
@ -163,23 +159,35 @@ impl Render for BufferSearchBar {
}); });
let should_show_replace_input = self.replace_enabled && supported_options.replacement; let should_show_replace_input = self.replace_enabled && supported_options.replacement;
let replace_all = should_show_replace_input let replace_all = should_show_replace_input
.then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll, "Replace all"));
let replace_next = should_show_replace_input let replace_next = should_show_replace_input.then(|| {
.then(|| super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext)); super::render_replace_button(ReplaceNext, ui::Icon::ReplaceNext, "Replace next")
});
let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
let mut key_context = KeyContext::default();
key_context.add("BufferSearchBar");
if in_replace {
key_context.add("in_replace");
}
h_stack() h_stack()
.key_context("BufferSearchBar") .key_context(key_context)
.on_action(cx.listener(Self::previous_history_query)) .on_action(cx.listener(Self::previous_history_query))
.on_action(cx.listener(Self::next_history_query)) .on_action(cx.listener(Self::next_history_query))
.on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::select_next_match)) .on_action(cx.listener(Self::select_next_match))
.on_action(cx.listener(Self::select_prev_match)) .on_action(cx.listener(Self::select_prev_match))
.on_action(cx.listener(|this, _: &ActivateRegexMode, cx| {
this.activate_search_mode(SearchMode::Regex, cx);
}))
.on_action(cx.listener(|this, _: &ActivateTextMode, cx| {
this.activate_search_mode(SearchMode::Text, cx);
}))
.when(self.supported_options().replacement, |this| { .when(self.supported_options().replacement, |this| {
this.on_action(cx.listener(Self::toggle_replace)) this.on_action(cx.listener(Self::toggle_replace))
.when(in_replace, |this| { .when(in_replace, |this| {
this.key_context("in_replace") this.on_action(cx.listener(Self::replace_next))
.on_action(cx.listener(Self::replace_next))
.on_action(cx.listener(Self::replace_all)) .on_action(cx.listener(Self::replace_all))
}) })
}) })
@ -238,21 +246,19 @@ impl Render for BufferSearchBar {
h_stack() h_stack()
.gap_0p5() .gap_0p5()
.flex_none() .flex_none()
.child(self.render_action_button(cx)) .child(self.render_action_button())
.children(match_count) .children(match_count)
.child(render_nav_button( .child(render_nav_button(
ui::Icon::ChevronLeft, ui::Icon::ChevronLeft,
self.active_match_index.is_some(), self.active_match_index.is_some(),
cx.listener(move |this, _, cx| { "Select previous match",
this.select_prev_match(&Default::default(), cx); &SelectPrevMatch,
}),
)) ))
.child(render_nav_button( .child(render_nav_button(
ui::Icon::ChevronRight, ui::Icon::ChevronRight,
self.active_match_index.is_some(), self.active_match_index.is_some(),
cx.listener(move |this, _, cx| { "Select next match",
this.select_next_match(&Default::default(), cx); &SelectNextMatch,
}),
)), )),
) )
} }
@ -597,14 +603,10 @@ impl BufferSearchBar {
self.update_matches(cx) self.update_matches(cx)
} }
fn render_action_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_action_button(&self) -> impl IntoElement {
// let tooltip_style = theme.tooltip.clone(); IconButton::new("select-all", ui::Icon::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
// let style = theme.search.action_button.clone(); .tooltip(|cx| Tooltip::for_action("Select all matches", &SelectAllMatches, cx))
IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| {
this.select_all_matches(&SelectAllMatches, cx);
}))
} }
pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) { pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {

View file

@ -1,3 +1,7 @@
use gpui::{Action, SharedString};
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config // TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode { pub enum SearchMode {
@ -15,6 +19,16 @@ impl SearchMode {
SearchMode::Regex => "Regex", SearchMode::Regex => "Regex",
} }
} }
pub(crate) fn tooltip(&self) -> SharedString {
format!("Activate {} Mode", self.label()).into()
}
pub(crate) fn action(&self) -> Box<dyn Action> {
match self {
SearchMode::Text => ActivateTextMode.boxed_clone(),
SearchMode::Semantic => ActivateSemanticMode.boxed_clone(),
SearchMode::Regex => ActivateRegexMode.boxed_clone(),
}
}
} }
pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {

View file

@ -1,8 +1,7 @@
use crate::{ use crate::{
history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateSemanticMode, history::SearchHistory, mode::SearchMode, ActivateRegexMode, ActivateTextMode, CycleMode,
ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions,
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
ToggleWholeWord,
}; };
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use collections::HashMap; use collections::HashMap;
@ -45,7 +44,10 @@ use workspace::{
WorkspaceId, WorkspaceId,
}; };
actions!(SearchInNew, ToggleFocus, NextField, ToggleFilters,); actions!(
project_search,
[SearchInNew, ToggleFocus, NextField, ToggleFilters]
);
#[derive(Default)] #[derive(Default)]
struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>); struct ActiveSearches(HashMap<WeakModel<Project>, WeakView<ProjectSearchView>>);
@ -58,7 +60,9 @@ pub fn init(cx: &mut AppContext) {
cx.set_global(ActiveSearches::default()); cx.set_global(ActiveSearches::default());
cx.set_global(ActiveSettings::default()); cx.set_global(ActiveSettings::default());
cx.observe_new_views(|workspace: &mut Workspace, _cx| { cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(ProjectSearchView::deploy); workspace
.register_action(ProjectSearchView::deploy)
.register_action(ProjectSearchBar::search_in_new);
}) })
.detach(); .detach();
} }
@ -1007,8 +1011,7 @@ impl ProjectSearchView {
}); });
} }
// Re-activate the most recently activated search or the most recent if it has been closed. // Add another search tab to the workspace.
// If no search exists in the workspace, create a new one.
fn deploy( fn deploy(
workspace: &mut Workspace, workspace: &mut Workspace,
_: &workspace::NewSearch, _: &workspace::NewSearch,
@ -1019,20 +1022,6 @@ impl ProjectSearchView {
state.0.retain(|project, _| project.is_upgradable()) state.0.retain(|project, _| project.is_upgradable())
}); });
let active_search = cx
.global::<ActiveSearches>()
.0
.get(&workspace.project().downgrade())
.and_then(WeakView::upgrade);
let existing = active_search
.and_then(|active_search| {
workspace
.items_of_type::<ProjectSearchView>(cx)
.find(|search| search == &active_search)
})
.or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
let query = workspace.active_item(cx).and_then(|item| { let query = workspace.active_item(cx).and_then(|item| {
let editor = item.act_as::<Editor>(cx)?; let editor = item.act_as::<Editor>(cx)?;
let query = editor.query_suggestion(cx); let query = editor.query_suggestion(cx);
@ -1043,28 +1032,22 @@ impl ProjectSearchView {
} }
}); });
let search = if let Some(existing) = existing { let settings = cx
workspace.activate_item(&existing, cx); .global::<ActiveSettings>()
existing .0
.get(&workspace.project().downgrade());
let settings = if let Some(settings) = settings {
Some(settings.clone())
} else { } else {
let settings = cx None
.global::<ActiveSettings>()
.0
.get(&workspace.project().downgrade());
let settings = if let Some(settings) = settings {
Some(settings.clone())
} else {
None
};
let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let view = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(view.clone()), cx);
view
}; };
let model = cx.build_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let search = cx.build_view(|cx| ProjectSearchView::new(model, cx, settings));
workspace.add_item(Box::new(search.clone()), cx);
search.update(cx, |search, cx| { search.update(cx, |search, cx| {
if let Some(query) = query { if let Some(query) = query {
search.set_query(&query, cx); search.set_query(&query, cx);
@ -2436,29 +2419,86 @@ pub mod tests {
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
}); });
deterministic.run_until_parked();
let Some(search_view_2) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search event trigger")
};
let search_view_id_2 = search_view_2.id();
assert_ne!(
search_view_2, search_view,
"New search view should be open after `workspace::NewSearch` event"
);
search_view.update(cx, |search_view, cx| { search_view.update(cx, |search_view, cx| {
assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query");
assert_eq!( assert_eq!(
search_view search_view
.results_editor .results_editor
.update(cx, |editor, cx| editor.display_text(cx)), .update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"Results should be unchanged after search view 2nd open in a row" "Results of the first search view should not update too"
); );
assert!( assert!(
search_view.query_editor.is_focused(cx), !search_view.query_editor.is_focused(cx),
"Focus should be moved into query editor again after search view 2nd open in a row" "Focus should be moved away from the first search view"
);
});
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2.query_editor.read(cx).text(cx),
"two",
"New search view should get the query from the text cursor was at during the event spawn (first search view's first result)"
);
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"",
"No search results should be in the 2nd view yet, as we did not spawn a search for it"
);
assert!(
search_view_2.query_editor.is_focused(cx),
"Focus should be moved into query editor fo the new window"
);
});
search_view_2.update(cx, |search_view_2, cx| {
search_view_2
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("FOUR", cx));
search_view_2.search(cx);
});
deterministic.run_until_parked();
search_view_2.update(cx, |search_view_2, cx| {
assert_eq!(
search_view_2
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst FOUR: usize = one::ONE + three::THREE;",
"New search view with the updated query should have new search results"
);
assert!(
search_view_2.results_editor.is_focused(cx),
"Search view with mismatching query should be focused after search results are available",
); );
}); });
cx.spawn(|mut cx| async move { cx.spawn(|mut cx| async move {
window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); window.dispatch_action(search_view_id_2, &ToggleFocus, &mut cx);
}) })
.detach(); .detach();
deterministic.run_until_parked(); deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| { search_view_id_2.update(cx, |search_view_2, cx| {
assert!( assert!(
search_view.results_editor.is_focused(cx), search_view_2.results_editor.is_focused(cx),
"Search view with matching query should switch focus to the results editor after the toggle focus event", "Search view with matching query should switch focus to the results editor after the toggle focus event",
); );
}); });

View file

@ -3,7 +3,7 @@ pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement}; use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode; pub use mode::SearchMode;
use project::search::SearchQuery; use project::search::SearchQuery;
use ui::prelude::*; use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, Icon, IconButton}; use ui::{ButtonStyle, Icon, IconButton};
//pub use project_search::{ProjectSearchBar, ProjectSearchView}; //pub use project_search::{ProjectSearchBar, ProjectSearchView};
// use theme::components::{ // use theme::components::{
@ -22,20 +22,23 @@ pub fn init(cx: &mut AppContext) {
} }
actions!( actions!(
CycleMode, search,
ToggleWholeWord, [
ToggleCaseSensitive, CycleMode,
ToggleReplace, ToggleWholeWord,
SelectNextMatch, ToggleCaseSensitive,
SelectPrevMatch, ToggleReplace,
SelectAllMatches, SelectNextMatch,
NextHistoryQuery, SelectPrevMatch,
PreviousHistoryQuery, SelectAllMatches,
ActivateTextMode, NextHistoryQuery,
ActivateSemanticMode, PreviousHistoryQuery,
ActivateRegexMode, ActivateTextMode,
ReplaceAll, ActivateSemanticMode,
ReplaceNext, ActivateRegexMode,
ReplaceAll,
ReplaceNext,
]
); );
bitflags! { bitflags! {
@ -85,7 +88,7 @@ impl SearchOptions {
} }
pub fn as_button(&self, active: bool) -> impl IntoElement { pub fn as_button(&self, active: bool) -> impl IntoElement {
IconButton::new(0, self.icon()) IconButton::new(self.label(), self.icon())
.on_click({ .on_click({
let action = self.to_toggle_action(); let action = self.to_toggle_action();
move |_, cx| { move |_, cx| {
@ -94,26 +97,38 @@ impl SearchOptions {
}) })
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.when(active, |button| button.style(ButtonStyle::Filled)) .when(active, |button| button.style(ButtonStyle::Filled))
.tooltip({
let action = self.to_toggle_action();
let label: SharedString = format!("Toggle {}", self.label()).into();
move |cx| Tooltip::for_action(label.clone(), &*action, cx)
})
} }
} }
fn toggle_replace_button(active: bool) -> impl IntoElement { fn toggle_replace_button(active: bool) -> impl IntoElement {
// todo: add toggle_replace button // todo: add toggle_replace button
IconButton::new(0, Icon::Replace) IconButton::new("buffer-search-bar-toggle-replace-button", Icon::Replace)
.on_click(|_, cx| { .on_click(|_, cx| {
cx.dispatch_action(Box::new(ToggleReplace)); cx.dispatch_action(Box::new(ToggleReplace));
cx.notify(); cx.notify();
}) })
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.when(active, |button| button.style(ButtonStyle::Filled)) .when(active, |button| button.style(ButtonStyle::Filled))
.tooltip(|cx| Tooltip::for_action("Toggle replace", &ToggleReplace, cx))
} }
fn render_replace_button( fn render_replace_button(
action: impl Action + 'static + Send + Sync, action: impl Action + 'static + Send + Sync,
icon: Icon, icon: Icon,
tooltip: &'static str,
) -> impl IntoElement { ) -> impl IntoElement {
// todo: add tooltip let id: SharedString = format!("search-replace-{}", action.name()).into();
IconButton::new(0, icon).on_click(move |_, cx| { IconButton::new(id, icon)
cx.dispatch_action(action.boxed_clone()); .tooltip({
}) let action = action.boxed_clone();
move |cx| Tooltip::for_action(tooltip, &*action, cx)
})
.on_click(move |_, cx| {
cx.dispatch_action(action.boxed_clone());
})
} }

View file

@ -1,30 +1,36 @@
use gpui::{ClickEvent, IntoElement, WindowContext}; use gpui::{Action, IntoElement};
use ui::prelude::*; use ui::{prelude::*, Tooltip};
use ui::{Button, IconButton}; use ui::{Button, IconButton};
use crate::mode::SearchMode; use crate::mode::SearchMode;
pub(super) fn render_nav_button( pub(super) fn render_nav_button(
icon: ui::Icon, icon: ui::Icon,
_active: bool, active: bool,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, tooltip: &'static str,
action: &'static dyn Action,
) -> impl IntoElement { ) -> impl IntoElement {
// let tooltip_style = cx.theme().tooltip.clone(); IconButton::new(
// let cursor_style = if active { SharedString::from(format!("search-nav-button-{}", action.name())),
// CursorStyle::PointingHand icon,
// } else { )
// CursorStyle::default() .on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
// }; .tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
// enum NavButton {} .disabled(!active)
IconButton::new("search-nav-button", icon).on_click(on_click)
} }
pub(crate) fn render_search_mode_button( pub(crate) fn render_search_mode_button(mode: SearchMode, is_active: bool) -> Button {
mode: SearchMode,
is_active: bool,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Button {
Button::new(mode.label(), mode.label()) Button::new(mode.label(), mode.label())
.selected(is_active) .selected(is_active)
.on_click(on_click) .on_click({
let action = mode.action();
move |_, cx| {
cx.dispatch_action(action.boxed_clone());
}
})
.tooltip({
let action = mode.action();
let tooltip_text = mode.tooltip();
move |cx| Tooltip::for_action(tooltip_text.clone(), &*action, cx)
})
} }

View file

@ -1,7 +1,7 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets}; use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::BTreeMap; use collections::BTreeMap;
use gpui::{actions, Action, AppContext, KeyBinding, SharedString}; use gpui::{Action, AppContext, KeyBinding, SharedString};
use schemars::{ use schemars::{
gen::{SchemaGenerator, SchemaSettings}, gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation}, schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@ -137,10 +137,8 @@ impl KeymapFile {
} }
} }
actions!(NoAction);
fn no_action() -> Box<dyn gpui::Action> { fn no_action() -> Box<dyn gpui::Action> {
NoAction.boxed_clone() gpui::NoAction.boxed_clone()
} }
#[cfg(test)] #[cfg(test)]

View file

@ -4,7 +4,7 @@ use gpui::{
}; };
use ui::prelude::*; use ui::prelude::*;
actions!(ActionA, ActionB, ActionC); actions!(focus, [ActionA, ActionB, ActionC]);
pub struct FocusStory { pub struct FocusStory {
child_1_focus: FocusHandle, child_1_focus: FocusHandle,

View file

@ -28,6 +28,7 @@ pub enum ComponentStory {
ListHeader, ListHeader,
ListItem, ListItem,
Scroll, Scroll,
Tab,
Text, Text,
ZIndex, ZIndex,
Picker, Picker,
@ -53,6 +54,7 @@ impl ComponentStory {
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(), Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
Self::Scroll => ScrollStory::view(cx).into(), Self::Scroll => ScrollStory::view(cx).into(),
Self::Text => TextStory::view(cx).into(), Self::Text => TextStory::view(cx).into(),
Self::Tab => cx.build_view(|_| ui::TabStory).into(),
Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(),
Self::Picker => PickerStory::new(cx).into(), Self::Picker => PickerStory::new(cx).into(),
} }

View file

@ -50,7 +50,7 @@ use std::{
use thiserror::Error; use thiserror::Error;
use gpui::{ use gpui::{
px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke, actions, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Point, ScrollWheelEvent, Size, Task, TouchPhase, Point, ScrollWheelEvent, Size, Task, TouchPhase,
}; };
@ -58,6 +58,19 @@ use gpui::{
use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
use lazy_static::lazy_static; use lazy_static::lazy_static;
actions!(
terminal,
[
Clear,
Copy,
Paste,
ShowCharacterPalette,
SearchTest,
SendText,
SendKeystroke,
]
);
///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
///Scroll multiplier that is set to 3 by default. This will be removed when I ///Scroll multiplier that is set to 3 by default. This will be removed when I
///Implement scroll bars. ///Implement scroll bars.

View file

@ -1,11 +1,11 @@
use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
use gpui::{ use gpui::{
black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext, black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext,
AvailableSpace, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, AvailableSpace, Bounds, DispatchPhase, Element, ElementId, ExternalPaths, FocusHandle, Font,
FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState,
LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels,
PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled,
TextRun, TextStyle, TextSystem, UnderlineStyle, View, WhiteSpace, WindowContext, TextRun, TextStyle, TextSystem, UnderlineStyle, WhiteSpace, WindowContext,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::CursorShape; use language::CursorShape;
@ -27,8 +27,6 @@ use ui::Tooltip;
use std::mem; use std::mem;
use std::{fmt::Debug, ops::RangeInclusive}; use std::{fmt::Debug, ops::RangeInclusive};
use crate::TerminalView;
///The information generated during layout that is necessary for painting ///The information generated during layout that is necessary for painting
pub struct LayoutState { pub struct LayoutState {
cells: Vec<LayoutCell>, cells: Vec<LayoutCell>,
@ -149,7 +147,6 @@ impl LayoutRect {
///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection?
pub struct TerminalElement { pub struct TerminalElement {
terminal: Model<Terminal>, terminal: Model<Terminal>,
terminal_view: View<TerminalView>,
focus: FocusHandle, focus: FocusHandle,
focused: bool, focused: bool,
cursor_visible: bool, cursor_visible: bool,
@ -168,7 +165,6 @@ impl StatefulInteractiveElement for TerminalElement {}
impl TerminalElement { impl TerminalElement {
pub fn new( pub fn new(
terminal: Model<Terminal>, terminal: Model<Terminal>,
terminal_view: View<TerminalView>,
focus: FocusHandle, focus: FocusHandle,
focused: bool, focused: bool,
cursor_visible: bool, cursor_visible: bool,
@ -176,7 +172,6 @@ impl TerminalElement {
) -> TerminalElement { ) -> TerminalElement {
TerminalElement { TerminalElement {
terminal, terminal,
terminal_view,
focused, focused,
focus: focus.clone(), focus: focus.clone(),
cursor_visible, cursor_visible,
@ -474,6 +469,7 @@ impl TerminalElement {
.size_full() .size_full()
.id("terminal-element") .id("terminal-element")
.tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx))
.into_any_element()
}); });
let TerminalContent { let TerminalContent {
@ -575,7 +571,7 @@ impl TerminalElement {
relative_highlighted_ranges, relative_highlighted_ranges,
mode: *mode, mode: *mode,
display_offset: *display_offset, display_offset: *display_offset,
hyperlink_tooltip: None, // todo!(tooltips) hyperlink_tooltip,
gutter, gutter,
} }
} }
@ -643,13 +639,11 @@ impl TerminalElement {
let connection = connection.clone(); let connection = connection.clone();
let focus = focus.clone(); let focus = focus.clone();
move |e, cx| { move |e, cx| {
if e.pressed_button.is_some() { if e.pressed_button.is_some() && focus.is_focused(cx) && !cx.has_active_drag() {
if focus.is_focused(cx) { connection.update(cx, |terminal, cx| {
connection.update(cx, |terminal, cx| { terminal.mouse_drag(e, origin, bounds);
terminal.mouse_drag(e, origin, bounds); cx.notify();
cx.notify(); })
})
}
} }
} }
}) })
@ -776,18 +770,11 @@ impl Element for TerminalElement {
(layout_id, interactive_state) (layout_id, interactive_state)
} }
fn paint( fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext<'_>) {
mut self,
bounds: Bounds<Pixels>,
state: &mut Self::State,
cx: &mut WindowContext<'_>,
) {
let mut layout = self.compute_layout(bounds, cx); let mut layout = self.compute_layout(bounds, cx);
let theme = cx.theme(); let theme = cx.theme();
let dispatch_context = self.terminal_view.read(cx).dispatch_context(cx);
self.interactivity().key_context = Some(dispatch_context);
cx.paint_quad( cx.paint_quad(
bounds, bounds,
Default::default(), Default::default(),
@ -806,7 +793,28 @@ impl Element for TerminalElement {
.map(|cursor| cursor.bounding_rect(origin)), .map(|cursor| cursor.bounding_rect(origin)),
}; };
let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx); let terminal_focus_handle = self.focus.clone();
let terminal_handle = self.terminal.clone();
let mut this: TerminalElement = self
.register_mouse_listeners(origin, layout.mode, bounds, cx)
.drag_over::<ExternalPaths>(|style| {
// todo!() why does not it work? z-index of elements?
style.bg(cx.theme().colors().ghost_element_hover)
})
.on_drop::<ExternalPaths>(move |external_paths, cx| {
cx.focus(&terminal_focus_handle);
let mut new_text = external_paths
.read(cx)
.paths()
.iter()
.map(|path| format!(" {path:?}"))
.join("");
new_text.push(' ');
terminal_handle.update(cx, |terminal, _| {
// todo!() long paths are not displayed properly albeit the text is there
terminal.paste(&new_text);
});
});
let interactivity = mem::take(&mut this.interactivity); let interactivity = mem::take(&mut this.interactivity);

View file

@ -24,7 +24,7 @@ use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(ToggleFocus); actions!(terminal_view, [ToggleFocus]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.observe_new_views( cx.observe_new_views(

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