Remove 2 suffix for language_tools, search, terminal_view, auto_update

Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-01-03 10:52:40 -08:00
parent 292b3397ab
commit 0ac8aae17b
51 changed files with 3900 additions and 14333 deletions

102
Cargo.lock generated
View File

@ -7,7 +7,7 @@ name = "activity_indicator"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update2",
"auto_update",
"editor2",
"futures 0.3.28",
"gpui2",
@ -392,7 +392,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"schemars",
"search2",
"search",
"semantic_index2",
"serde",
"serde_json",
@ -752,30 +752,6 @@ dependencies = [
[[package]]
name = "auto_update"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"db",
"gpui",
"isahc",
"lazy_static",
"log",
"menu",
"project",
"serde",
"serde_derive",
"serde_json",
"settings",
"smol",
"tempdir",
"theme",
"util",
"workspace",
]
[[package]]
name = "auto_update2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
@ -1119,7 +1095,7 @@ dependencies = [
"language2",
"outline2",
"project2",
"search2",
"search",
"settings2",
"theme2",
"ui2",
@ -1799,7 +1775,7 @@ name = "collab_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update2",
"auto_update",
"call2",
"channel2",
"client2",
@ -3047,7 +3023,7 @@ dependencies = [
"postage",
"project2",
"regex",
"search2",
"search",
"serde",
"serde_derive",
"settings2",
@ -4558,29 +4534,6 @@ dependencies = [
[[package]]
name = "language_tools"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"editor",
"env_logger",
"futures 0.3.28",
"gpui",
"language",
"lsp",
"project",
"serde",
"settings",
"theme",
"tree-sitter",
"unindent",
"util",
"workspace",
]
[[package]]
name = "language_tools2"
version = "0.1.0"
dependencies = [
"anyhow",
"client2",
@ -6562,7 +6515,7 @@ dependencies = [
"pretty_assertions",
"project2",
"schemars",
"search2",
"search",
"serde",
"serde_derive",
"serde_json",
@ -6761,7 +6714,7 @@ dependencies = [
"assistant2",
"editor2",
"gpui2",
"search2",
"search",
"ui2",
"workspace2",
]
@ -7768,35 +7721,6 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "search"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"client",
"collections",
"editor",
"futures 0.3.28",
"gpui",
"language",
"log",
"menu",
"postage",
"project",
"semantic_index",
"serde",
"serde_derive",
"serde_json",
"settings",
"smallvec",
"smol",
"theme",
"unindent",
"util",
"workspace",
]
[[package]]
name = "search2"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 1.3.2",
@ -9084,7 +9008,7 @@ dependencies = [
]
[[package]]
name = "terminal_view2"
name = "terminal_view"
version = "0.1.0"
dependencies = [
"anyhow",
@ -10345,7 +10269,7 @@ dependencies = [
"nvim-rs",
"parking_lot 0.11.2",
"project2",
"search2",
"search",
"serde",
"serde_derive",
"serde_json",
@ -11179,7 +11103,7 @@ dependencies = [
"async-tar",
"async-trait",
"audio2",
"auto_update2",
"auto_update",
"backtrace",
"breadcrumbs",
"call2",
@ -11213,7 +11137,7 @@ dependencies = [
"journal2",
"language2",
"language_selector",
"language_tools2",
"language_tools",
"lazy_static",
"libc",
"log",
@ -11237,7 +11161,7 @@ dependencies = [
"rsa 0.4.0",
"rust-embed",
"schemars",
"search2",
"search",
"semantic_index2",
"serde",
"serde_derive",
@ -11249,7 +11173,7 @@ dependencies = [
"smol",
"sum_tree",
"tempdir",
"terminal_view2",
"terminal_view",
"text2",
"theme2",
"theme_selector",

View File

@ -7,7 +7,6 @@ members = [
"crates/audio",
"crates/audio2",
"crates/auto_update",
"crates/auto_update2",
"crates/breadcrumbs",
"crates/call",
"crates/call2",
@ -57,7 +56,6 @@ members = [
"crates/language2",
"crates/language_selector",
"crates/language_tools",
"crates/language_tools2",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
@ -88,7 +86,6 @@ members = [
"crates/rpc",
"crates/rpc2",
"crates/search",
"crates/search2",
"crates/semantic_index",
"crates/semantic_index2",
"crates/settings",
@ -101,7 +98,7 @@ members = [
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
"crates/terminal_view2",
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme2",

View File

@ -9,7 +9,7 @@ path = "src/activity_indicator.rs"
doctest = false
[dependencies]
auto_update = { path = "../auto_update2", package = "auto_update2" }
auto_update = { path = "../auto_update" }
editor = { path = "../editor2", package = "editor2" }
language = { path = "../language2", package = "language2" }
gpui = { path = "../gpui2", package = "gpui2" }

View File

@ -19,7 +19,7 @@ language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }

View File

@ -9,14 +9,14 @@ path = "src/auto_update.rs"
doctest = false
[dependencies]
db = { path = "../db" }
client = { path = "../client" }
gpui = { path = "../gpui" }
menu = { path = "../menu" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
db = { package = "db2", path = "../db2" }
client = { package = "client2", path = "../client2" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }
util = { path = "../util" }
anyhow.workspace = true
isahc.workspace = true

View File

@ -3,18 +3,23 @@ mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
Task, WeakViewHandle,
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
ViewContext, VisualContext, WindowContext,
};
use isahc::AsyncBody;
use serde::Deserialize;
use serde_derive::Serialize;
use settings::{Setting, SettingsStore};
use smol::{fs::File, io::AsyncReadExt, process::Command};
use smol::io::AsyncReadExt;
use settings::{Settings, SettingsStore};
use smol::{fs::File, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::ReleaseChannel;
use util::channel::{AppCommitSha, ReleaseChannel};
use util::http::HttpClient;
use workspace::Workspace;
@ -42,9 +47,9 @@ pub enum AutoUpdateStatus {
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: AppVersion,
current_version: SemanticVersion,
http_client: Arc<dyn HttpClient>,
pending_poll: Option<Task<()>>,
pending_poll: Option<Task<Option<()>>>,
server_url: String,
}
@ -54,13 +59,9 @@ struct JsonRelease {
url: String,
}
impl Entity for AutoUpdater {
type Event = ();
}
struct AutoUpdateSetting(bool);
impl Setting for AutoUpdateSetting {
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>;
@ -68,7 +69,7 @@ impl Setting for AutoUpdateSetting {
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
_: &AppContext,
_: &mut AppContext,
) -> Result<Self> {
Ok(Self(
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
@ -77,18 +78,31 @@ impl Setting for AutoUpdateSetting {
}
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
settings::register::<AutoUpdateSetting>(cx);
AutoUpdateSetting::register(cx);
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
let auto_updater = cx.add_model(|cx| {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|_, action: &Check, cx| check(action, cx));
workspace.register_action(|_, action, cx| view_release_notes(action, cx));
// @nate - code to trigger update notification on launch
// todo!("remove this when Nate is done")
// workspace.show_notification(0, _cx, |cx| {
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
// });
})
.detach();
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url);
let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore, _>(move |updater, cx| {
if settings::get::<AutoUpdateSetting>(cx).0 {
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
@ -101,19 +115,22 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
updater
});
cx.set_global(Some(auto_updater));
cx.add_global_action(check);
cx.add_global_action(view_release_notes);
cx.add_action(UpdateNotification::dismiss);
}
}
pub fn check(_: &Check, cx: &mut AppContext) {
pub fn check(_: &Check, cx: &mut WindowContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
&["Ok"],
));
}
}
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
@ -122,31 +139,28 @@ fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
match cx.global::<ReleaseChannel>() {
ReleaseChannel::Dev => {}
ReleaseChannel::Nightly => {}
ReleaseChannel::Preview => cx
.platform()
.open_url(&format!("{server_url}/releases/preview/{current_version}")),
ReleaseChannel::Stable => cx
.platform()
.open_url(&format!("{server_url}/releases/stable/{current_version}")),
ReleaseChannel::Preview => {
cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
}
ReleaseChannel::Stable => {
cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
}
}
}
}
}
pub fn notify_of_any_new_update(
workspace: WeakViewHandle<Workspace>,
cx: &mut AppContext,
) -> Option<()> {
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|mut cx| async move {
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.add_view(|_| UpdateNotification::new(version))
cx.new_view(|_| UpdateNotification::new(version))
});
updater
.read(cx)
@ -162,12 +176,12 @@ pub fn notify_of_any_new_update(
}
impl AutoUpdater {
pub fn get(cx: &mut AppContext) -> Option<ModelHandle<Self>> {
cx.default_global::<Option<ModelHandle<Self>>>().clone()
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
cx.default_global::<Option<Model<Self>>>().clone()
}
fn new(
current_version: AppVersion,
current_version: SemanticVersion,
http_client: Arc<dyn HttpClient>,
server_url: String,
) -> Self {
@ -180,11 +194,11 @@ impl AutoUpdater {
}
}
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<()> {
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.spawn(|this, mut cx| async move {
loop {
this.update(&mut cx, |this, cx| this.poll(cx));
cx.background().timer(POLL_INTERVAL).await;
this.update(&mut cx, |this, cx| this.poll(cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
@ -198,7 +212,7 @@ impl AutoUpdater {
cx.notify();
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
let result = Self::update(this.clone(), cx.clone()).await;
let result = Self::update(this.upgrade()?, cx.clone()).await;
this.update(&mut cx, |this, cx| {
this.pending_poll = None;
if let Err(error) = result {
@ -206,7 +220,8 @@ impl AutoUpdater {
this.status = AutoUpdateStatus::Errored;
cx.notify();
}
});
})
.ok()
}));
}
@ -219,26 +234,26 @@ impl AutoUpdater {
cx.notify();
}
async fn update(this: ModelHandle<Self>, mut cx: AsyncAppContext) -> Result<()> {
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
(
this.http_client.clone(),
this.server_url.clone(),
this.current_version,
)
});
})?;
let mut url_string = format!(
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
);
cx.read(|cx| {
cx.update(|cx| {
if cx.has_global::<ReleaseChannel>() {
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
url_string += "&";
url_string += param;
}
}
});
})?;
let mut response = client.get(&url_string, Default::default(), true).await?;
@ -251,26 +266,32 @@ impl AutoUpdater {
let release: JsonRelease =
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
let latest_version = release.version.parse::<AppVersion>()?;
if latest_version <= current_version {
let should_download = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => cx
.try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
.unwrap_or(true),
_ => release.version.parse::<SemanticVersion>()? <= current_version,
};
if !should_download {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
});
})?;
return Ok(());
}
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Downloading;
cx.notify();
});
})?;
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let running_app_path = ZED_APP_PATH
.clone()
.map_or_else(|| cx.platform().app_path(), Ok)?;
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
@ -279,15 +300,15 @@ impl AutoUpdater {
let mut dmg_file = File::create(&dmg_path).await?;
let (installation_id, release_channel, telemetry) = cx.read(|cx| {
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
let release_channel = cx
.has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name());
let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
let telemetry = TelemetrySettings::get_global(cx).metrics;
(installation_id, release_channel, telemetry)
});
})?;
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
@ -302,7 +323,7 @@ impl AutoUpdater {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
});
})?;
let output = Command::new("hdiutil")
.args(&["attach", "-nobrowse"])
@ -348,7 +369,7 @@ impl AutoUpdater {
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated;
cx.notify();
});
})?;
Ok(())
}
@ -357,7 +378,7 @@ impl AutoUpdater {
should_show: bool,
cx: &AppContext,
) -> Task<Result<()>> {
cx.background().spawn(async move {
cx.background_executor().spawn(async move {
if should_show {
KEY_VALUE_STORE
.write_kvp(
@ -375,7 +396,7 @@ impl AutoUpdater {
}
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
cx.background().spawn(async move {
cx.background_executor().spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
.is_some())

View File

@ -1,106 +1,56 @@
use crate::ViewReleaseNotes;
use gpui::{
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
platform::{AppVersion, CursorStyle, MouseButton},
Element, Entity, View, ViewContext,
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
};
use menu::Cancel;
use util::channel::ReleaseChannel;
use workspace::notifications::Notification;
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
pub struct UpdateNotification {
version: AppVersion,
version: SemanticVersion,
}
pub enum Event {
Dismiss,
}
impl Entity for UpdateNotification {
type Event = Event;
}
impl View for UpdateNotification {
fn ui_name() -> &'static str {
"UpdateNotification"
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
let theme = theme::current(cx).clone();
let theme = &theme.update_notification;
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
Flex::column()
.with_child(
Flex::row()
.with_child(
Text::new(
format!("Updated to {app_name} {}", self.version),
theme.message.text.clone(),
)
.contained()
.with_style(theme.message.container)
.aligned()
.top()
.left()
.flex(1., true),
)
.with_child(
MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
let style = theme.dismiss_button.style_for(state);
Svg::new("icons/x.svg")
.with_color(style.color)
.constrained()
.with_width(style.icon_width)
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_width(style.button_width)
.with_height(style.button_width)
})
.with_padding(Padding::uniform(5.))
.on_click(MouseButton::Left, move |_, this, cx| {
this.dismiss(&Default::default(), cx)
})
.aligned()
.constrained()
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
.aligned()
.top()
.flex_float(),
),
)
.with_child({
let style = theme.action_message.style_for(state);
Text::new("View the release notes", style.text.clone())
.contained()
.with_style(style.container)
})
.contained()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, |_, _, cx| {
crate::view_release_notes(&Default::default(), cx)
})
.into_any_named("update notification")
}
}
impl Notification for UpdateNotification {
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
matches!(event, Event::Dismiss)
v_stack()
.on_action(cx.listener(UpdateNotification::dismiss))
.elevation_3(cx)
.p_4()
.child(
h_stack()
.justify_between()
.child(Label::new(format!(
"Updated to {app_name} {}",
self.version
)))
.child(
div()
.id("cancel")
.child(IconElement::new(Icon::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
),
)
.child(
div()
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
)
}
}
impl UpdateNotification {
pub fn new(version: AppVersion) -> Self {
pub fn new(version: SemanticVersion) -> Self {
Self { version }
}
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismiss);
cx.emit(DismissEvent);
}
}

View File

@ -1,29 +0,0 @@
[package]
name = "auto_update2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/auto_update.rs"
doctest = false
[dependencies]
db = { package = "db2", path = "../db2" }
client = { package = "client2", path = "../client2" }
gpui = { package = "gpui2", path = "../gpui2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }
util = { path = "../util" }
anyhow.workspace = true
isahc.workspace = true
lazy_static.workspace = true
log.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
smol.workspace = true
tempdir.workspace = true

View File

@ -1,405 +0,0 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
ViewContext, VisualContext, WindowContext,
};
use isahc::AsyncBody;
use serde::Deserialize;
use serde_derive::Serialize;
use smol::io::AsyncReadExt;
use settings::{Settings, SettingsStore};
use smol::{fs::File, process::Command};
use std::{ffi::OsString, sync::Arc, time::Duration};
use update_notification::UpdateNotification;
use util::channel::{AppCommitSha, ReleaseChannel};
use util::http::HttpClient;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
#[derive(Serialize)]
struct UpdateRequestBody {
installation_id: Option<Arc<str>>,
release_channel: Option<&'static str>,
telemetry: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AutoUpdateStatus {
Idle,
Checking,
Downloading,
Installing,
Updated,
Errored,
}
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: SemanticVersion,
http_client: Arc<dyn HttpClient>,
pending_poll: Option<Task<Option<()>>>,
server_url: String,
}
#[derive(Deserialize)]
struct JsonRelease {
version: String,
url: String,
}
struct AutoUpdateSetting(bool);
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>;
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
_: &mut AppContext,
) -> Result<Self> {
Ok(Self(
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
))
}
}
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|_, action: &Check, cx| check(action, cx));
workspace.register_action(|_, action, cx| view_release_notes(action, cx));
// @nate - code to trigger update notification on launch
// todo!("remove this when Nate is done")
// workspace.show_notification(0, _cx, |cx| {
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
// });
})
.detach();
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
let auto_updater = cx.new_model(|cx| {
let updater = AutoUpdater::new(version, http_client, server_url);
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
}
})
.detach();
updater
});
cx.set_global(Some(auto_updater));
}
}
pub fn check(_: &Check, cx: &mut WindowContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
drop(cx.prompt(
gpui::PromptLevel::Info,
"Auto-updates disabled for non-bundled app.",
&["Ok"],
));
}
}
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
let current_version = auto_updater.current_version;
if cx.has_global::<ReleaseChannel>() {
match cx.global::<ReleaseChannel>() {
ReleaseChannel::Dev => {}
ReleaseChannel::Nightly => {}
ReleaseChannel::Preview => {
cx.open_url(&format!("{server_url}/releases/preview/{current_version}"))
}
ReleaseChannel::Stable => {
cx.open_url(&format!("{server_url}/releases/stable/{current_version}"))
}
}
}
}
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
workspace.show_notification(0, cx, |cx| {
cx.new_view(|_| UpdateNotification::new(version))
});
updater
.read(cx)
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
})?;
}
anyhow::Ok(())
})
.detach();
None
}
impl AutoUpdater {
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
cx.default_global::<Option<Model<Self>>>().clone()
}
fn new(
current_version: SemanticVersion,
http_client: Arc<dyn HttpClient>,
server_url: String,
) -> Self {
Self {
status: AutoUpdateStatus::Idle,
current_version,
http_client,
server_url,
pending_poll: None,
}
}
pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.spawn(|this, mut cx| async move {
loop {
this.update(&mut cx, |this, cx| this.poll(cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
return;
}
self.status = AutoUpdateStatus::Checking;
cx.notify();
self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
let result = Self::update(this.upgrade()?, cx.clone()).await;
this.update(&mut cx, |this, cx| {
this.pending_poll = None;
if let Err(error) = result {
log::error!("auto-update failed: error:{:?}", error);
this.status = AutoUpdateStatus::Errored;
cx.notify();
}
})
.ok()
}));
}
pub fn status(&self) -> AutoUpdateStatus {
self.status
}
pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
self.status = AutoUpdateStatus::Idle;
cx.notify();
}
async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
let (client, server_url, current_version) = this.read_with(&cx, |this, _| {
(
this.http_client.clone(),
this.server_url.clone(),
this.current_version,
)
})?;
let mut url_string = format!(
"{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"
);
cx.update(|cx| {
if cx.has_global::<ReleaseChannel>() {
if let Some(param) = cx.global::<ReleaseChannel>().release_query_param() {
url_string += "&";
url_string += param;
}
}
})?;
let mut response = client.get(&url_string, Default::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading release")?;
let release: JsonRelease =
serde_json::from_slice(body.as_slice()).context("error deserializing release")?;
let should_download = match *RELEASE_CHANNEL {
ReleaseChannel::Nightly => cx
.try_read_global::<AppCommitSha, _>(|sha, _| release.version != sha.0)
.unwrap_or(true),
_ => release.version.parse::<SemanticVersion>()? <= current_version,
};
if !should_download {
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Idle;
cx.notify();
})?;
return Ok(());
}
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Downloading;
cx.notify();
})?;
let temp_dir = tempdir::TempDir::new("zed-auto-update")?;
let dmg_path = temp_dir.path().join("Zed.dmg");
let mount_path = temp_dir.path().join("Zed");
let running_app_path = ZED_APP_PATH
.clone()
.map_or_else(|| cx.update(|cx| cx.app_path())?, Ok)?;
let running_app_filename = running_app_path
.file_name()
.ok_or_else(|| anyhow!("invalid running app path"))?;
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let mut dmg_file = File::create(&dmg_path).await?;
let (installation_id, release_channel, telemetry) = cx.update(|cx| {
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
let release_channel = cx
.has_global::<ReleaseChannel>()
.then(|| cx.global::<ReleaseChannel>().display_name());
let telemetry = TelemetrySettings::get_global(cx).metrics;
(installation_id, release_channel, telemetry)
})?;
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
installation_id,
release_channel,
telemetry,
})?);
let mut response = client.get(&release.url, request_body, true).await?;
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
log::info!("downloaded update. path:{:?}", dmg_path);
this.update(&mut cx, |this, cx| {
this.status = AutoUpdateStatus::Installing;
cx.notify();
})?;
let output = Command::new("hdiutil")
.args(&["attach", "-nobrowse"])
.arg(&dmg_path)
.arg("-mountroot")
.arg(&temp_dir.path())
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to mount: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
let output = Command::new("rsync")
.args(&["-av", "--delete"])
.arg(&mounted_app_path)
.arg(&running_app_path)
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to copy app: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
let output = Command::new("hdiutil")
.args(&["detach"])
.arg(&mount_path)
.output()
.await?;
if !output.status.success() {
Err(anyhow!(
"failed to unmount: {:?}",
String::from_utf8_lossy(&output.stderr)
))?;
}
this.update(&mut cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated;
cx.notify();
})?;
Ok(())
}
fn set_should_show_update_notification(
&self,
should_show: bool,
cx: &AppContext,
) -> Task<Result<()>> {
cx.background_executor().spawn(async move {
if should_show {
KEY_VALUE_STORE
.write_kvp(
SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
"".to_string(),
)
.await?;
} else {
KEY_VALUE_STORE
.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
.await?;
}
Ok(())
})
}
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
cx.background_executor().spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
.is_some())
})
}
}

View File

@ -1,56 +0,0 @@
use gpui::{
div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
};
use menu::Cancel;
use util::channel::ReleaseChannel;
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
pub struct UpdateNotification {
version: SemanticVersion,
}
impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let app_name = cx.global::<ReleaseChannel>().display_name();
v_stack()
.on_action(cx.listener(UpdateNotification::dismiss))
.elevation_3(cx)
.p_4()
.child(
h_stack()
.justify_between()
.child(Label::new(format!(
"Updated to {app_name} {}",
self.version
)))
.child(
div()
.id("cancel")
.child(IconElement::new(Icon::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
),
)
.child(
div()
.id("notes")
.child(Label::new("View the release notes"))
.cursor_pointer()
.on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
)
}
}
impl UpdateNotification {
pub fn new(version: SemanticVersion) -> Self {
Self { version }
}
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
}

View File

@ -15,7 +15,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" }

View File

@ -22,7 +22,7 @@ test-support = [
]
[dependencies]
auto_update = { package = "auto_update2", path = "../auto_update2" }
auto_update = { path = "../auto_update" }
db = { package = "db2", path = "../db2" }
call = { package = "call2", path = "../call2" }
client = { package = "client2", path = "../client2" }

View File

@ -18,7 +18,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
ui = { package = "ui2", path = "../ui2" }

View File

@ -10,24 +10,25 @@ doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
settings = { path = "../settings" }
theme = { path = "../theme" }
language = { path = "../language" }
project = { path = "../project" }
workspace = { path = "../workspace" }
gpui = { path = "../gpui" }
editor = { package = "editor2", path = "../editor2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
workspace = { package = "workspace2", path = "../workspace2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
lsp = { path = "../lsp" }
lsp = { package = "lsp2", path = "../lsp2" }
futures.workspace = true
serde.workspace = true
anyhow.workspace = true
tree-sitter.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
env_logger.workspace = true
unindent.workspace = true

View File

@ -1,25 +1,20 @@
use collections::{HashMap, VecDeque};
use editor::{Editor, MoveToEnd};
use editor::{Editor, EditorEvent, MoveToEnd};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions,
elements::{
AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, Stack,
},
platform::{CursorStyle, MouseButton},
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
ViewContext, ViewHandle, WeakModelHandle,
actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription,
View, ViewContext, VisualContext, WeakModel, WindowContext,
};
use language::{LanguageServerId, LanguageServerName};
use lsp::IoKind;
use project::{search::SearchQuery, Project};
use std::{borrow::Cow, sync::Arc};
use theme::{ui, Theme};
use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchableItem, SearchableItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
const SEND_LINE: &str = "// Send:";
@ -27,8 +22,8 @@ const RECEIVE_LINE: &str = "// Receive:";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
pub struct LogStore {
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, IoKind, String)>,
projects: HashMap<WeakModel<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModel<Project>, LanguageServerId, IoKind, String)>,
}
struct ProjectState {
@ -49,19 +44,19 @@ struct LanguageServerRpcState {
}
pub struct LspLogView {
pub(crate) editor: ViewHandle<Editor>,
pub(crate) editor: View<Editor>,
editor_subscription: Subscription,
log_store: ModelHandle<LogStore>,
log_store: Model<LogStore>,
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
project: ModelHandle<Project>,
project: Model<Project>,
focus_handle: FocusHandle,
_log_store_subscriptions: Vec<Subscription>,
}
pub struct LspLogToolbarItemView {
log_view: Option<ViewHandle<LspLogView>>,
log_view: Option<View<LspLogView>>,
_log_view_subscription: Option<Subscription>,
menu_open: bool,
}
#[derive(Copy, Clone, PartialEq, Eq)]
@ -83,37 +78,30 @@ pub(crate) struct LogMenuItem {
actions!(debug, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
let log_store = cx.add_model(|cx| LogStore::new(cx));
let log_store = cx.new_model(|cx| LogStore::new(cx));
cx.subscribe_global::<WorkspaceCreated, _>({
let log_store = log_store.clone();
move |event, cx| {
let workspace = &event.0;
if let Some(workspace) = workspace.upgrade(cx) {
let project = workspace.read(cx).project().clone();
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(&project, cx);
});
}
}
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
let project = workspace.project();
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(&project, cx);
});
}
})
.detach();
cx.add_action(
move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| {
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item(
Box::new(cx.add_view(|cx| {
Box::new(cx.new_view(|cx| {
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
})),
cx,
);
}
},
);
});
})
.detach();
}
impl LogStore {
@ -123,28 +111,28 @@ impl LogStore {
projects: HashMap::default(),
io_tx,
};
cx.spawn_weak(|this, mut cx| async move {
cx.spawn(|this, mut cx| async move {
while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
if let Some(this) = this.upgrade(&cx) {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.on_io(project, server_id, io_kind, &message, cx);
});
})?;
}
}
anyhow::Ok(())
})
.detach();
.detach_and_log_err(cx);
this
}
pub fn add_project(&mut self, project: &ModelHandle<Project>, cx: &mut ModelContext<Self>) {
pub fn add_project(&mut self, project: &Model<Project>, cx: &mut ModelContext<Self>) {
let weak_project = project.downgrade();
self.projects.insert(
weak_project,
project.downgrade(),
ProjectState {
servers: HashMap::default(),
_subscriptions: [
cx.observe_release(&project, move |this, _, _| {
cx.observe_release(project, move |this, _, _| {
this.projects.remove(&weak_project);
}),
cx.subscribe(project, |this, project, event, cx| match event {
@ -166,7 +154,7 @@ impl LogStore {
fn add_language_server(
&mut self,
project: &ModelHandle<Project>,
project: &Model<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<&mut LanguageServerState> {
@ -194,22 +182,21 @@ impl LogStore {
server_state._io_logs_subscription = server.as_ref().map(|server| {
server.on_io(move |io_kind, message| {
io_tx
.unbounded_send((weak_project, id, io_kind, message.to_string()))
.unbounded_send((weak_project.clone(), id, io_kind, message.to_string()))
.ok();
})
});
let this = cx.weak_handle();
let this = cx.handle().downgrade();
let weak_project = project.downgrade();
server_state._lsp_logs_subscription = server.map(|server| {
let server_id = server.server_id();
server.on_notification::<lsp::notification::LogMessage, _>({
move |params, mut cx| {
if let Some((project, this)) =
weak_project.upgrade(&mut cx).zip(this.upgrade(&mut cx))
{
if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) {
this.update(&mut cx, |this, cx| {
this.add_language_server_log(&project, server_id, &params.message, cx);
});
})
.ok();
}
}
})
@ -219,7 +206,7 @@ impl LogStore {
fn add_language_server_log(
&mut self,
project: &ModelHandle<Project>,
project: &Model<Project>,
id: LanguageServerId,
message: &str,
cx: &mut ModelContext<Self>,
@ -251,7 +238,7 @@ impl LogStore {
fn remove_language_server(
&mut self,
project: &ModelHandle<Project>,
project: &Model<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<()> {
@ -263,7 +250,7 @@ impl LogStore {
fn server_logs(
&self,
project: &ModelHandle<Project>,
project: &Model<Project>,
server_id: LanguageServerId,
) -> Option<&VecDeque<String>> {
let weak_project = project.downgrade();
@ -274,7 +261,7 @@ impl LogStore {
fn enable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
project: &Model<Project>,
server_id: LanguageServerId,
) -> Option<&mut LanguageServerRpcState> {
let weak_project = project.downgrade();
@ -291,7 +278,7 @@ impl LogStore {
pub fn disable_rpc_trace_for_language_server(
&mut self,
project: &ModelHandle<Project>,
project: &Model<Project>,
server_id: LanguageServerId,
_: &mut ModelContext<Self>,
) -> Option<()> {
@ -304,7 +291,7 @@ impl LogStore {
fn on_io(
&mut self,
project: WeakModelHandle<Project>,
project: WeakModel<Project>,
language_server_id: LanguageServerId,
io_kind: IoKind,
message: &str,
@ -314,7 +301,7 @@ impl LogStore {
IoKind::StdOut => true,
IoKind::StdIn => false,
IoKind::StdErr => {
let project = project.upgrade(cx)?;
let project = project.upgrade()?;
let message = format!("stderr: {}", message.trim());
self.add_language_server_log(&project, language_server_id, &message, cx);
return Some(());
@ -365,8 +352,8 @@ impl LogStore {
impl LspLogView {
pub fn new(
project: ModelHandle<Project>,
log_store: ModelHandle<LogStore>,
project: Model<Project>,
log_store: Model<LogStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let server_id = log_store
@ -427,14 +414,25 @@ impl LspLogView {
}
});
let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
cx.focus_view(&log_view.editor);
});
let mut this = Self {
focus_handle,
editor,
editor_subscription,
project,
log_store,
current_server_id: None,
is_showing_rpc_trace: false,
_log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
_log_store_subscriptions: vec![
model_changes_subscription,
events_subscriptions,
focus_subscription,
],
};
if let Some(server_id) = server_id {
this.show_logs_for_server(server_id, cx);
@ -445,15 +443,20 @@ impl LspLogView {
fn editor_for_logs(
log_contents: String,
cx: &mut ViewContext<Self>,
) -> (ViewHandle<Editor>, Subscription) {
let editor = cx.add_view(|cx| {
let mut editor = Editor::multi_line(None, cx);
) -> (View<Editor>, Subscription) {
let editor = cx.new_view(|cx| {
let mut editor = Editor::multi_line(cx);
editor.set_text(log_contents, cx);
editor.move_to_end(&MoveToEnd, cx);
editor.set_read_only(true);
editor
});
let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
let editor_subscription = cx.subscribe(
&editor,
|_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
cx.emit(event.clone())
},
);
(editor, editor_subscription)
}
@ -516,6 +519,7 @@ impl LspLogView {
self.editor_subscription = editor_subscription;
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn show_rpc_trace_for_server(
@ -540,22 +544,24 @@ impl LspLogView {
.as_singleton()
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn_weak({
cx.spawn({
let buffer = cx.handle();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
});
})
}
})
.detach();
.detach_and_log_err(cx);
});
self.editor = editor;
self.editor_subscription = editor_subscription;
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn toggle_rpc_trace_for_server(
@ -588,33 +594,31 @@ fn log_contents(lines: &VecDeque<String>) -> String {
}
}
impl View for LspLogView {
fn ui_name() -> &'static str {
"LspLogView"
impl Render for LspLogView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor
.update(cx, |editor, cx| editor.render(cx).into_any_element())
}
}
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>) {
if cx.is_self_focused() {
cx.focus(&self.editor);
}
impl FocusableView for LspLogView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for LspLogView {
fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> AnyElement<V> {
Label::new("LSP Logs", style.label.clone()).into_any()
type Event = EditorEvent;
fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
Editor::to_item_events(event, f)
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
fn tab_content(&self, _: Option<usize>, _: bool, _: &WindowContext<'_>) -> AnyElement {
Label::new("LSP Logs").into_any_element()
}
fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
}
@ -622,15 +626,6 @@ impl Item for LspLogView {
impl SearchableItem for LspLogView {
type Match = <Editor as SearchableItem>::Match;
fn to_search_event(
&mut self,
event: &Self::Event,
cx: &mut ViewContext<Self>,
) -> Option<workspace::searchable::SearchEvent> {
self.editor
.update(cx, |editor, cx| editor.to_search_event(event, cx))
}
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |e, cx| e.clear_matches(cx))
}
@ -689,22 +684,21 @@ impl SearchableItem for LspLogView {
}
}
impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
impl ToolbarItemView for LspLogToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
self.log_view = Some(log_view.clone());
self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
cx.notify();
}));
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
return ToolbarItemLocation::PrimaryLeft;
}
}
self.log_view = None;
@ -713,15 +707,10 @@ impl ToolbarItemView for LspLogToolbarItemView {
}
}
impl View for LspLogToolbarItemView {
fn ui_name() -> &'static str {
"LspLogView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = theme::current(cx).clone();
let Some(log_view) = self.log_view.as_ref() else {
return Empty::new().into_any();
impl Render for LspLogToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(log_view) = self.log_view.clone() else {
return div();
};
let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
@ -736,99 +725,128 @@ impl View for LspLogToolbarItemView {
None
}
});
let server_selected = current_server.is_some();
enum LspLogScroll {}
enum Menu {}
let lsp_menu = Stack::new()
.with_child(Self::render_language_server_menu_header(
current_server,
&theme,
cx,
))
.with_children(if self.menu_open {
Some(
Overlay::new(
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
Flex::column()
.scrollable::<LspLogScroll>(0, None, cx)
.with_children(menu_rows.into_iter().map(|row| {
Self::render_language_server_menu_item(
row.server_id,
row.server_name,
&row.worktree_root_name,
row.rpc_trace_enabled,
row.logs_selected,
row.rpc_trace_selected,
&theme,
cx,
)
}))
.contained()
.with_style(theme.toolbar_dropdown_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.menu_open = false;
cx.notify()
}),
)
.with_hoverable(true)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left(),
)
} else {
None
})
.aligned()
.left()
.clipped();
enum LspCleanupButton {}
let log_cleanup_button =
MouseEventHandler::new::<LspCleanupButton, _>(1, cx, |state, cx| {
let theme = theme::current(cx).clone();
let style = theme
.workspace
.toolbar
.toggleable_text_tool
.in_state(server_selected)
.style_for(state);
Label::new("Clear", style.text.clone())
.aligned()
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0)
})
.on_click(MouseButton::Left, move |_, this, cx| {
if let Some(log_view) = this.log_view.as_ref() {
log_view.update(cx, |log_view, cx| {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(cx);
editor.set_read_only(true);
});
let log_toolbar_view = cx.view().clone();
let lsp_menu = popover_menu("LspLogView")
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
current_server
.and_then(|row| {
Some(Cow::Owned(format!(
"{} ({}) - {}",
row.server_name.0,
row.worktree_root_name,
if row.rpc_trace_selected {
RPC_MESSAGES
} else {
SERVER_LOGS
},
)))
})
}
})
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.right();
.unwrap_or_else(|| "No server selected".into()),
))
.menu(move |cx| {
let menu_rows = menu_rows.clone();
let log_view = log_view.clone();
let log_toolbar_view = log_toolbar_view.clone();
ContextMenu::build(cx, move |mut menu, cx| {
for (ix, row) in menu_rows.into_iter().enumerate() {
let server_selected = Some(row.server_id) == current_server_id;
menu = menu
.header(format!(
"{} ({})",
row.server_name.0, row.worktree_root_name
))
.entry(
SERVER_LOGS,
None,
cx.handler_for(&log_view, move |view, cx| {
view.show_logs_for_server(row.server_id, cx);
}),
);
if server_selected && row.logs_selected {
debug_assert_eq!(
Some(ix * 3 + 1),
menu.select_last(),
"Could not scroll to a just added LSP menu item"
);
}
Flex::row()
.with_child(lsp_menu)
.with_child(log_cleanup_button)
.contained()
.aligned()
.left()
.into_any_named("lsp log controls")
menu = menu.custom_entry(
{
let log_toolbar_view = log_toolbar_view.clone();
move |cx| {
h_stack()
.w_full()
.justify_between()
.child(Label::new(RPC_MESSAGES))
.child(
div().z_index(120).child(
Checkbox::new(
ix,
if row.rpc_trace_enabled {
Selection::Selected
} else {
Selection::Unselected
},
)
.on_click(cx.listener_for(
&log_toolbar_view,
move |view, selection, cx| {
let enabled = matches!(
selection,
Selection::Selected
);
view.toggle_logging_for_server(
row.server_id,
enabled,
cx,
);
cx.stop_propagation();
},
)),
),
)
.into_any_element()
}
},
cx.handler_for(&log_view, move |view, cx| {
view.show_rpc_trace_for_server(row.server_id, cx);
}),
);
if server_selected && row.rpc_trace_selected {
debug_assert_eq!(
Some(ix * 3 + 2),
menu.select_last(),
"Could not scroll to a just added LSP menu item"
);
}
}
menu
})
.into()
});
h_stack().size_full().child(lsp_menu).child(
div()
.child(
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|this, _, cx| {
if let Some(log_view) = this.log_view.as_ref() {
log_view.update(cx, |log_view, cx| {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(cx);
editor.set_read_only(true);
});
})
}
},
)),
)
.ml_2(),
)
}
}
@ -838,17 +856,11 @@ const SERVER_LOGS: &str = "Server Logs";
impl LspLogToolbarItemView {
pub fn new() -> Self {
Self {
menu_open: false,
log_view: None,
_log_view_subscription: None,
}
}
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
self.menu_open = !self.menu_open;
cx.notify();
}
fn toggle_logging_for_server(
&mut self,
id: LanguageServerId,
@ -862,144 +874,11 @@ impl LspLogToolbarItemView {
log_view.show_logs_for_server(id, cx);
cx.notify();
}
cx.focus(&log_view.focus_handle);
});
}
cx.notify();
}
fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx));
self.menu_open = false;
cx.notify();
}
}
fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext<Self>) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx));
self.menu_open = false;
cx.notify();
}
}
fn render_language_server_menu_header(
current_server: Option<LogMenuItem>,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
let label: Cow<str> = current_server
.and_then(|row| {
Some(
format!(
"{} ({}) - {}",
row.server_name.0,
row.worktree_root_name,
if row.rpc_trace_selected {
RPC_MESSAGES
} else {
SERVER_LOGS
},
)
.into(),
)
})
.unwrap_or_else(|| "No server selected".into());
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Label::new(label, style.text.clone())
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx);
})
}
fn render_language_server_menu_item(
id: LanguageServerId,
name: LanguageServerName,
worktree_root_name: &str,
rpc_trace_enabled: bool,
logs_selected: bool,
rpc_trace_selected: bool,
theme: &Arc<Theme>,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ActivateLog {}
enum ActivateRpcTrace {}
enum LanguageServerCheckbox {}
Flex::column()
.with_child({
let style = &theme.toolbar_dropdown_menu.section_header;
Label::new(
format!("{} ({})", name.0, worktree_root_name),
style.text.clone(),
)
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_child(
MouseEventHandler::new::<ActivateLog, _>(id.0, cx, move |state, _| {
let style = theme
.toolbar_dropdown_menu
.item
.in_state(logs_selected)
.style_for(state);
Label::new(SERVER_LOGS, style.text.clone())
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.show_logs_for_server(id, cx);
}),
)
.with_child(
MouseEventHandler::new::<ActivateRpcTrace, _>(id.0, cx, move |state, cx| {
let style = theme
.toolbar_dropdown_menu
.item
.in_state(rpc_trace_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(RPC_MESSAGES, style.text.clone())
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height),
)
.with_child(
ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
Empty::new(),
&theme.welcome.checkbox,
rpc_trace_enabled,
id.0,
cx,
move |this, enabled, cx| {
this.toggle_logging_for_server(id, enabled, cx);
},
)
.flex_float(),
)
.align_children_center()
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.toolbar_dropdown_menu.row_height)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.show_rpc_trace_for_server(id, cx);
}),
)
}
}
pub enum Event {
@ -1010,14 +889,7 @@ pub enum Event {
},
}
impl Entity for LogStore {
type Event = Event;
}
impl Entity for LspLogView {
type Event = editor::Event;
}
impl Entity for LspLogToolbarItemView {
type Event = ();
}
impl EventEmitter<Event> for LogStore {}
impl EventEmitter<Event> for LspLogView {}
impl EventEmitter<EditorEvent> for LspLogView {}
impl EventEmitter<SearchEvent> for LspLogView {}

View File

@ -4,7 +4,7 @@ use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, TestAppContext};
use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext};
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
use project::{FakeFs, Project};
use settings::SettingsStore;
@ -32,7 +32,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
}))
.await;
let fs = FakeFs::new(cx.background());
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/the-root",
json!({
@ -46,7 +46,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
project.languages().add(Arc::new(rust_language));
});
let log_store = cx.add_model(|cx| LogStore::new(cx));
let log_store = cx.new_model(|cx| LogStore::new(cx));
log_store.update(cx, |store, cx| store.add_project(&project, cx));
let _rust_buffer = project
@ -61,17 +61,17 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await;
let log_view = cx
.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx))
.root(cx);
let window = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
let log_view = window.root(cx).unwrap();
let mut cx = VisualTestContext::from_window(*window, cx);
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
message: "hello from the server".into(),
typ: lsp::MessageType::INFO,
});
cx.foreground().run_until_parked();
cx.executor().run_until_parked();
log_view.read_with(cx, |view, cx| {
log_view.update(&mut cx, |view, cx| {
assert_eq!(
view.menu_items(cx).unwrap(),
&[LogMenuItem {
@ -79,7 +79,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
server_name: LanguageServerName("the-rust-language-server".into()),
worktree_root_name: project
.read(cx)
.worktrees(cx)
.worktrees()
.next()
.unwrap()
.read(cx)
@ -95,11 +95,10 @@ async fn test_lsp_logs(cx: &mut TestAppContext) {
}
fn init_test(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
cx.update(|cx| {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
client::init_settings(cx);
Project::init_settings(cx);

View File

@ -1,85 +1,85 @@
use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
use gpui::{
actions,
elements::{
AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
},
fonts::TextStyle,
platform::{CursorStyle, MouseButton},
AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div,
EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model,
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
use std::{mem, ops::Range, sync::Arc};
use theme::{Theme, ThemeSettings};
use language::{Buffer, OwnedSyntaxLayerInfo};
use settings::Settings;
use std::{mem, ops::Range};
use theme::{ActiveTheme, ThemeSettings};
use tree_sitter::{Node, TreeCursor};
use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
use workspace::{
item::{Item, ItemHandle},
ToolbarItemLocation, ToolbarItemView, Workspace,
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
actions!(debug, [OpenSyntaxTreeView]);
pub fn init(cx: &mut AppContext) {
cx.add_action(
move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| {
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
let syntax_tree_view =
cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
workspace.add_item(Box::new(syntax_tree_view), cx);
},
);
cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx)
});
})
.detach();
}
pub struct SyntaxTreeView {
workspace_handle: WeakViewHandle<Workspace>,
workspace_handle: WeakView<Workspace>,
editor: Option<EditorState>,
mouse_y: Option<f32>,
line_height: Option<f32>,
list_state: UniformListState,
mouse_y: Option<Pixels>,
line_height: Option<Pixels>,
list_scroll_handle: UniformListScrollHandle,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
}
pub struct SyntaxTreeToolbarItemView {
tree_view: Option<ViewHandle<SyntaxTreeView>>,
tree_view: Option<View<SyntaxTreeView>>,
subscription: Option<gpui::Subscription>,
menu_open: bool,
}
struct EditorState {
editor: ViewHandle<Editor>,
editor: View<Editor>,
active_buffer: Option<BufferState>,
_subscription: gpui::Subscription,
}
#[derive(Clone)]
struct BufferState {
buffer: ModelHandle<Buffer>,
buffer: Model<Buffer>,
excerpt_id: ExcerptId,
active_layer: Option<OwnedSyntaxLayerInfo>,
}
impl SyntaxTreeView {
pub fn new(
workspace_handle: WeakViewHandle<Workspace>,
workspace_handle: WeakView<Workspace>,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
workspace_handle: workspace_handle.clone(),
list_state: UniformListState::default(),
list_scroll_handle: UniformListScrollHandle::new(),
editor: None,
mouse_y: None,
line_height: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
};
this.workspace_updated(active_item, cx);
cx.observe(
&workspace_handle.upgrade(cx).unwrap(),
&workspace_handle.upgrade().unwrap(),
|this, workspace, cx| {
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
},
@ -95,7 +95,7 @@ impl SyntaxTreeView {
cx: &mut ViewContext<Self>,
) {
if let Some(item) = active_item {
if item.id() != cx.view_id() {
if item.item_id() != cx.entity_id() {
if let Some(editor) = item.act_as::<Editor>(cx) {
self.set_editor(editor, cx);
}
@ -103,7 +103,7 @@ impl SyntaxTreeView {
}
}
fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
return;
@ -115,8 +115,8 @@ impl SyntaxTreeView {
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let did_reparse = match event {
editor::Event::Reparsed => true,
editor::Event::SelectionsChanged { .. } => false,
editor::EditorEvent::Reparsed => true,
editor::EditorEvent::SelectionsChanged { .. } => false,
_ => return,
};
this.editor_updated(did_reparse, cx);
@ -202,15 +202,15 @@ impl SyntaxTreeView {
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
self.list_scroll_handle.scroll_to_item(descendant_ix);
cx.notify();
Some(())
}
fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
let line_height = self.line_height?;
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
// Put the cursor at the beginning of the node.
@ -225,14 +225,14 @@ impl SyntaxTreeView {
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
if self.hovered_descendant_ix != Some(ix) {
self.hovered_descendant_ix = Some(ix);
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
vec![range],
|theme| theme.editor.document_highlight_write_background,
|theme| theme.editor_document_highlight_write_background,
cx,
);
});
@ -275,113 +275,48 @@ impl SyntaxTreeView {
Some(())
}
fn render_node(
cursor: &TreeCursor,
depth: u32,
selected: bool,
hovered: bool,
list_hovered: bool,
style: &TextStyle,
editor_theme: &theme::Editor,
cx: &AppContext,
) -> gpui::AnyElement<SyntaxTreeView> {
let node = cursor.node();
let mut range_style = style.clone();
let em_width = style.em_width(cx.font_cache());
let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
range_style.color = editor_theme.line_number;
let mut anonymous_node_style = style.clone();
let string_color = editor_theme
.syntax
.highlights
.iter()
.find_map(|(name, style)| (name == "string").then(|| style.color)?);
let property_color = editor_theme
.syntax
.highlights
.iter()
.find_map(|(name, style)| (name == "property").then(|| style.color)?);
if let Some(color) = string_color {
anonymous_node_style.color = color;
}
let mut row = Flex::row();
fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div {
let colors = cx.theme().colors();
let mut row = h_stack();
if let Some(field_name) = cursor.field_name() {
let mut field_style = style.clone();
if let Some(color) = property_color {
field_style.color = color;
}
row.add_children([
Label::new(field_name, field_style),
Label::new(": ", style.clone()),
]);
row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
}
let node = cursor.node();
return row
.with_child(
if node.is_named() {
Label::new(node.kind(), style.clone())
} else {
Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
}
.contained()
.with_margin_right(em_width),
)
.with_child(Label::new(format_node_range(node), range_style))
.contained()
.with_background_color(if selected {
editor_theme.selection.selection
} else if hovered && list_hovered {
editor_theme.active_line_background
.child(if node.is_named() {
Label::new(node.kind()).color(Color::Default)
} else {
Default::default()
Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
})
.with_padding_left(gutter_padding + depth as f32 * 18.0)
.into_any();
.child(
div()
.child(Label::new(format_node_range(node)).color(Color::Muted))
.pl_1(),
)
.text_bg(if selected {
colors.element_selected
} else {
Hsla::default()
})
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover));
}
}
impl Entity for SyntaxTreeView {
type Event = ();
}
impl View for SyntaxTreeView {
fn ui_name() -> &'static str {
"SyntaxTreeView"
}
fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
let settings = settings::get::<ThemeSettings>(cx);
let font_family_id = settings.buffer_font_family;
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
let font_properties = Default::default();
let font_id = cx
.font_cache()
.select_font(font_family_id, &font_properties)
.unwrap();
let font_size = settings.buffer_font_size(cx);
let editor_theme = settings.theme.editor.clone();
let style = TextStyle {
color: editor_theme.text_color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties: Default::default(),
underline: Default::default(),
soft_wrap: false,
};
let line_height = cx.font_cache().line_height(font_size);
impl Render for SyntaxTreeView {
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let line_height = cx
.text_style()
.line_height_in_pixels(settings.buffer_font_size(cx));
if Some(line_height) != self.line_height {
self.line_height = Some(line_height);
self.hover_state_changed(cx);
}
let mut rendered = div().flex_1();
if let Some(layer) = self
.editor
.as_ref()
@ -389,108 +324,118 @@ impl View for SyntaxTreeView {
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone();
let theme = editor_theme.clone();
return MouseEventHandler::new::<Self, _>(0, cx, move |state, cx| {
let list_hovered = state.hovered();
UniformList::new(
self.list_state.clone(),
layer.node().descendant_count(),
cx,
move |this, range, items, cx| {
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start as usize;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
break;
}
let list = uniform_list(
cx.view().clone(),
"SyntaxTreeView",
layer.node().descendant_count(),
move |this, range, cx| {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start as usize;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
items.push(Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
Some(descendant_ix) == this.hovered_descendant_ix,
list_hovered,
&style,
&theme,
cx,
));
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
break;
}
} else {
items.push(Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
cx,
));
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
}
},
)
})
.on_move(move |event, this, cx| {
let y = event.position.y() - event.region.origin_y();
this.mouse_y = Some(y);
this.hover_state_changed(cx);
})
.on_click(MouseButton::Left, move |event, this, cx| {
let y = event.position.y() - event.region.origin_y();
this.handle_click(y, cx);
})
.contained()
.with_background_color(editor_theme.background)
.into_any();
}
items
},
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())
.on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| {
tree_view.mouse_y = Some(event.position.y);
tree_view.hover_state_changed(cx);
}))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |tree_view, event: &MouseDownEvent, cx| {
tree_view.handle_click(event.position.y, cx);
}),
)
.text_bg(cx.theme().colors().background);
rendered = rendered.child(
canvas(move |bounds, cx| {
list.into_any_element().draw(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
)
})
.size_full(),
);
}
Empty::new().into_any()
rendered
}
}
impl EventEmitter<()> for SyntaxTreeView {}
impl FocusableView for SyntaxTreeView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for SyntaxTreeView {
fn tab_content<V: 'static>(
&self,
_: Option<usize>,
style: &theme::Tab,
_: &AppContext,
) -> gpui::AnyElement<V> {
Label::new("Syntax Tree", style.label.clone()).into_any()
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content(&self, _: Option<usize>, _: bool, _: &WindowContext<'_>) -> AnyElement {
Label::new("Syntax Tree").into_any_element()
}
fn clone_on_split(
&self,
_workspace_id: workspace::WorkspaceId,
_: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<Self>
) -> Option<View<Self>>
where
Self: Sized,
{
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
if let Some(editor) = &self.editor {
clone.set_editor(editor.editor.clone(), cx)
}
Some(clone)
Some(cx.new_view(|cx| {
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
if let Some(editor) = &self.editor {
clone.set_editor(editor.editor.clone(), cx)
}
clone
}))
}
}
impl SyntaxTreeToolbarItemView {
pub fn new() -> Self {
Self {
menu_open: false,
tree_view: None,
subscription: None,
}
}
fn render_menu(
&mut self,
cx: &mut ViewContext<'_, '_, Self>,
) -> Option<gpui::AnyElement<Self>> {
let theme = theme::current(cx).clone();
fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option<PopoverMenu<ContextMenu>> {
let tree_view = self.tree_view.as_ref()?;
let tree_view = tree_view.read(cx);
@ -499,51 +444,32 @@ impl SyntaxTreeToolbarItemView {
let active_layer = buffer_state.active_layer.clone()?;
let active_buffer = buffer_state.buffer.read(cx).snapshot();
enum Menu {}
let view = cx.view().clone();
Some(
Stack::new()
.with_child(Self::render_header(&theme, &active_layer, cx))
.with_children(self.menu_open.then(|| {
Overlay::new(
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
Flex::column()
.with_children(active_buffer.syntax_layers().enumerate().map(
|(ix, layer)| {
Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
},
))
.contained()
.with_style(theme.toolbar_dropdown_menu.container)
.constrained()
.with_width(400.)
.with_height(400.)
})
.on_down_out(MouseButton::Left, |_, this, cx| {
this.menu_open = false;
cx.notify()
}),
)
.with_hoverable(true)
.with_fit_mode(OverlayFitMode::SwitchAnchor)
.with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
.left()
}))
.aligned()
.left()
.clipped()
.into_any(),
popover_menu("Syntax Tree")
.trigger(Self::render_header(&active_layer))
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
menu = menu.entry(
format!(
"{} {}",
layer.language.name(),
format_node_range(layer.node())
),
None,
cx.handler_for(&view, move |view, cx| {
view.select_layer(layer_ix, cx);
}),
);
}
menu
})
.into()
}),
)
}
fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
self.menu_open = !self.menu_open;
cx.notify();
}
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
let tree_view = self.tree_view.as_ref()?;
tree_view.update(cx, |view, cx| {
@ -553,77 +479,16 @@ impl SyntaxTreeToolbarItemView {
let layer = snapshot.syntax_layers().nth(layer_ix)?;
buffer_state.active_layer = Some(layer.to_owned());
view.selected_descendant_ix = None;
self.menu_open = false;
cx.notify();
view.focus_handle.focus(cx);
Some(())
})
}
fn render_header(
theme: &Arc<Theme>,
active_layer: &OwnedSyntaxLayerInfo,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ToggleMenu {}
MouseEventHandler::new::<ToggleMenu, _>(0, cx, move |state, _| {
let style = theme.toolbar_dropdown_menu.header.style_for(state);
Flex::row()
.with_child(
Label::new(active_layer.language.name().to_string(), style.text.clone())
.contained()
.with_margin_right(style.secondary_text_spacing),
)
.with_child(Label::new(
format_node_range(active_layer.node()),
style
.secondary_text
.clone()
.unwrap_or_else(|| style.text.clone()),
))
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.toggle_menu(cx);
})
}
fn render_menu_item(
theme: &Arc<Theme>,
active_layer: &OwnedSyntaxLayerInfo,
layer: SyntaxLayerInfo,
layer_ix: usize,
cx: &mut ViewContext<Self>,
) -> impl Element<Self> {
enum ActivateLayer {}
MouseEventHandler::new::<ActivateLayer, _>(layer_ix, cx, move |state, _| {
let is_selected = layer.node() == active_layer.node();
let style = theme
.toolbar_dropdown_menu
.item
.in_state(is_selected)
.style_for(state);
Flex::row()
.with_child(
Label::new(layer.language.name().to_string(), style.text.clone())
.contained()
.with_margin_right(style.secondary_text_spacing),
)
.with_child(Label::new(
format_node_range(layer.node()),
style
.secondary_text
.clone()
.unwrap_or_else(|| style.text.clone()),
))
.contained()
.with_style(style.container)
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(MouseButton::Left, move |_, view, cx| {
view.select_layer(layer_ix, cx);
})
fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike {
ButtonLike::new("syntax tree header")
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
}
@ -639,35 +504,26 @@ fn format_node_range(node: Node) -> String {
)
}
impl Entity for SyntaxTreeToolbarItemView {
type Event = ();
}
impl View for SyntaxTreeToolbarItemView {
fn ui_name() -> &'static str {
"SyntaxTreeToolbarItemView"
}
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement {
self.render_menu(cx)
.unwrap_or_else(|| Empty::new().into_any())
.unwrap_or_else(|| popover_menu("Empty Syntax Tree"))
}
}
impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
impl ToolbarItemView for SyntaxTreeToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
self.menu_open = false;
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item {
if let Some(view) = item.downcast::<SyntaxTreeView>() {
self.tree_view = Some(view.clone());
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
return ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
};
return ToolbarItemLocation::PrimaryLeft;
}
}
self.tree_view = None;

View File

@ -1,34 +0,0 @@
[package]
name = "language_tools2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/language_tools.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" }
workspace = { package = "workspace2", path = "../workspace2" }
gpui = { package = "gpui2", path = "../gpui2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
lsp = { package = "lsp2", path = "../lsp2" }
futures.workspace = true
serde.workspace = true
anyhow.workspace = true
tree-sitter.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
env_logger.workspace = true
unindent.workspace = true

View File

@ -1,15 +0,0 @@
mod lsp_log;
mod syntax_tree_view;
#[cfg(test)]
mod lsp_log_tests;
use gpui::AppContext;
pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
pub fn init(cx: &mut AppContext) {
lsp_log::init(cx);
syntax_tree_view::init(cx);
}

View File

@ -1,895 +0,0 @@
use collections::{HashMap, VecDeque};
use editor::{Editor, EditorEvent, MoveToEnd};
use futures::{channel::mpsc, StreamExt};
use gpui::{
actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle,
FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription,
View, ViewContext, VisualContext, WeakModel, WindowContext,
};
use language::{LanguageServerId, LanguageServerName};
use lsp::IoKind;
use project::{search::SearchQuery, Project};
use std::{borrow::Cow, sync::Arc};
use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection};
use workspace::{
item::{Item, ItemHandle},
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
const SEND_LINE: &str = "// Send:";
const RECEIVE_LINE: &str = "// Receive:";
const MAX_STORED_LOG_ENTRIES: usize = 2000;
pub struct LogStore {
projects: HashMap<WeakModel<Project>, ProjectState>,
io_tx: mpsc::UnboundedSender<(WeakModel<Project>, LanguageServerId, IoKind, String)>,
}
struct ProjectState {
servers: HashMap<LanguageServerId, LanguageServerState>,
_subscriptions: [gpui::Subscription; 2],
}
struct LanguageServerState {
log_messages: VecDeque<String>,
rpc_state: Option<LanguageServerRpcState>,
_io_logs_subscription: Option<lsp::Subscription>,
_lsp_logs_subscription: Option<lsp::Subscription>,
}
struct LanguageServerRpcState {
rpc_messages: VecDeque<String>,
last_message_kind: Option<MessageKind>,
}
pub struct LspLogView {
pub(crate) editor: View<Editor>,
editor_subscription: Subscription,
log_store: Model<LogStore>,
current_server_id: Option<LanguageServerId>,
is_showing_rpc_trace: bool,
project: Model<Project>,
focus_handle: FocusHandle,
_log_store_subscriptions: Vec<Subscription>,
}
pub struct LspLogToolbarItemView {
log_view: Option<View<LspLogView>>,
_log_view_subscription: Option<Subscription>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum MessageKind {
Send,
Receive,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct LogMenuItem {
pub server_id: LanguageServerId,
pub server_name: LanguageServerName,
pub worktree_root_name: String,
pub rpc_trace_enabled: bool,
pub rpc_trace_selected: bool,
pub logs_selected: bool,
}
actions!(debug, [OpenLanguageServerLogs]);
pub fn init(cx: &mut AppContext) {
let log_store = cx.new_model(|cx| LogStore::new(cx));
cx.observe_new_views(move |workspace: &mut Workspace, cx| {
let project = workspace.project();
if project.read(cx).is_local() {
log_store.update(cx, |store, cx| {
store.add_project(&project, cx);
});
}
let log_store = log_store.clone();
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
let project = workspace.project().read(cx);
if project.is_local() {
workspace.add_item(
Box::new(cx.new_view(|cx| {
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
})),
cx,
);
}
});
})
.detach();
}
impl LogStore {
pub fn new(cx: &mut ModelContext<Self>) -> Self {
let (io_tx, mut io_rx) = mpsc::unbounded();
let this = Self {
projects: HashMap::default(),
io_tx,
};
cx.spawn(|this, mut cx| async move {
while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(&mut cx, |this, cx| {
this.on_io(project, server_id, io_kind, &message, cx);
})?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
this
}
pub fn add_project(&mut self, project: &Model<Project>, cx: &mut ModelContext<Self>) {
let weak_project = project.downgrade();
self.projects.insert(
project.downgrade(),
ProjectState {
servers: HashMap::default(),
_subscriptions: [
cx.observe_release(project, move |this, _, _| {
this.projects.remove(&weak_project);
}),
cx.subscribe(project, |this, project, event, cx| match event {
project::Event::LanguageServerAdded(id) => {
this.add_language_server(&project, *id, cx);
}
project::Event::LanguageServerRemoved(id) => {
this.remove_language_server(&project, *id, cx);
}
project::Event::LanguageServerLog(id, message) => {
this.add_language_server_log(&project, *id, message, cx);
}
_ => {}
}),
],
},
);
}
fn add_language_server(
&mut self,
project: &Model<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<&mut LanguageServerState> {
let project_state = self.projects.get_mut(&project.downgrade())?;
let server_state = project_state.servers.entry(id).or_insert_with(|| {
cx.notify();
LanguageServerState {
rpc_state: None,
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
_io_logs_subscription: None,
_lsp_logs_subscription: None,
}
});
let server = project.read(cx).language_server_for_id(id);
if let Some(server) = server.as_deref() {
if server.has_notification_handler::<lsp::notification::LogMessage>() {
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
return Some(server_state);
}
}
let weak_project = project.downgrade();
let io_tx = self.io_tx.clone();
server_state._io_logs_subscription = server.as_ref().map(|server| {
server.on_io(move |io_kind, message| {
io_tx
.unbounded_send((weak_project.clone(), id, io_kind, message.to_string()))
.ok();
})
});
let this = cx.handle().downgrade();
let weak_project = project.downgrade();
server_state._lsp_logs_subscription = server.map(|server| {
let server_id = server.server_id();
server.on_notification::<lsp::notification::LogMessage, _>({
move |params, mut cx| {
if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) {
this.update(&mut cx, |this, cx| {
this.add_language_server_log(&project, server_id, &params.message, cx);
})
.ok();
}
}
})
});
Some(server_state)
}
fn add_language_server_log(
&mut self,
project: &Model<Project>,
id: LanguageServerId,
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let language_server_state = match self
.projects
.get_mut(&project.downgrade())?
.servers
.get_mut(&id)
{
Some(existing_state) => existing_state,
None => self.add_language_server(&project, id, cx)?,
};
let log_lines = &mut language_server_state.log_messages;
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
log_lines.pop_front();
}
let message = message.trim();
log_lines.push_back(message.to_string());
cx.emit(Event::NewServerLogEntry {
id,
entry: message.to_string(),
is_rpc: false,
});
cx.notify();
Some(())
}
fn remove_language_server(
&mut self,
project: &Model<Project>,
id: LanguageServerId,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let project_state = self.projects.get_mut(&project.downgrade())?;
project_state.servers.remove(&id);
cx.notify();
Some(())
}
fn server_logs(
&self,
project: &Model<Project>,
server_id: LanguageServerId,
) -> Option<&VecDeque<String>> {
let weak_project = project.downgrade();
let project_state = self.projects.get(&weak_project)?;
let server_state = project_state.servers.get(&server_id)?;
Some(&server_state.log_messages)
}
fn enable_rpc_trace_for_language_server(
&mut self,
project: &Model<Project>,
server_id: LanguageServerId,
) -> Option<&mut LanguageServerRpcState> {
let weak_project = project.downgrade();
let project_state = self.projects.get_mut(&weak_project)?;
let server_state = project_state.servers.get_mut(&server_id)?;
let rpc_state = server_state
.rpc_state
.get_or_insert_with(|| LanguageServerRpcState {
rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
last_message_kind: None,
});
Some(rpc_state)
}
pub fn disable_rpc_trace_for_language_server(
&mut self,
project: &Model<Project>,
server_id: LanguageServerId,
_: &mut ModelContext<Self>,
) -> Option<()> {
let project = project.downgrade();
let project_state = self.projects.get_mut(&project)?;
let server_state = project_state.servers.get_mut(&server_id)?;
server_state.rpc_state.take();
Some(())
}
fn on_io(
&mut self,
project: WeakModel<Project>,
language_server_id: LanguageServerId,
io_kind: IoKind,
message: &str,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let is_received = match io_kind {
IoKind::StdOut => true,
IoKind::StdIn => false,
IoKind::StdErr => {
let project = project.upgrade()?;
let message = format!("stderr: {}", message.trim());
self.add_language_server_log(&project, language_server_id, &message, cx);
return Some(());
}
};
let state = self
.projects
.get_mut(&project)?
.servers
.get_mut(&language_server_id)?
.rpc_state
.as_mut()?;
let kind = if is_received {
MessageKind::Receive
} else {
MessageKind::Send
};
let rpc_log_lines = &mut state.rpc_messages;
if state.last_message_kind != Some(kind) {
let line_before_message = match kind {
MessageKind::Send => SEND_LINE,
MessageKind::Receive => RECEIVE_LINE,
};
rpc_log_lines.push_back(line_before_message.to_string());
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: line_before_message.to_string(),
is_rpc: true,
});
}
while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
rpc_log_lines.pop_front();
}
let message = message.trim();
rpc_log_lines.push_back(message.to_string());
cx.emit(Event::NewServerLogEntry {
id: language_server_id,
entry: message.to_string(),
is_rpc: true,
});
cx.notify();
Some(())
}
}
impl LspLogView {
pub fn new(
project: Model<Project>,
log_store: Model<LogStore>,
cx: &mut ViewContext<Self>,
) -> Self {
let server_id = log_store
.read(cx)
.projects
.get(&project.downgrade())
.and_then(|project| project.servers.keys().copied().next());
let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
(|| -> Option<()> {
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
if let Some(current_lsp) = this.current_server_id {
if !project_state.servers.contains_key(&current_lsp) {
if let Some(server) = project_state.servers.iter().next() {
if this.is_showing_rpc_trace {
this.show_rpc_trace_for_server(*server.0, cx)
} else {
this.show_logs_for_server(*server.0, cx)
}
} else {
this.current_server_id = None;
this.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(cx);
editor.set_read_only(true);
});
cx.notify();
}
}
} else {
if let Some(server) = project_state.servers.iter().next() {
if this.is_showing_rpc_trace {
this.show_rpc_trace_for_server(*server.0, cx)
} else {
this.show_logs_for_server(*server.0, cx)
}
}
}
Some(())
})();
cx.notify();
});
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
Event::NewServerLogEntry { id, entry, is_rpc } => {
if log_view.current_server_id == Some(*id) {
if (*is_rpc && log_view.is_showing_rpc_trace)
|| (!*is_rpc && !log_view.is_showing_rpc_trace)
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.handle_input(entry.trim(), cx);
editor.handle_input("\n", cx);
editor.set_read_only(true);
});
}
}
}
});
let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
let focus_handle = cx.focus_handle();
let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| {
cx.focus_view(&log_view.editor);
});
let mut this = Self {
focus_handle,
editor,
editor_subscription,
project,
log_store,
current_server_id: None,
is_showing_rpc_trace: false,
_log_store_subscriptions: vec![
model_changes_subscription,
events_subscriptions,
focus_subscription,
],
};
if let Some(server_id) = server_id {
this.show_logs_for_server(server_id, cx);
}
this
}
fn editor_for_logs(
log_contents: String,
cx: &mut ViewContext<Self>,
) -> (View<Editor>, Subscription) {
let editor = cx.new_view(|cx| {
let mut editor = Editor::multi_line(cx);
editor.set_text(log_contents, cx);
editor.move_to_end(&MoveToEnd, cx);
editor.set_read_only(true);
editor
});
let editor_subscription = cx.subscribe(
&editor,
|_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| {
cx.emit(event.clone())
},
);
(editor, editor_subscription)
}
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
let log_store = self.log_store.read(cx);
let state = log_store.projects.get(&self.project.downgrade())?;
let mut rows = self
.project
.read(cx)
.language_servers()
.filter_map(|(server_id, language_server_name, worktree_id)| {
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
let state = state.servers.get(&server_id)?;
Some(LogMenuItem {
server_id,
server_name: language_server_name,
worktree_root_name: worktree.read(cx).root_name().to_string(),
rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
})
})
.chain(
self.project
.read(cx)
.supplementary_language_servers()
.filter_map(|(&server_id, (name, _))| {
let state = state.servers.get(&server_id)?;
Some(LogMenuItem {
server_id,
server_name: name.clone(),
worktree_root_name: "supplementary".to_string(),
rpc_trace_enabled: state.rpc_state.is_some(),
rpc_trace_selected: self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
logs_selected: !self.is_showing_rpc_trace
&& self.current_server_id == Some(server_id),
})
}),
)
.collect::<Vec<_>>();
rows.sort_by_key(|row| row.server_id);
rows.dedup_by_key(|row| row.server_id);
Some(rows)
}
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
let log_contents = self
.log_store
.read(cx)
.server_logs(&self.project, server_id)
.map(log_contents);
if let Some(log_contents) = log_contents {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = false;
let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
self.editor = editor;
self.editor_subscription = editor_subscription;
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn show_rpc_trace_for_server(
&mut self,
server_id: LanguageServerId,
cx: &mut ViewContext<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.enable_rpc_trace_for_language_server(&self.project, server_id)
.map(|state| log_contents(&state.rpc_messages))
});
if let Some(rpc_log) = rpc_log {
self.current_server_id = Some(server_id);
self.is_showing_rpc_trace = true;
let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
.read(cx)
.buffer()
.read(cx)
.as_singleton()
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
let buffer = cx.handle();
|_, mut cx| async move {
let language = language.await.ok();
buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(language, cx);
})
}
})
.detach_and_log_err(cx);
});
self.editor = editor;
self.editor_subscription = editor_subscription;
cx.notify();
}
cx.focus(&self.focus_handle);
}
fn toggle_rpc_trace_for_server(
&mut self,
server_id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
self.log_store.update(cx, |log_store, cx| {
if enabled {
log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
} else {
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
}
});
if !enabled && Some(server_id) == self.current_server_id {
self.show_logs_for_server(server_id, cx);
cx.notify();
}
}
}
fn log_contents(lines: &VecDeque<String>) -> String {
let (a, b) = lines.as_slices();
let log_contents = a.join("\n");
if b.is_empty() {
log_contents
} else {
log_contents + "\n" + &b.join("\n")
}
}
impl Render for LspLogView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor
.update(cx, |editor, cx| editor.render(cx).into_any_element())
}
}
impl FocusableView for LspLogView {
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Item for LspLogView {
type Event = EditorEvent;
fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
Editor::to_item_events(event, f)
}
fn tab_content(&self, _: Option<usize>, _: bool, _: &WindowContext<'_>) -> AnyElement {
Label::new("LSP Logs").into_any_element()
}
fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
}
impl SearchableItem for LspLogView {
type Match = <Editor as SearchableItem>::Match;
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.editor.update(cx, |e, cx| e.clear_matches(cx))
}
fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |e, cx| e.update_matches(matches, cx))
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
self.editor.update(cx, |e, cx| e.query_suggestion(cx))
}
fn activate_match(
&mut self,
index: usize,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) {
self.editor
.update(cx, |e, cx| e.activate_match(index, matches, cx))
}
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.editor
.update(cx, |e, cx| e.select_matches(matches, cx))
}
fn find_matches(
&mut self,
query: Arc<project::search::SearchQuery>,
cx: &mut ViewContext<Self>,
) -> gpui::Task<Vec<Self::Match>> {
self.editor.update(cx, |e, cx| e.find_matches(query, cx))
}
fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
// Since LSP Log is read-only, it doesn't make sense to support replace operation.
}
fn supported_options() -> workspace::searchable::SearchOptions {
workspace::searchable::SearchOptions {
case: true,
word: true,
regex: true,
// LSP log is read-only.
replacement: false,
}
}
fn active_match_index(
&mut self,
matches: Vec<Self::Match>,
cx: &mut ViewContext<Self>,
) -> Option<usize> {
self.editor
.update(cx, |e, cx| e.active_match_index(matches, cx))
}
}
impl EventEmitter<ToolbarItemEvent> for LspLogToolbarItemView {}
impl ToolbarItemView for LspLogToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> workspace::ToolbarItemLocation {
if let Some(item) = active_pane_item {
if let Some(log_view) = item.downcast::<LspLogView>() {
self.log_view = Some(log_view.clone());
self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| {
cx.notify();
}));
return ToolbarItemLocation::PrimaryLeft;
}
}
self.log_view = None;
self._log_view_subscription = None;
ToolbarItemLocation::Hidden
}
}
impl Render for LspLogToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(log_view) = self.log_view.clone() else {
return div();
};
let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| {
let menu_rows = log_view.menu_items(cx).unwrap_or_default();
let current_server_id = log_view.current_server_id;
(menu_rows, current_server_id)
});
let current_server = current_server_id.and_then(|current_server_id| {
if let Ok(ix) = menu_rows.binary_search_by_key(&current_server_id, |e| e.server_id) {
Some(menu_rows[ix].clone())
} else {
None
}
});
let log_toolbar_view = cx.view().clone();
let lsp_menu = popover_menu("LspLogView")
.anchor(AnchorCorner::TopLeft)
.trigger(Button::new(
"language_server_menu_header",
current_server
.and_then(|row| {
Some(Cow::Owned(format!(
"{} ({}) - {}",
row.server_name.0,
row.worktree_root_name,
if row.rpc_trace_selected {
RPC_MESSAGES
} else {
SERVER_LOGS
},
)))
})
.unwrap_or_else(|| "No server selected".into()),
))
.menu(move |cx| {
let menu_rows = menu_rows.clone();
let log_view = log_view.clone();
let log_toolbar_view = log_toolbar_view.clone();
ContextMenu::build(cx, move |mut menu, cx| {
for (ix, row) in menu_rows.into_iter().enumerate() {
let server_selected = Some(row.server_id) == current_server_id;
menu = menu
.header(format!(
"{} ({})",
row.server_name.0, row.worktree_root_name
))
.entry(
SERVER_LOGS,
None,
cx.handler_for(&log_view, move |view, cx| {
view.show_logs_for_server(row.server_id, cx);
}),
);
if server_selected && row.logs_selected {
debug_assert_eq!(
Some(ix * 3 + 1),
menu.select_last(),
"Could not scroll to a just added LSP menu item"
);
}
menu = menu.custom_entry(
{
let log_toolbar_view = log_toolbar_view.clone();
move |cx| {
h_stack()
.w_full()
.justify_between()
.child(Label::new(RPC_MESSAGES))
.child(
div().z_index(120).child(
Checkbox::new(
ix,
if row.rpc_trace_enabled {
Selection::Selected
} else {
Selection::Unselected
},
)
.on_click(cx.listener_for(
&log_toolbar_view,
move |view, selection, cx| {
let enabled = matches!(
selection,
Selection::Selected
);
view.toggle_logging_for_server(
row.server_id,
enabled,
cx,
);
cx.stop_propagation();
},
)),
),
)
.into_any_element()
}
},
cx.handler_for(&log_view, move |view, cx| {
view.show_rpc_trace_for_server(row.server_id, cx);
}),
);
if server_selected && row.rpc_trace_selected {
debug_assert_eq!(
Some(ix * 3 + 2),
menu.select_last(),
"Could not scroll to a just added LSP menu item"
);
}
}
menu
})
.into()
});
h_stack().size_full().child(lsp_menu).child(
div()
.child(
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|this, _, cx| {
if let Some(log_view) = this.log_view.as_ref() {
log_view.update(cx, |log_view, cx| {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
editor.clear(cx);
editor.set_read_only(true);
});
})
}
},
)),
)
.ml_2(),
)
}
}
const RPC_MESSAGES: &str = "RPC Messages";
const SERVER_LOGS: &str = "Server Logs";
impl LspLogToolbarItemView {
pub fn new() -> Self {
Self {
log_view: None,
_log_view_subscription: None,
}
}
fn toggle_logging_for_server(
&mut self,
id: LanguageServerId,
enabled: bool,
cx: &mut ViewContext<Self>,
) {
if let Some(log_view) = &self.log_view {
log_view.update(cx, |log_view, cx| {
log_view.toggle_rpc_trace_for_server(id, enabled, cx);
if !enabled && Some(id) == log_view.current_server_id {
log_view.show_logs_for_server(id, cx);
cx.notify();
}
cx.focus(&log_view.focus_handle);
});
}
cx.notify();
}
}
pub enum Event {
NewServerLogEntry {
id: LanguageServerId,
entry: String,
is_rpc: bool,
},
}
impl EventEmitter<Event> for LogStore {}
impl EventEmitter<Event> for LspLogView {}
impl EventEmitter<EditorEvent> for LspLogView {}
impl EventEmitter<SearchEvent> for LspLogView {}

View File

@ -1,107 +0,0 @@
use std::sync::Arc;
use crate::lsp_log::LogMenuItem;
use super::*;
use futures::StreamExt;
use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext};
use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName};
use project::{FakeFs, Project};
use settings::SettingsStore;
#[gpui::test]
async fn test_lsp_logs(cx: &mut TestAppContext) {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
init_test(cx);
let mut rust_language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_rust_servers = rust_language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
name: "the-rust-language-server",
..Default::default()
}))
.await;
let fs = FakeFs::new(cx.background_executor.clone());
fs.insert_tree(
"/the-root",
json!({
"test.rs": "",
"package.json": "",
}),
)
.await;
let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages().add(Arc::new(rust_language));
});
let log_store = cx.new_model(|cx| LogStore::new(cx));
log_store.update(cx, |store, cx| store.add_project(&project, cx));
let _rust_buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/the-root/test.rs", cx)
})
.await
.unwrap();
let mut language_server = fake_rust_servers.next().await.unwrap();
language_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await;
let window = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx));
let log_view = window.root(cx).unwrap();
let mut cx = VisualTestContext::from_window(*window, cx);
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
message: "hello from the server".into(),
typ: lsp::MessageType::INFO,
});
cx.executor().run_until_parked();
log_view.update(&mut cx, |view, cx| {
assert_eq!(
view.menu_items(cx).unwrap(),
&[LogMenuItem {
server_id: language_server.server.server_id(),
server_name: LanguageServerName("the-rust-language-server".into()),
worktree_root_name: project
.read(cx)
.worktrees()
.next()
.unwrap()
.read(cx)
.root_name()
.to_string(),
rpc_trace_enabled: false,
rpc_trace_selected: false,
logs_selected: true,
}]
);
assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");
});
}
fn init_test(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme::init(theme::LoadThemes::JustBase, cx);
language::init(cx);
client::init_settings(cx);
Project::init_settings(cx);
editor::init_settings(cx);
});
}

View File

@ -1,533 +0,0 @@
use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
use gpui::{
actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div,
EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model,
MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled,
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use language::{Buffer, OwnedSyntaxLayerInfo};
use settings::Settings;
use std::{mem, ops::Range};
use theme::{ActiveTheme, ThemeSettings};
use tree_sitter::{Node, TreeCursor};
use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
use workspace::{
item::{Item, ItemHandle},
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
};
actions!(debug, [OpenSyntaxTreeView]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| {
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
let syntax_tree_view =
cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx)
});
})
.detach();
}
pub struct SyntaxTreeView {
workspace_handle: WeakView<Workspace>,
editor: Option<EditorState>,
mouse_y: Option<Pixels>,
line_height: Option<Pixels>,
list_scroll_handle: UniformListScrollHandle,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
}
pub struct SyntaxTreeToolbarItemView {
tree_view: Option<View<SyntaxTreeView>>,
subscription: Option<gpui::Subscription>,
}
struct EditorState {
editor: View<Editor>,
active_buffer: Option<BufferState>,
_subscription: gpui::Subscription,
}
#[derive(Clone)]
struct BufferState {
buffer: Model<Buffer>,
excerpt_id: ExcerptId,
active_layer: Option<OwnedSyntaxLayerInfo>,
}
impl SyntaxTreeView {
pub fn new(
workspace_handle: WeakView<Workspace>,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> Self {
let mut this = Self {
workspace_handle: workspace_handle.clone(),
list_scroll_handle: UniformListScrollHandle::new(),
editor: None,
mouse_y: None,
line_height: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
};
this.workspace_updated(active_item, cx);
cx.observe(
&workspace_handle.upgrade().unwrap(),
|this, workspace, cx| {
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
},
)
.detach();
this
}
fn workspace_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) {
if let Some(item) = active_item {
if item.item_id() != cx.entity_id() {
if let Some(editor) = item.act_as::<Editor>(cx) {
self.set_editor(editor, cx);
}
}
}
}
fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
return;
}
editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx)
});
}
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let did_reparse = match event {
editor::EditorEvent::Reparsed => true,
editor::EditorEvent::SelectionsChanged { .. } => false,
_ => return,
};
this.editor_updated(did_reparse, cx);
});
self.editor = Some(EditorState {
editor,
_subscription: subscription,
active_buffer: None,
});
self.editor_updated(true, cx);
}
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
let editor_state = self.editor.as_mut()?;
let editor = &editor_state.editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
let multibuffer = editor.buffer().read(cx);
let (buffer, range, excerpt_id) = multibuffer
.range_to_buffer_ranges(selection_range, cx)
.pop()?;
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
// from that buffer.
let buffer_state = editor_state
.active_buffer
.get_or_insert_with(|| BufferState {
buffer: buffer.clone(),
excerpt_id,
active_layer: None,
});
let mut prev_layer = None;
if did_reparse {
prev_layer = buffer_state.active_layer.take();
}
if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
buffer_state.buffer = buffer.clone();
buffer_state.excerpt_id = excerpt_id;
buffer_state.active_layer = None;
}
let layer = match &mut buffer_state.active_layer {
Some(layer) => layer,
None => {
let snapshot = buffer.read(cx).snapshot();
let layer = if let Some(prev_layer) = prev_layer {
let prev_range = prev_layer.node().byte_range();
snapshot
.syntax_layers()
.filter(|layer| layer.language == &prev_layer.language)
.min_by_key(|layer| {
let range = layer.node().byte_range();
((range.start as i64) - (prev_range.start as i64)).abs()
+ ((range.end as i64) - (prev_range.end as i64)).abs()
})?
} else {
snapshot.syntax_layers().next()?
};
buffer_state.active_layer.insert(layer.to_owned())
}
};
// Within the active layer, find the syntax node under the cursor,
// and scroll to it.
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(range.start).is_some() {
if !range.is_empty() && cursor.node().end_byte() == range.start {
cursor.goto_next_sibling();
}
}
// Ascend to the smallest ancestor that contains the range.
loop {
let node_range = cursor.node().byte_range();
if node_range.start <= range.start && node_range.end >= range.end {
break;
}
if !cursor.goto_parent() {
break;
}
}
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_scroll_handle.scroll_to_item(descendant_ix);
cx.notify();
Some(())
}
fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
let line_height = self.line_height?;
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| {
// Put the cursor at the beginning of the node.
mem::swap(&mut range.start, &mut range.end);
editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
selections.select_ranges(vec![range]);
});
});
Some(())
}
fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize;
if self.hovered_descendant_ix != Some(ix) {
self.hovered_descendant_ix = Some(ix);
self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
vec![range],
|theme| theme.editor_document_highlight_write_background,
cx,
);
});
cx.notify();
}
}
}
fn update_editor_with_range_for_descendant_ix(
&self,
descendant_ix: usize,
cx: &mut ViewContext<Self>,
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
) -> Option<()> {
let editor_state = self.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let layer = buffer_state.active_layer.as_ref()?;
// Find the node.
let mut cursor = layer.node().walk();
cursor.goto_descendant(descendant_ix);
let node = cursor.node();
let range = node.byte_range();
// Build a text anchor range.
let buffer = buffer_state.buffer.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
// Build a multibuffer anchor range.
let multibuffer = editor_state.editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let excerpt_id = buffer_state.excerpt_id;
let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
// Update the editor with the anchor range.
editor_state.editor.update(cx, |editor, cx| {
f(editor, range, cx);
});
Some(())
}
fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div {
let colors = cx.theme().colors();
let mut row = h_stack();
if let Some(field_name) = cursor.field_name() {
row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
}
let node = cursor.node();
return row
.child(if node.is_named() {
Label::new(node.kind()).color(Color::Default)
} else {
Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
})
.child(
div()
.child(Label::new(format_node_range(node)).color(Color::Muted))
.pl_1(),
)
.text_bg(if selected {
colors.element_selected
} else {
Hsla::default()
})
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover));
}
}
impl Render for SyntaxTreeView {
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let line_height = cx
.text_style()
.line_height_in_pixels(settings.buffer_font_size(cx));
if Some(line_height) != self.line_height {
self.line_height = Some(line_height);
self.hover_state_changed(cx);
}
let mut rendered = div().flex_1();
if let Some(layer) = self
.editor
.as_ref()
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone();
let list = uniform_list(
cx.view().clone(),
"SyntaxTreeView",
layer.node().descendant_count(),
move |this, range, cx| {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start as usize;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
break;
}
} else {
items.push(Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
cx,
));
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
}
}
items
},
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())
.on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| {
tree_view.mouse_y = Some(event.position.y);
tree_view.hover_state_changed(cx);
}))
.on_mouse_down(
MouseButton::Left,
cx.listener(move |tree_view, event: &MouseDownEvent, cx| {
tree_view.handle_click(event.position.y, cx);
}),
)
.text_bg(cx.theme().colors().background);
rendered = rendered.child(
canvas(move |bounds, cx| {
list.into_any_element().draw(
bounds.origin,
bounds.size.map(AvailableSpace::Definite),
cx,
)
})
.size_full(),
);
}
rendered
}
}
impl EventEmitter<()> for SyntaxTreeView {}
impl FocusableView for SyntaxTreeView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for SyntaxTreeView {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content(&self, _: Option<usize>, _: bool, _: &WindowContext<'_>) -> AnyElement {
Label::new("Syntax Tree").into_any_element()
}
fn clone_on_split(
&self,
_: workspace::WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where
Self: Sized,
{
Some(cx.new_view(|cx| {
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
if let Some(editor) = &self.editor {
clone.set_editor(editor.editor.clone(), cx)
}
clone
}))
}
}
impl SyntaxTreeToolbarItemView {
pub fn new() -> Self {
Self {
tree_view: None,
subscription: None,
}
}
fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option<PopoverMenu<ContextMenu>> {
let tree_view = self.tree_view.as_ref()?;
let tree_view = tree_view.read(cx);
let editor_state = tree_view.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let active_layer = buffer_state.active_layer.clone()?;
let active_buffer = buffer_state.buffer.read(cx).snapshot();
let view = cx.view().clone();
Some(
popover_menu("Syntax Tree")
.trigger(Self::render_header(&active_layer))
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
menu = menu.entry(
format!(
"{} {}",
layer.language.name(),
format_node_range(layer.node())
),
None,
cx.handler_for(&view, move |view, cx| {
view.select_layer(layer_ix, cx);
}),
);
}
menu
})
.into()
}),
)
}
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
let tree_view = self.tree_view.as_ref()?;
tree_view.update(cx, |view, cx| {
let editor_state = view.editor.as_mut()?;
let buffer_state = editor_state.active_buffer.as_mut()?;
let snapshot = buffer_state.buffer.read(cx).snapshot();
let layer = snapshot.syntax_layers().nth(layer_ix)?;
buffer_state.active_layer = Some(layer.to_owned());
view.selected_descendant_ix = None;
cx.notify();
view.focus_handle.focus(cx);
Some(())
})
}
fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike {
ButtonLike::new("syntax tree header")
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
}
fn format_node_range(node: Node) -> String {
let start = node.start_position();
let end = node.end_position();
format!(
"[{}:{} - {}:{}]",
start.row + 1,
start.column + 1,
end.row + 1,
end.column + 1,
)
}
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement {
self.render_menu(cx)
.unwrap_or_else(|| popover_menu("Empty Syntax Tree"))
}
}
impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
impl ToolbarItemView for SyntaxTreeToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item {
if let Some(view) = item.downcast::<SyntaxTreeView>() {
self.tree_view = Some(view.clone());
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
return ToolbarItemLocation::PrimaryLeft;
}
}
self.tree_view = None;
self.subscription = None;
ToolbarItemLocation::Hidden
}
}

View File

@ -15,7 +15,7 @@ editor = { path = "../editor2", package = "editor2" }
gpui = { path = "../gpui2", package = "gpui2" }
menu = { path = "../menu2", package = "menu2" }
project = { path = "../project2", package = "project2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
settings = { path = "../settings2", package = "settings2" }
theme = { path = "../theme2", package = "theme2" }
ui = { path = "../ui2", package = "ui2" }

View File

@ -12,7 +12,7 @@ doctest = false
assistant = { package = "assistant2", path = "../assistant2" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
workspace = { package = "workspace2", path = "../workspace2" }
ui = { package = "ui2", path = "../ui2" }

View File

@ -11,16 +11,17 @@ doctest = false
[dependencies]
bitflags = "1"
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
menu = { path = "../menu" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
semantic_index = { path = "../semantic_index" }
ui = {package = "ui2", path = "../ui2"}
workspace = { package = "workspace2", path = "../workspace2" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
anyhow.workspace = true
futures.workspace = true
log.workspace = true
@ -31,9 +32,9 @@ smallvec.workspace = true
smol.workspace = true
serde_json.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
unindent.workspace = true

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
use gpui::Action;
use gpui::{Action, SharedString};
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode {
@ -10,12 +11,6 @@ pub enum SearchMode {
Regex,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Side {
Left,
Right,
}
impl SearchMode {
pub(crate) fn label(&self) -> &'static str {
match self {
@ -24,28 +19,14 @@ impl SearchMode {
SearchMode::Regex => "Regex",
}
}
pub(crate) fn region_id(&self) -> usize {
match self {
SearchMode::Text => 3,
SearchMode::Semantic => 4,
SearchMode::Regex => 5,
}
pub(crate) fn tooltip(&self) -> SharedString {
format!("Activate {} Mode", self.label()).into()
}
pub(crate) fn tooltip_text(&self) -> &'static str {
pub(crate) fn action(&self) -> Box<dyn Action> {
match self {
SearchMode::Text => "Activate Text Search",
SearchMode::Semantic => "Activate Semantic Search",
SearchMode::Regex => "Activate Regex Search",
}
}
pub(crate) fn activate_action(&self) -> Box<dyn Action> {
match self {
SearchMode::Text => Box::new(ActivateTextMode),
SearchMode::Semantic => Box::new(ActivateSemanticMode),
SearchMode::Regex => Box::new(ActivateRegexMode),
SearchMode::Text => ActivateTextMode.boxed_clone(),
SearchMode::Semantic => ActivateSemanticMode.boxed_clone(),
SearchMode::Regex => ActivateRegexMode.boxed_clone(),
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,11 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{
actions,
elements::{Component, SafeStylable, TooltipStyle},
Action, AnyElement, AppContext, Element, View,
};
use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
use theme::components::{
action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
};
pub use project_search::ProjectSearchView;
use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, IconButton};
pub mod buffer_search;
mod history;
@ -19,6 +14,7 @@ pub mod project_search;
pub(crate) mod search_bar;
pub fn init(cx: &mut AppContext) {
menu::init();
buffer_search::init(cx);
project_search::init(cx);
}
@ -57,28 +53,28 @@ bitflags! {
impl SearchOptions {
pub fn label(&self) -> &'static str {
match *self {
Self::WHOLE_WORD => "Match Whole Word",
Self::CASE_SENSITIVE => "Match Case",
Self::INCLUDE_IGNORED => "Include Ignored",
_ => panic!("{self:?} is not a named SearchOption"),
SearchOptions::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case",
SearchOptions::INCLUDE_IGNORED => "Include ignored",
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn icon(&self) -> &'static str {
pub fn icon(&self) -> ui::Icon {
match *self {
Self::WHOLE_WORD => "icons/word_search.svg",
Self::CASE_SENSITIVE => "icons/case_insensitive.svg",
Self::INCLUDE_IGNORED => "icons/case_insensitive.svg",
_ => panic!("{self:?} is not a named SearchOption"),
SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit,
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn to_toggle_action(&self) -> Box<dyn Action> {
pub fn to_toggle_action(&self) -> Box<dyn Action + Sync + Send + 'static> {
match *self {
Self::WHOLE_WORD => Box::new(ToggleWholeWord),
Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
_ => panic!("{self:?} is not a named SearchOption"),
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
_ => panic!("{:?} is not a named SearchOption", self),
}
}
@ -94,47 +90,19 @@ impl SearchOptions {
options
}
pub fn as_button<V: View>(
pub fn as_button(
&self,
active: bool,
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
Button::dynamic_action(self.to_toggle_action())
.with_tooltip(format!("Toggle {}", self.label()), tooltip_style)
.with_contents(Svg::new(self.icon()))
.toggleable(active)
.with_style(button_style)
.element()
.into_any()
action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
) -> impl IntoElement {
IconButton::new(self.label(), self.icon())
.on_click(action)
.style(ButtonStyle::Subtle)
.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<V: View>(
active: bool,
tooltip_style: TooltipStyle,
button_style: ToggleIconButtonStyle,
) -> AnyElement<V> {
Button::dynamic_action(Box::new(ToggleReplace))
.with_tooltip("Toggle Replace", tooltip_style)
.with_contents(theme::components::svg::Svg::new("icons/replace.svg"))
.toggleable(active)
.with_style(button_style)
.element()
.into_any()
}
fn replace_action<V: View>(
action: impl Action,
name: &'static str,
icon_path: &'static str,
tooltip_style: TooltipStyle,
button_style: IconButtonStyle,
) -> AnyElement<V> {
Button::dynamic_action(Box::new(action))
.with_tooltip(name, tooltip_style)
.with_contents(theme::components::svg::Svg::new(icon_path))
.with_style(button_style)
.element()
.into_any()
}

View File

@ -1,174 +1,18 @@
use std::borrow::Cow;
use gpui::{Action, IntoElement};
use ui::IconButton;
use ui::{prelude::*, Tooltip};
use gpui::{
elements::{Label, MouseEventHandler, Svg},
platform::{CursorStyle, MouseButton},
scene::{CornerRadii, MouseClick},
Action, AnyElement, Element, EventContext, View, ViewContext,
};
use workspace::searchable::Direction;
use crate::{
mode::{SearchMode, Side},
SelectNextMatch, SelectPrevMatch,
};
pub(super) fn render_nav_button<V: View>(
icon: &'static str,
direction: Direction,
pub(super) fn render_nav_button(
icon: ui::Icon,
active: bool,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let action: Box<dyn Action>;
let tooltip;
match direction {
Direction::Prev => {
action = Box::new(SelectPrevMatch);
tooltip = "Select Previous Match";
}
Direction::Next => {
action = Box::new(SelectNextMatch);
tooltip = "Select Next Match";
}
};
let tooltip_style = theme::current(cx).tooltip.clone();
let cursor_style = if active {
CursorStyle::PointingHand
} else {
CursorStyle::default()
};
enum NavButton {}
MouseEventHandler::new::<NavButton, _>(direction as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
.nav_button
.in_state(active)
.style_for(state)
.clone();
let mut container_style = style.container.clone();
let label = Label::new(icon, style.label.clone()).aligned().contained();
container_style.corner_radii = match direction {
Direction::Prev => CornerRadii {
bottom_right: 0.,
top_right: 0.,
..container_style.corner_radii
},
Direction::Next => CornerRadii {
bottom_left: 0.,
top_left: 0.,
..container_style.corner_radii
},
};
if direction == Direction::Prev {
// Remove right border so that when both Next and Prev buttons are
// next to one another, there's no double border between them.
container_style.border.right = false;
}
label.with_style(container_style)
})
.on_click(MouseButton::Left, on_click)
.with_cursor_style(cursor_style)
.with_tooltip::<NavButton>(
direction as usize,
tooltip.to_string(),
Some(action),
tooltip_style,
cx,
tooltip: &'static str,
action: &'static dyn Action,
) -> impl IntoElement {
IconButton::new(
SharedString::from(format!("search-nav-button-{}", action.name())),
icon,
)
.into_any()
}
pub(crate) fn render_search_mode_button<V: View>(
mode: SearchMode,
side: Option<Side>,
is_active: bool,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
enum SearchModeButton {}
MouseEventHandler::new::<SearchModeButton, _>(mode.region_id(), cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
.mode_button
.in_state(is_active)
.style_for(state)
.clone();
let mut container_style = style.container;
if let Some(button_side) = side {
if button_side == Side::Left {
container_style.border.left = true;
container_style.corner_radii = CornerRadii {
bottom_right: 0.,
top_right: 0.,
..container_style.corner_radii
};
} else {
container_style.border.left = false;
container_style.corner_radii = CornerRadii {
bottom_left: 0.,
top_left: 0.,
..container_style.corner_radii
};
}
} else {
container_style.border.left = false;
container_style.corner_radii = CornerRadii::default();
}
Label::new(mode.label(), style.text)
.aligned()
.contained()
.with_style(container_style)
.constrained()
.with_height(theme.search.search_bar_row_height)
})
.on_click(MouseButton::Left, on_click)
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<SearchModeButton>(
mode.region_id(),
mode.tooltip_text().to_owned(),
Some(mode.activate_action()),
tooltip_style,
cx,
)
.into_any()
}
pub(crate) fn render_option_button_icon<V: View>(
is_active: bool,
icon: &'static str,
id: usize,
label: impl Into<Cow<'static, str>>,
action: Box<dyn Action>,
on_click: impl Fn(MouseClick, &mut V, &mut EventContext<V>) + 'static,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let tooltip_style = theme::current(cx).tooltip.clone();
MouseEventHandler::new::<V, _>(id, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
.option_button
.in_state(is_active)
.style_for(state);
Svg::new(icon)
.with_color(style.color.clone())
.constrained()
.with_width(style.icon_width)
.contained()
.with_style(style.container)
.constrained()
.with_height(theme.search.option_button_height)
.with_width(style.button_width)
})
.on_click(MouseButton::Left, on_click)
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<V>(id, label, Some(action), tooltip_style, cx)
.into_any()
.on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
.tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
.disabled(!active)
}

View File

@ -1,40 +0,0 @@
[package]
name = "search2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/search.rs"
doctest = false
[dependencies]
bitflags = "1"
collections = { path = "../collections" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
project = { package = "project2", path = "../project2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
ui = {package = "ui2", path = "../ui2"}
workspace = { package = "workspace2", path = "../workspace2" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
anyhow.workspace = true
futures.workspace = true
log.workspace = true
postage.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
serde_json.workspace = true
[dev-dependencies]
client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
unindent.workspace = true

File diff suppressed because it is too large Load Diff

View File

@ -1,184 +0,0 @@
use smallvec::SmallVec;
const SEARCH_HISTORY_LIMIT: usize = 20;
#[derive(Default, Debug, Clone)]
pub struct SearchHistory {
history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
selected: Option<usize>,
}
impl SearchHistory {
pub fn add(&mut self, search_string: String) {
if let Some(i) = self.selected {
if search_string == self.history[i] {
return;
}
}
if let Some(previously_searched) = self.history.last_mut() {
if search_string.find(previously_searched.as_str()).is_some() {
*previously_searched = search_string;
self.selected = Some(self.history.len() - 1);
return;
}
}
self.history.push(search_string);
if self.history.len() > SEARCH_HISTORY_LIMIT {
self.history.remove(0);
}
self.selected = Some(self.history.len() - 1);
}
pub fn next(&mut self) -> Option<&str> {
let history_size = self.history.len();
if history_size == 0 {
return None;
}
let selected = self.selected?;
if selected == history_size - 1 {
return None;
}
let next_index = selected + 1;
self.selected = Some(next_index);
Some(&self.history[next_index])
}
pub fn current(&self) -> Option<&str> {
Some(&self.history[self.selected?])
}
pub fn previous(&mut self) -> Option<&str> {
let history_size = self.history.len();
if history_size == 0 {
return None;
}
let prev_index = match self.selected {
Some(selected_index) => {
if selected_index == 0 {
return None;
} else {
selected_index - 1
}
}
None => history_size - 1,
};
self.selected = Some(prev_index);
Some(&self.history[prev_index])
}
pub fn reset_selection(&mut self) {
self.selected = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
let mut search_history = SearchHistory::default();
assert_eq!(
search_history.current(),
None,
"No current selection should be set fo the default search history"
);
search_history.add("rust".to_string());
assert_eq!(
search_history.current(),
Some("rust"),
"Newly added item should be selected"
);
// check if duplicates are not added
search_history.add("rust".to_string());
assert_eq!(
search_history.history.len(),
1,
"Should not add a duplicate"
);
assert_eq!(search_history.current(), Some("rust"));
// check if new string containing the previous string replaces it
search_history.add("rustlang".to_string());
assert_eq!(
search_history.history.len(),
1,
"Should replace previous item if it's a substring"
);
assert_eq!(search_history.current(), Some("rustlang"));
// push enough items to test SEARCH_HISTORY_LIMIT
for i in 0..SEARCH_HISTORY_LIMIT * 2 {
search_history.add(format!("item{i}"));
}
assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
}
#[test]
fn test_next_and_previous() {
let mut search_history = SearchHistory::default();
assert_eq!(
search_history.next(),
None,
"Default search history should not have a next item"
);
search_history.add("Rust".to_string());
assert_eq!(search_history.next(), None);
search_history.add("JavaScript".to_string());
assert_eq!(search_history.next(), None);
search_history.add("TypeScript".to_string());
assert_eq!(search_history.next(), None);
assert_eq!(search_history.current(), Some("TypeScript"));
assert_eq!(search_history.previous(), Some("JavaScript"));
assert_eq!(search_history.current(), Some("JavaScript"));
assert_eq!(search_history.previous(), Some("Rust"));
assert_eq!(search_history.current(), Some("Rust"));
assert_eq!(search_history.previous(), None);
assert_eq!(search_history.current(), Some("Rust"));
assert_eq!(search_history.next(), Some("JavaScript"));
assert_eq!(search_history.current(), Some("JavaScript"));
assert_eq!(search_history.next(), Some("TypeScript"));
assert_eq!(search_history.current(), Some("TypeScript"));
assert_eq!(search_history.next(), None);
assert_eq!(search_history.current(), Some("TypeScript"));
}
#[test]
fn test_reset_selection() {
let mut search_history = SearchHistory::default();
search_history.add("Rust".to_string());
search_history.add("JavaScript".to_string());
search_history.add("TypeScript".to_string());
assert_eq!(search_history.current(), Some("TypeScript"));
search_history.reset_selection();
assert_eq!(search_history.current(), None);
assert_eq!(
search_history.previous(),
Some("TypeScript"),
"Should start from the end after reset on previous item query"
);
search_history.previous();
assert_eq!(search_history.current(), Some("JavaScript"));
search_history.previous();
assert_eq!(search_history.current(), Some("Rust"));
search_history.reset_selection();
assert_eq!(search_history.current(), None);
}
}

View File

@ -1,46 +0,0 @@
use gpui::{Action, SharedString};
use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode {
#[default]
Text,
Semantic,
Regex,
}
impl SearchMode {
pub(crate) fn label(&self) -> &'static str {
match self {
SearchMode::Text => "Text",
SearchMode::Semantic => "Semantic",
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 {
match mode {
SearchMode::Text => SearchMode::Regex,
SearchMode::Regex => {
if semantic_enabled {
SearchMode::Semantic
} else {
SearchMode::Text
}
}
SearchMode::Semantic => SearchMode::Text,
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,108 +0,0 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext, IntoElement};
pub use mode::SearchMode;
use project::search::SearchQuery;
pub use project_search::ProjectSearchView;
use ui::{prelude::*, Tooltip};
use ui::{ButtonStyle, IconButton};
pub mod buffer_search;
mod history;
mod mode;
pub mod project_search;
pub(crate) mod search_bar;
pub fn init(cx: &mut AppContext) {
menu::init();
buffer_search::init(cx);
project_search::init(cx);
}
actions!(
search,
[
CycleMode,
ToggleWholeWord,
ToggleCaseSensitive,
ToggleIncludeIgnored,
ToggleReplace,
SelectNextMatch,
SelectPrevMatch,
SelectAllMatches,
NextHistoryQuery,
PreviousHistoryQuery,
ActivateTextMode,
ActivateSemanticMode,
ActivateRegexMode,
ReplaceAll,
ReplaceNext,
]
);
bitflags! {
#[derive(Default)]
pub struct SearchOptions: u8 {
const NONE = 0b000;
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
const INCLUDE_IGNORED = 0b100;
}
}
impl SearchOptions {
pub fn label(&self) -> &'static str {
match *self {
SearchOptions::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case",
SearchOptions::INCLUDE_IGNORED => "Include ignored",
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn icon(&self) -> ui::Icon {
match *self {
SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit,
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn to_toggle_action(&self) -> Box<dyn Action + Sync + Send + 'static> {
match *self {
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
SearchOptions::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn none() -> SearchOptions {
SearchOptions::NONE
}
pub fn from_query(query: &SearchQuery) -> SearchOptions {
let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
options
}
pub fn as_button(
&self,
active: bool,
action: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static,
) -> impl IntoElement {
IconButton::new(self.label(), self.icon())
.on_click(action)
.style(ButtonStyle::Subtle)
.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)
})
}
}

View File

@ -1,18 +0,0 @@
use gpui::{Action, IntoElement};
use ui::IconButton;
use ui::{prelude::*, Tooltip};
pub(super) fn render_nav_button(
icon: ui::Icon,
active: bool,
tooltip: &'static str,
action: &'static dyn Action,
) -> impl IntoElement {
IconButton::new(
SharedString::from(format!("search-nav-button-{}", action.name())),
icon,
)
.on_click(|_, cx| cx.dispatch_action(action.boxed_clone()))
.tooltip(move |cx| Tooltip::for_action(tooltip, action, cx))
.disabled(!active)
}

View File

@ -9,19 +9,19 @@ path = "src/terminal_view.rs"
doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
editor = { path = "../editor" }
language = { path = "../language" }
gpui = { path = "../gpui" }
project = { path = "../project" }
search = { path = "../search" }
settings = { path = "../settings" }
theme = { path = "../theme" }
editor = { package = "editor2", path = "../editor2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
project = { package = "project2", path = "../project2" }
# search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { path = "../workspace" }
db = { path = "../db" }
workspace = { package = "workspace2", path = "../workspace2" }
db = { package = "db2", path = "../db2" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
terminal = { path = "../terminal" }
terminal = { package = "terminal2", path = "../terminal2" }
ui = { package = "ui2", path = "../ui2" }
smallvec.workspace = true
smol.workspace = true
mio-extras = "2.0.6"
@ -38,9 +38,9 @@ serde.workspace = true
serde_derive.workspace = true
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] }
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { package = "project2", path = "../project2", features = ["test-support"]}
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
rand.workspace = true

File diff suppressed because it is too large Load Diff

View File

@ -3,110 +3,107 @@ use std::{path::PathBuf, sync::Arc};
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, anyhow::Result, elements::*, serde_json, Action, AppContext, AsyncAppContext, Entity,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use project::Fs;
use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel},
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane, DraggedItem, Pane, Workspace,
pane,
ui::Icon,
Pane, Workspace,
};
use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(terminal_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalPanel::new_terminal);
cx.add_action(TerminalPanel::open_terminal);
}
#[derive(Debug)]
pub enum Event {
Close,
DockPositionChanged,
ZoomIn,
ZoomOut,
Focus,
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal);
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<TerminalPanel>(cx);
});
},
)
.detach();
}
pub struct TerminalPanel {
pane: ViewHandle<Pane>,
pane: View<Pane>,
fs: Arc<dyn Fs>,
workspace: WeakViewHandle<Workspace>,
width: Option<f32>,
height: Option<f32>,
workspace: WeakView<Workspace>,
width: Option<Pixels>,
height: Option<Pixels>,
pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>,
}
impl TerminalPanel {
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let weak_self = cx.weak_handle();
let pane = cx.add_view(|cx| {
let window = cx.window();
let terminal_panel = cx.view().clone();
let pane = cx.new_view(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
workspace.project().clone(),
workspace.app_state().background_actions,
Default::default(),
Some(Arc::new(|a, cx| {
if let Some(tab) = a.downcast_ref::<workspace::pane::DraggedTab>() {
if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) {
return item.downcast::<TerminalView>().is_some();
}
}
if a.downcast_ref::<ExternalPaths>().is_some() {
return true;
}
false
})),
cx,
);
pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
pane.on_can_drop(move |drag_and_drop, cx| {
drag_and_drop
.currently_dragged::<DraggedItem>(window)
.map_or(false, |(_, item)| {
item.handle.act_as::<TerminalView>(cx).is_some()
})
});
pane.display_nav_history_buttons(false);
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
let this = weak_self.clone();
Flex::row()
.with_child(Pane::render_tab_bar_button(
0,
"icons/plus.svg",
false,
Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))),
cx,
move |_, cx| {
let this = this.clone();
cx.window_context().defer(move |cx| {
if let Some(this) = this.upgrade(cx) {
this.update(cx, |this, cx| {
this.add_terminal(None, cx);
});
}
h_stack()
.gap_2()
.child(
IconButton::new("plus", Icon::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| {
terminal_panel.add_terminal(None, cx);
}))
.tooltip(|cx| Tooltip::text("New Terminal", cx)),
)
.child({
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", Icon::Maximize)
.icon_size(IconSize::Small)
.selected(zoomed)
.selected_icon(Icon::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);
}))
.tooltip(move |cx| {
Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
})
},
|_, _| {},
None,
))
.with_child(Pane::render_tab_bar_button(
1,
if pane.is_zoomed() {
"icons/minimize.svg"
} else {
"icons/maximize.svg"
},
pane.is_zoomed(),
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
cx,
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|_, _| {},
None,
))
.into_any()
})
.into_any_element()
});
let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
pane.toolbar()
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
// let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
// pane.toolbar()
// .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane
});
let subscriptions = vec![
@ -123,105 +120,103 @@ impl TerminalPanel {
_subscriptions: subscriptions,
};
let mut old_dock_position = this.position(cx);
cx.observe_global::<SettingsStore, _>(move |this, cx| {
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(Event::DockPositionChanged);
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this
}
pub fn load(
workspace: WeakViewHandle<Workspace>,
cx: AsyncAppContext,
) -> Task<Result<ViewHandle<Self>>> {
cx.spawn(|mut cx| async move {
let serialized_panel = if let Some(panel) = cx
.background()
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.await
.log_err()
.flatten()
{
Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
} else {
None
};
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.pane.update(cx, |_, cx| {
serialized_panel
.items
.iter()
.map(|item_id| {
TerminalView::deserialize(
workspace.project().clone(),
workspace.weak_handle(),
workspace.database_id(),
*item_id,
cx,
)
})
.collect::<Vec<_>>()
})
})
} else {
Default::default()
};
let pane = panel.read(cx).pane.clone();
(panel, pane, items)
})?;
pub async fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
) -> Result<View<Self>> {
let serialized_panel = cx
.background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.await
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
.transpose()
.log_err()
.flatten();
let pane = pane.downgrade();
let items = futures::future::join_all(items).await;
pane.update(&mut cx, |pane, cx| {
let active_item_id = serialized_panel
.as_ref()
.and_then(|panel| panel.active_item_id);
let mut active_ix = None;
for item in items {
if let Some(item) = item.log_err() {
let item_id = item.id();
pane.add_item(Box::new(item), false, false, None, cx);
if Some(item_id) == active_item_id {
active_ix = Some(pane.items_len() - 1);
}
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.pane.update(cx, |_, cx| {
serialized_panel
.items
.iter()
.map(|item_id| {
TerminalView::deserialize(
workspace.project().clone(),
workspace.weak_handle(),
workspace.database_id(),
*item_id,
cx,
)
})
.collect::<Vec<_>>()
})
})
} else {
Default::default()
};
let pane = panel.read(cx).pane.clone();
(panel, pane, items)
})?;
let pane = pane.downgrade();
let items = futures::future::join_all(items).await;
pane.update(&mut cx, |pane, cx| {
let active_item_id = serialized_panel
.as_ref()
.and_then(|panel| panel.active_item_id);
let mut active_ix = None;
for item in items {
if let Some(item) = item.log_err() {
let item_id = item.entity_id().as_u64();
pane.add_item(Box::new(item), false, false, None, cx);
if Some(item_id) == active_item_id {
active_ix = Some(pane.items_len() - 1);
}
}
}
if let Some(active_ix) = active_ix {
pane.activate_item(active_ix, false, false, cx)
}
})?;
if let Some(active_ix) = active_ix {
pane.activate_item(active_ix, false, false, cx)
}
})?;
Ok(panel)
})
Ok(panel)
}
fn handle_pane_event(
&mut self,
_pane: ViewHandle<Pane>,
_pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
match event {
pane::Event::ActivateItem { .. } => self.serialize(cx),
pane::Event::RemoveItem { .. } => self.serialize(cx),
pane::Event::Remove => cx.emit(Event::Close),
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
pane::Event::Focus => cx.emit(Event::Focus),
pane::Event::Remove => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::Focus => cx.emit(PanelEvent::Focus),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade(cx) {
if let Some(workspace) = self.workspace.upgrade() {
let pane = self.pane.clone();
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
}
@ -261,24 +256,23 @@ impl TerminalPanel {
fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move {
let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
workspace.update(&mut cx, |workspace, cx| {
let working_directory = if let Some(working_directory) = working_directory {
Some(working_directory)
} else {
let working_directory_strategy = settings::get::<TerminalSettings>(cx)
.working_directory
.clone();
let working_directory_strategy =
TerminalSettings::get_global(cx).working_directory.clone();
crate::get_working_directory(workspace, cx, working_directory_strategy)
};
let window = cx.window();
let window = cx.window_handle();
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
project
.create_terminal(working_directory, window, cx)
.log_err()
}) {
let terminal = Box::new(cx.add_view(|cx| {
let terminal = Box::new(cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
@ -287,7 +281,7 @@ impl TerminalPanel {
)
}));
pane.update(cx, |pane, cx| {
let focus = pane.has_focus();
let focus = pane.has_focus(cx);
pane.add_item(terminal, true, focus, None, cx);
});
}
@ -303,12 +297,16 @@ impl TerminalPanel {
.pane
.read(cx)
.items()
.map(|item| item.id())
.map(|item| item.item_id().as_u64())
.collect::<Vec<_>>();
let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
let active_item_id = self
.pane
.read(cx)
.active_item()
.map(|item| item.item_id().as_u64());
let height = self.height;
let width = self.width;
self.pending_serialization = cx.background().spawn(
self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
@ -328,29 +326,23 @@ impl TerminalPanel {
}
}
impl Entity for TerminalPanel {
type Event = Event;
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child(self.pane.clone())
}
}
impl View for TerminalPanel {
fn ui_name() -> &'static str {
"TerminalPanel"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
ChildView::new(&self.pane, cx).into_any()
}
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.pane);
}
impl FocusableView for TerminalPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.pane.focus_handle(cx)
}
}
impl Panel for TerminalPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
match settings::get::<TerminalSettings>(cx).dock {
match TerminalSettings::get_global(cx).dock {
TerminalDockPosition::Left => DockPosition::Left,
TerminalDockPosition::Bottom => DockPosition::Bottom,
TerminalDockPosition::Right => DockPosition::Right,
@ -372,8 +364,8 @@ impl Panel for TerminalPanel {
});
}
fn size(&self, cx: &WindowContext) -> f32 {
let settings = settings::get::<TerminalSettings>(cx);
fn size(&self, cx: &WindowContext) -> Pixels {
let settings = TerminalSettings::get_global(cx);
match self.position(cx) {
DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or_else(|| settings.default_width)
@ -382,7 +374,7 @@ impl Panel for TerminalPanel {
}
}
fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
@ -391,14 +383,6 @@ impl Panel for TerminalPanel {
cx.notify();
}
fn should_zoom_in_on_event(event: &Event) -> bool {
matches!(event, Event::ZoomIn)
}
fn should_zoom_out_on_event(event: &Event) -> bool {
matches!(event, Event::ZoomOut)
}
fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).is_zoomed()
}
@ -413,14 +397,6 @@ impl Panel for TerminalPanel {
}
}
fn icon_path(&self, _: &WindowContext) -> Option<&'static str> {
Some("icons/terminal.svg")
}
fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
let count = self.pane.read(cx).items_len();
if count == 0 {
@ -430,31 +406,32 @@ impl Panel for TerminalPanel {
}
}
fn should_change_position_on_event(event: &Self::Event) -> bool {
matches!(event, Event::DockPositionChanged)
fn persistent_name() -> &'static str {
"TerminalPanel"
}
fn should_activate_on_event(_: &Self::Event) -> bool {
false
// todo!()
// fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
// ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
// }
fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
Some(Icon::Terminal)
}
fn should_close_on_event(event: &Event) -> bool {
matches!(event, Event::Close)
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
Some("Terminal Panel")
}
fn has_focus(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).has_focus()
}
fn is_focus_event(event: &Self::Event) -> bool {
matches!(event, Event::Focus)
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
#[derive(Serialize, Deserialize)]
struct SerializedTerminalPanel {
items: Vec<usize>,
active_item_id: Option<usize>,
width: Option<f32>,
height: Option<f32>,
items: Vec<u64>,
active_item_id: Option<u64>,
width: Option<Pixels>,
height: Option<Pixels>,
}

View File

@ -2,48 +2,47 @@ mod persistence;
pub mod terminal_element;
pub mod terminal_panel;
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
use anyhow::Context;
use context_menu::{ContextMenu, ContextMenuItem};
use dirs::home_dir;
// todo!()
// use crate::terminal_element::TerminalElement;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions,
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
geometry::vector::Vector2F,
impl_actions,
keymap_matcher::{KeymapContext, Keystroke},
platform::{KeyDownEvent, ModifiersChangedEvent},
AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels,
Render, Styled, Subscription, Task, View, VisualContext, WeakView,
};
use language::Bias;
use persistence::TERMINAL_DB;
use project::{search::SearchQuery, LocalWorktree, Project};
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer;
use std::{
borrow::Cow,
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use terminal::{
alacritty_terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
Event, MaybeNavigationTarget, Terminal,
Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
};
use terminal_element::TerminalElement;
use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label};
use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent},
notifications::NotifyResultExt,
pane, register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
use anyhow::Context;
use dirs::home_dir;
use serde::Deserialize;
use settings::Settings;
use smol::Timer;
use std::{
ops::RangeInclusive,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@ -52,18 +51,13 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, PartialEq)]
pub struct ScrollTerminal(pub i32);
#[derive(Clone, Default, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct SendText(String);
#[derive(Clone, Default, Deserialize, PartialEq)]
#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
pub struct SendKeystroke(String);
actions!(
terminal,
[Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
);
impl_actions!(terminal, [SendText, SendKeystroke]);
impl_actions!(terminal_view, [SendText, SendKeystroke]);
pub fn init(cx: &mut AppContext) {
terminal_panel::init(cx);
@ -71,35 +65,37 @@ pub fn init(cx: &mut AppContext) {
register_deserializable_item::<TerminalView>(cx);
cx.add_action(TerminalView::deploy);
//Useful terminal views
cx.add_action(TerminalView::send_text);
cx.add_action(TerminalView::send_keystroke);
cx.add_action(TerminalView::copy);
cx.add_action(TerminalView::paste);
cx.add_action(TerminalView::clear);
cx.add_action(TerminalView::show_character_palette);
cx.add_action(TerminalView::select_all)
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(TerminalView::deploy);
})
.detach();
}
///A terminal view, maintains the PTY's file handles and communicates with the terminal
pub struct TerminalView {
terminal: ModelHandle<Terminal>,
terminal: Model<Terminal>,
focus_handle: FocusHandle,
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
context_menu: ViewHandle<ContextMenu>,
context_menu: Option<(View<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
blink_epoch: usize,
can_navigate_to_selected_word: bool,
workspace_id: WorkspaceId,
_subscriptions: Vec<Subscription>,
}
impl Entity for TerminalView {
type Event = Event;
impl EventEmitter<Event> for TerminalView {}
impl EventEmitter<ItemEvent> for TerminalView {}
impl EventEmitter<SearchEvent> for TerminalView {}
impl FocusableView for TerminalView {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl TerminalView {
@ -109,11 +105,11 @@ impl TerminalView {
_: &NewCenterTerminal,
cx: &mut ViewContext<Workspace>,
) {
let strategy = settings::get::<TerminalSettings>(cx);
let strategy = TerminalSettings::get_global(cx);
let working_directory =
get_working_directory(workspace, cx, strategy.working_directory.clone());
let window = cx.window();
let window = cx.window_handle();
let terminal = workspace
.project()
.update(cx, |project, cx| {
@ -122,7 +118,7 @@ impl TerminalView {
.notify_err(workspace, cx);
if let Some(terminal) = terminal {
let view = cx.add_view(|cx| {
let view = cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
@ -135,20 +131,21 @@ impl TerminalView {
}
pub fn new(
terminal: ModelHandle<Terminal>,
workspace: WeakViewHandle<Workspace>,
terminal: Model<Terminal>,
workspace: WeakView<Workspace>,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let view_id = cx.view_id();
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
cx.subscribe(&terminal, move |this, _, event, cx| match event {
Event::Wakeup => {
if !cx.is_self_focused() {
if !this.focus_handle.is_focused(cx) {
this.has_new_content = true;
}
cx.notify();
cx.emit(Event::Wakeup);
cx.emit(ItemEvent::UpdateTab);
cx.emit(SearchEvent::MatchesInvalidated);
}
Event::Bell => {
@ -159,15 +156,16 @@ impl TerminalView {
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
Event::TitleChanged => {
cx.emit(ItemEvent::UpdateTab);
if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
let cwd = foreground_info.cwd.clone();
let item_id = cx.view_id();
let item_id = cx.entity_id();
let workspace_id = this.workspace_id;
cx.background()
cx.background_executor()
.spawn(async move {
TERMINAL_DB
.save_working_directory(item_id, workspace_id, cwd)
.save_working_directory(item_id.as_u64(), workspace_id, cwd)
.await
.log_err();
})
@ -186,7 +184,7 @@ impl TerminalView {
}
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
MaybeNavigationTarget::Url(url) => cx.open_url(url),
MaybeNavigationTarget::PathLike(maybe_path) => {
if !this.can_navigate_to_selected_word {
@ -252,26 +250,37 @@ impl TerminalView {
}
}
},
_ => cx.emit(event.clone()),
Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
Event::SelectionsChanged => cx.emit(SearchEvent::ActiveMatchChanged),
})
.detach();
let focus_handle = cx.focus_handle();
let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| {
terminal_view.focus_in(cx);
});
let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, cx| {
terminal_view.focus_out(cx);
});
Self {
terminal,
has_new_content: true,
has_bell: false,
context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
focus_handle: cx.focus_handle(),
context_menu: None,
blink_state: true,
blinking_on: false,
blinking_paused: false,
blink_epoch: 0,
can_navigate_to_selected_word: false,
workspace_id,
_subscriptions: vec![focus_in, focus_out],
}
}
pub fn model(&self) -> &ModelHandle<Terminal> {
pub fn model(&self) -> &Model<Terminal> {
&self.terminal
}
@ -288,17 +297,29 @@ impl TerminalView {
cx.emit(Event::Wakeup);
}
pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
];
self.context_menu.update(cx, |menu, cx| {
menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
pub fn deploy_context_menu(
&mut self,
position: gpui::Point<Pixels>,
cx: &mut ViewContext<Self>,
) {
let context_menu = ContextMenu::build(cx, |menu, _| {
menu.action("Clear", Box::new(Clear))
.action("Close", Box::new(CloseActiveItem { save_intent: None }))
});
cx.notify();
cx.focus_view(&context_menu);
let subscription =
cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
if this.context_menu.as_ref().is_some_and(|context_menu| {
context_menu.0.focus_handle(cx).contains_focused(cx)
}) {
cx.focus_self();
}
this.context_menu.take();
cx.notify();
});
self.context_menu = Some((context_menu, position, subscription));
}
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
@ -314,7 +335,7 @@ impl TerminalView {
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&Keystroke::parse("ctrl-cmd-space").unwrap(),
settings::get::<TerminalSettings>(cx).option_as_meta,
TerminalSettings::get_global(cx).option_as_meta,
)
});
}
@ -345,7 +366,7 @@ impl TerminalView {
return true;
}
match settings::get::<TerminalSettings>(cx).blinking {
match TerminalSettings::get_global(cx).blinking {
//If the user requested to never blink, don't blink it.
TerminalBlink::Off => true,
//If the terminal is controlling it, check terminal mode
@ -392,11 +413,11 @@ impl TerminalView {
self.terminal
.update(cx, |term, cx| term.find_matches(searcher, cx))
} else {
cx.background().spawn(async { Vec::new() })
cx.background_executor().spawn(async { Vec::new() })
}
}
pub fn terminal(&self) -> &ModelHandle<Terminal> {
pub fn terminal(&self) -> &Model<Terminal> {
&self.terminal
}
@ -436,19 +457,91 @@ impl TerminalView {
if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
self.clear_bel(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&keystroke,
settings::get::<TerminalSettings>(cx).option_as_meta,
);
term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
});
}
}
fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
let mut dispatch_context = KeyContext::default();
dispatch_context.add("Terminal");
let mode = self.terminal.read(cx).last_content.mode;
dispatch_context.set(
"screen",
if mode.contains(TermMode::ALT_SCREEN) {
"alt"
} else {
"normal"
},
);
if mode.contains(TermMode::APP_CURSOR) {
dispatch_context.add("DECCKM");
}
if mode.contains(TermMode::APP_KEYPAD) {
dispatch_context.add("DECPAM");
} else {
dispatch_context.add("DECPNM");
}
if mode.contains(TermMode::SHOW_CURSOR) {
dispatch_context.add("DECTCEM");
}
if mode.contains(TermMode::LINE_WRAP) {
dispatch_context.add("DECAWM");
}
if mode.contains(TermMode::ORIGIN) {
dispatch_context.add("DECOM");
}
if mode.contains(TermMode::INSERT) {
dispatch_context.add("IRM");
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
dispatch_context.add("LNM");
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
dispatch_context.add("report_focus");
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
dispatch_context.add("alternate_scroll");
}
if mode.contains(TermMode::BRACKETED_PASTE) {
dispatch_context.add("bracketed_paste");
}
if mode.intersects(TermMode::MOUSE_MODE) {
dispatch_context.add("any_mouse_reporting");
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
"click"
} else if mode.contains(TermMode::MOUSE_DRAG) {
"drag"
} else if mode.contains(TermMode::MOUSE_MOTION) {
"motion"
} else {
"off"
};
dispatch_context.set("mouse_reporting", mouse_reporting);
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
"sgr"
} else if mode.contains(TermMode::UTF8_MOUSE) {
"utf8"
} else {
"normal"
};
dispatch_context.set("mouse_format", format);
};
dispatch_context
}
}
fn possible_open_targets(
workspace: &WeakViewHandle<Workspace>,
workspace: &WeakView<Workspace>,
maybe_path: &String,
cx: &mut ViewContext<'_, '_, TerminalView>,
cx: &mut ViewContext<'_, TerminalView>,
) -> Vec<PathLikeWithPosition<PathBuf>> {
let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
@ -467,7 +560,7 @@ fn possible_open_targets(
} else {
Vec::new()
}
} else if let Some(workspace) = workspace.upgrade(cx) {
} else if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
workspace
.worktrees(cx)
@ -495,198 +588,102 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<Re
searcher.ok()
}
impl View for TerminalView {
fn ui_name() -> &'static str {
"Terminal"
}
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
let terminal_handle = self.terminal.clone().downgrade();
let self_id = cx.view_id();
let focused = cx
.focused_view_id()
.filter(|view_id| *view_id == self_id)
.is_some();
Stack::new()
.with_child(
TerminalElement::new(
terminal_handle,
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
)
.contained(),
)
.with_child(ChildView::new(&self.context_menu, cx))
.into_any()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.has_new_content = false;
self.terminal.read(cx).focus_in();
self.blink_cursors(self.blink_epoch, cx);
cx.notify();
}
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |terminal, _| {
terminal.focus_out();
});
cx.notify();
}
fn modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) -> bool {
let handled = self
.terminal()
.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
if handled {
cx.notify();
}
handled
}
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
impl TerminalView {
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
self.terminal.update(cx, |term, cx| {
term.try_keystroke(
&event.keystroke,
settings::get::<TerminalSettings>(cx).option_as_meta,
TerminalSettings::get_global(cx).option_as_meta,
)
})
}
//IME stuff
fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
if self
.terminal
.read(cx)
.last_content
.mode
.contains(TermMode::ALT_SCREEN)
{
None
} else {
Some(0..0)
}
}
fn replace_text_in_range(
&mut self,
_: Option<std::ops::Range<usize>>,
text: &str,
cx: &mut ViewContext<Self>,
) {
self.terminal.update(cx, |terminal, _| {
terminal.input(text.into());
});
}
fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) {
Self::reset_to_default_keymap_context(keymap);
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
self.has_new_content = false;
self.terminal.read(cx).focus_in();
self.blink_cursors(self.blink_epoch, cx);
cx.notify();
}
let mode = self.terminal.read(cx).last_content.mode;
keymap.add_key(
"screen",
if mode.contains(TermMode::ALT_SCREEN) {
"alt"
} else {
"normal"
},
);
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
self.terminal.update(cx, |terminal, _| {
terminal.focus_out();
});
cx.notify();
}
}
if mode.contains(TermMode::APP_CURSOR) {
keymap.add_identifier("DECCKM");
}
if mode.contains(TermMode::APP_KEYPAD) {
keymap.add_identifier("DECPAM");
} else {
keymap.add_identifier("DECPNM");
}
if mode.contains(TermMode::SHOW_CURSOR) {
keymap.add_identifier("DECTCEM");
}
if mode.contains(TermMode::LINE_WRAP) {
keymap.add_identifier("DECAWM");
}
if mode.contains(TermMode::ORIGIN) {
keymap.add_identifier("DECOM");
}
if mode.contains(TermMode::INSERT) {
keymap.add_identifier("IRM");
}
//LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
keymap.add_identifier("LNM");
}
if mode.contains(TermMode::FOCUS_IN_OUT) {
keymap.add_identifier("report_focus");
}
if mode.contains(TermMode::ALTERNATE_SCROLL) {
keymap.add_identifier("alternate_scroll");
}
if mode.contains(TermMode::BRACKETED_PASTE) {
keymap.add_identifier("bracketed_paste");
}
if mode.intersects(TermMode::MOUSE_MODE) {
keymap.add_identifier("any_mouse_reporting");
}
{
let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
"click"
} else if mode.contains(TermMode::MOUSE_DRAG) {
"drag"
} else if mode.contains(TermMode::MOUSE_MOTION) {
"motion"
} else {
"off"
};
keymap.add_key("mouse_reporting", mouse_reporting);
}
{
let format = if mode.contains(TermMode::SGR_MOUSE) {
"sgr"
} else if mode.contains(TermMode::UTF8_MOUSE) {
"utf8"
} else {
"normal"
};
keymap.add_key("mouse_format", format);
}
impl Render for TerminalView {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let terminal_handle = self.terminal.clone();
let focused = self.focus_handle.is_focused(cx);
div()
.size_full()
.relative()
.track_focus(&self.focus_handle)
.key_context(self.dispatch_context(cx))
.on_action(cx.listener(TerminalView::send_text))
.on_action(cx.listener(TerminalView::send_keystroke))
.on_action(cx.listener(TerminalView::copy))
.on_action(cx.listener(TerminalView::paste))
.on_action(cx.listener(TerminalView::clear))
.on_action(cx.listener(TerminalView::show_character_palette))
.on_action(cx.listener(TerminalView::select_all))
.on_key_down(cx.listener(Self::key_down))
.on_mouse_down(
MouseButton::Right,
cx.listener(|this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, cx);
cx.notify();
}),
)
.child(
// TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
div().size_full().child(TerminalElement::new(
terminal_handle,
self.focus_handle.clone(),
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
)),
)
.children(self.context_menu.as_ref().map(|(menu, positon, _)| {
overlay()
.position(*positon)
.anchor(gpui::AnchorCorner::TopLeft)
.child(menu.clone())
}))
}
}
impl Item for TerminalView {
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
type Event = ItemEvent;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
Some(self.terminal().read(cx).title().into())
}
fn tab_content<T: 'static>(
fn tab_content(
&self,
_detail: Option<usize>,
tab_theme: &theme::Tab,
cx: &gpui::AppContext,
) -> AnyElement<T> {
selected: bool,
cx: &WindowContext,
) -> AnyElement {
let title = self.terminal().read(cx).title();
Flex::row()
.with_child(
gpui::elements::Svg::new("icons/terminal.svg")
.with_color(tab_theme.label.text.color)
.constrained()
.with_width(tab_theme.type_icon_width)
.aligned()
.contained()
.with_margin_right(tab_theme.spacing),
)
.with_child(Label::new(title, tab_theme.label.clone()).aligned())
h_stack()
.gap_2()
.child(IconElement::new(Icon::Terminal))
.child(Label::new(title).color(if selected {
Color::Default
} else {
Color::Muted
}))
.into_any()
}
@ -694,7 +691,7 @@ impl Item for TerminalView {
&self,
_workspace_id: WorkspaceId,
_cx: &mut ViewContext<Self>,
) -> Option<Self> {
) -> Option<View<Self>> {
//From what I can tell, there's no way to tell the current working
//Directory of the terminal from outside the shell. There might be
//solutions to this, but they are non-trivial and require more IPC
@ -717,21 +714,13 @@ impl Item for TerminalView {
false
}
fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
match event {
Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs],
Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab],
Event::CloseTerminal => smallvec![ItemEvent::CloseItem],
_ => smallvec![],
}
}
// todo!(search)
// fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
// Some(Box::new(handle.clone()))
// }
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@ -746,51 +735,55 @@ impl Item for TerminalView {
}
fn deserialize(
project: ModelHandle<Project>,
workspace: WeakViewHandle<Workspace>,
project: Model<Project>,
workspace: WeakView<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
cx: &mut ViewContext<Pane>,
) -> Task<anyhow::Result<ViewHandle<Self>>> {
let window = cx.window();
) -> Task<anyhow::Result<View<Self>>> {
let window = cx.window_handle();
cx.spawn(|pane, mut cx| async move {
let cwd = TERMINAL_DB
.get_working_directory(item_id, workspace_id)
.log_err()
.flatten()
.or_else(|| {
cx.read(|cx| {
let strategy = settings::get::<TerminalSettings>(cx)
.working_directory
.clone();
cx.update(|_, cx| {
let strategy = TerminalSettings::get_global(cx).working_directory.clone();
workspace
.upgrade(cx)
.upgrade()
.map(|workspace| {
get_working_directory(workspace.read(cx), cx, strategy)
})
.flatten()
})
.ok()
.flatten()
});
let terminal = project.update(&mut cx, |project, cx| {
project.create_terminal(cwd, window, cx)
})?;
Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
})?)
})??;
pane.update(&mut cx, |_, cx| {
cx.new_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
})
})
}
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
cx.background()
cx.background_executor()
.spawn(TERMINAL_DB.update_workspace_id(
workspace.database_id(),
self.workspace_id,
cx.view_id(),
cx.entity_id().as_u64(),
))
.detach();
self.workspace_id = workspace.database_id();
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
f(*event)
}
}
impl SearchableItem for TerminalView {
@ -805,19 +798,6 @@ impl SearchableItem for TerminalView {
}
}
/// Convert events raised by this item into search-relevant events (if applicable)
fn to_search_event(
&mut self,
event: &Self::Event,
_: &mut ViewContext<Self>,
) -> Option<SearchEvent> {
match event {
Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
_ => None,
}
}
/// Clear stored matches
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
self.terminal().update(cx, |term, _| term.matches.clear())
@ -1074,12 +1054,10 @@ mod tests {
}
/// Creates a worktree with 1 file: /root.txt
pub async fn init_test(
cx: &mut TestAppContext,
) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
let params = cx.update(AppState::test);
cx.update(|cx| {
theme::init((), cx);
theme::init(theme::LoadThemes::JustBase, cx);
Project::init_settings(cx);
language::init(cx);
});
@ -1087,35 +1065,36 @@ mod tests {
let project = Project::test(params.fs.clone(), [], cx).await;
let workspace = cx
.add_window(|cx| Workspace::test_new(project.clone(), cx))
.root(cx);
.root_view(cx)
.unwrap();
(project, workspace)
}
/// Creates a worktree with 1 folder: /root{suffix}/
async fn create_folder_wt(
project: ModelHandle<Project>,
project: Model<Project>,
path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Worktree>, Entry) {
) -> (Model<Worktree>, Entry) {
create_wt(project, true, path, cx).await
}
/// Creates a worktree with 1 file: /root{suffix}.txt
async fn create_file_wt(
project: ModelHandle<Project>,
project: Model<Project>,
path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Worktree>, Entry) {
) -> (Model<Worktree>, Entry) {
create_wt(project, false, path, cx).await
}
async fn create_wt(
project: ModelHandle<Project>,
project: Model<Project>,
is_dir: bool,
path: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> (ModelHandle<Worktree>, Entry) {
) -> (Model<Worktree>, Entry) {
let (wt, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree(path, true, cx)
@ -1139,9 +1118,9 @@ mod tests {
}
pub fn insert_active_entry_for(
wt: ModelHandle<Worktree>,
wt: Model<Worktree>,
entry: Entry,
project: ModelHandle<Project>,
project: Model<Project>,
cx: &mut TestAppContext,
) {
cx.update(|cx| {

View File

@ -1,46 +0,0 @@
[package]
name = "terminal_view2"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/terminal_view.rs"
doctest = false
[dependencies]
editor = { package = "editor2", path = "../editor2" }
language = { package = "language2", path = "../language2" }
gpui = { package = "gpui2", path = "../gpui2" }
project = { package = "project2", path = "../project2" }
# search = { package = "search2", path = "../search2" }
settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" }
workspace = { package = "workspace2", path = "../workspace2" }
db = { package = "db2", path = "../db2" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false }
terminal = { package = "terminal2", path = "../terminal2" }
ui = { package = "ui2", path = "../ui2" }
smallvec.workspace = true
smol.workspace = true
mio-extras = "2.0.6"
futures.workspace = true
ordered-float.workspace = true
itertools = "0.10"
dirs = "4.0.0"
shellexpand = "2.1.0"
libc = "0.2"
anyhow.workspace = true
thiserror.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_derive.workspace = true
[dev-dependencies]
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { package = "project2", path = "../project2", features = ["test-support"]}
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
rand.workspace = true

View File

@ -1,23 +0,0 @@
Design notes:
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
#Input
There are currently many distinct paths for getting keystrokes to the terminal:
1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal
2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View File

@ -1,96 +0,0 @@
#!/bin/bash
# Tom Hale, 2016. MIT Licence.
# Print out 256 colours, with each number printed in its corresponding colour
# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163
set -eu # Fail on errors or undeclared variables
printable_colours=256
# Return a colour that contrasts with the given colour
# Bash only does integer division, so keep it integral
function contrast_colour {
local r g b luminance
colour="$1"
if (( colour < 16 )); then # Initial 16 ANSI colours
(( colour == 0 )) && printf "15" || printf "0"
return
fi
# Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8
if (( colour > 231 )); then # Greyscale ramp
(( colour < 244 )) && printf "15" || printf "0"
return
fi
# All other colours:
# 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5]
# See http://stackoverflow.com/a/27165165/5353461
# r=$(( (colour-16) / 36 ))
g=$(( ((colour-16) % 36) / 6 ))
# b=$(( (colour-16) % 6 ))
# If luminance is bright, print number in black, white otherwise.
# Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601
(( g > 2)) && printf "0" || printf "15"
return
# Uncomment the below for more precise luminance calculations
# # Calculate perceived brightness
# # See https://www.w3.org/TR/AERT#color-contrast
# # and http://www.itu.int/rec/R-REC-BT.601
# # Luminance is in range 0..5000 as each value is 0..5
# luminance=$(( (r * 299) + (g * 587) + (b * 114) ))
# (( $luminance > 2500 )) && printf "0" || printf "15"
}
# Print a coloured block with the number of that colour
function print_colour {
local colour="$1" contrast
contrast=$(contrast_colour "$1")
printf "\e[48;5;%sm" "$colour" # Start block of colour
printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number
printf "\e[0m " # Reset colour
}
# Starting at $1, print a run of $2 colours
function print_run {
local i
for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do
print_colour "$i"
done
printf " "
}
# Print blocks of colours
function print_blocks {
local start="$1" i
local end="$2" # inclusive
local block_cols="$3"
local block_rows="$4"
local blocks_per_line="$5"
local block_length=$((block_cols * block_rows))
# Print sets of blocks
for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do
printf "\n" # Space before each set of blocks
# For each block row
for (( row = 0; row < block_rows; row++ )) do
# Print block columns for all blocks on the line
for (( block = 0; block < blocks_per_line; block++ )) do
print_run $(( i + (block * block_length) )) "$block_cols"
done
(( i += block_cols )) # Prepare to print the next row
printf "\n"
done
done
}
print_run 0 16 # The first 16 colours are spread over the whole spectrum
printf "\n"
print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive
print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey

View File

@ -1,19 +0,0 @@
#!/bin/bash
# Copied from: https://unix.stackexchange.com/a/696756
# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213
awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{
s="/\\";
total_cols=term_cols*term_lines;
for (colnum = 0; colnum<total_cols; colnum++) {
r = 255-(colnum*255/total_cols);
g = (colnum*510/total_cols);
b = (colnum*255/total_cols);
if (g>255) g = 510-g;
printf "\033[48;2;%d;%d;%dm", r,g,b;
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
printf "%s\033[0m", substr(s,colnum%2+1,1);
if (colnum%term_cols==term_cols) printf "\n";
}
printf "\n";
}'

View File

@ -1,71 +0,0 @@
use std::path::PathBuf;
use db::{define_connection, query, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection! {
pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
&[sql!(
CREATE TABLE terminals (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
// Remove the unique constraint on the item_id table
// SQLite doesn't have a way of doing this automatically, so
// we have to do this silly copying.
sql!(
CREATE TABLE terminals2 (
workspace_id INTEGER,
item_id INTEGER,
working_directory BLOB,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
INSERT INTO terminals2 (workspace_id, item_id, working_directory)
SELECT workspace_id, item_id, working_directory FROM terminals;
DROP TABLE terminals;
ALTER TABLE terminals2 RENAME TO terminals;
)];
}
impl TerminalDb {
query! {
pub async fn update_workspace_id(
new_id: WorkspaceId,
old_id: WorkspaceId,
item_id: ItemId
) -> Result<()> {
UPDATE terminals
SET workspace_id = ?
WHERE workspace_id = ? AND item_id = ?
}
}
query! {
pub async fn save_working_directory(
item_id: ItemId,
workspace_id: WorkspaceId,
working_directory: PathBuf
) -> Result<()> {
INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
VALUES (?, ?, ?)
}
}
query! {
pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT working_directory
FROM terminals
WHERE item_id = ? AND workspace_id = ?
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,437 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use project::Fs;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
use util::{ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
item::Item,
pane,
ui::Icon,
Pane, Workspace,
};
use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(terminal_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal);
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<TerminalPanel>(cx);
});
},
)
.detach();
}
pub struct TerminalPanel {
pane: View<Pane>,
fs: Arc<dyn Fs>,
workspace: WeakView<Workspace>,
width: Option<Pixels>,
height: Option<Pixels>,
pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>,
}
impl TerminalPanel {
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let terminal_panel = cx.view().clone();
let pane = cx.new_view(|cx| {
let mut pane = Pane::new(
workspace.weak_handle(),
workspace.project().clone(),
Default::default(),
Some(Arc::new(|a, cx| {
if let Some(tab) = a.downcast_ref::<workspace::pane::DraggedTab>() {
if let Some(item) = tab.pane.read(cx).item_for_index(tab.ix) {
return item.downcast::<TerminalView>().is_some();
}
}
if a.downcast_ref::<ExternalPaths>().is_some() {
return true;
}
false
})),
cx,
);
pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx);
pane.display_nav_history_buttons(false);
pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
h_stack()
.gap_2()
.child(
IconButton::new("plus", Icon::Plus)
.icon_size(IconSize::Small)
.on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| {
terminal_panel.add_terminal(None, cx);
}))
.tooltip(|cx| Tooltip::text("New Terminal", cx)),
)
.child({
let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", Icon::Maximize)
.icon_size(IconSize::Small)
.selected(zoomed)
.selected_icon(Icon::Minimize)
.on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx);
}))
.tooltip(move |cx| {
Tooltip::text(if zoomed { "Zoom Out" } else { "Zoom In" }, cx)
})
})
.into_any_element()
});
// let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
// pane.toolbar()
// .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane
});
let subscriptions = vec![
cx.observe(&pane, |_, _, cx| cx.notify()),
cx.subscribe(&pane, Self::handle_pane_event),
];
let this = Self {
pane,
fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(),
pending_serialization: Task::ready(None),
width: None,
height: None,
_subscriptions: subscriptions,
};
let mut old_dock_position = this.position(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
cx.emit(PanelEvent::ChangePosition);
}
})
.detach();
this
}
pub async fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
) -> Result<View<Self>> {
let serialized_panel = cx
.background_executor()
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.await
.log_err()
.flatten()
.map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
.transpose()
.log_err()
.flatten();
let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
panel.update(cx, |panel, cx| {
cx.notify();
panel.height = serialized_panel.height;
panel.width = serialized_panel.width;
panel.pane.update(cx, |_, cx| {
serialized_panel
.items
.iter()
.map(|item_id| {
TerminalView::deserialize(
workspace.project().clone(),
workspace.weak_handle(),
workspace.database_id(),
*item_id,
cx,
)
})
.collect::<Vec<_>>()
})
})
} else {
Default::default()
};
let pane = panel.read(cx).pane.clone();
(panel, pane, items)
})?;
let pane = pane.downgrade();
let items = futures::future::join_all(items).await;
pane.update(&mut cx, |pane, cx| {
let active_item_id = serialized_panel
.as_ref()
.and_then(|panel| panel.active_item_id);
let mut active_ix = None;
for item in items {
if let Some(item) = item.log_err() {
let item_id = item.entity_id().as_u64();
pane.add_item(Box::new(item), false, false, None, cx);
if Some(item_id) == active_item_id {
active_ix = Some(pane.items_len() - 1);
}
}
}
if let Some(active_ix) = active_ix {
pane.activate_item(active_ix, false, false, cx)
}
})?;
Ok(panel)
}
fn handle_pane_event(
&mut self,
_pane: View<Pane>,
event: &pane::Event,
cx: &mut ViewContext<Self>,
) {
match event {
pane::Event::ActivateItem { .. } => self.serialize(cx),
pane::Event::RemoveItem { .. } => self.serialize(cx),
pane::Event::Remove => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::Focus => cx.emit(PanelEvent::Focus),
pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade() {
let pane = self.pane.clone();
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
}
}
_ => {}
}
}
pub fn open_terminal(
workspace: &mut Workspace,
action: &workspace::OpenTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| {
this.add_terminal(Some(action.working_directory.clone()), cx)
})
}
///Create a new Terminal in the current working directory or the user's home directory
fn new_terminal(
workspace: &mut Workspace,
_: &workspace::NewTerminal,
cx: &mut ViewContext<Workspace>,
) {
let Some(this) = workspace.focus_panel::<Self>(cx) else {
return;
};
this.update(cx, |this, cx| this.add_terminal(None, cx))
}
fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move {
let pane = this.update(&mut cx, |this, _| this.pane.clone())?;
workspace.update(&mut cx, |workspace, cx| {
let working_directory = if let Some(working_directory) = working_directory {
Some(working_directory)
} else {
let working_directory_strategy =
TerminalSettings::get_global(cx).working_directory.clone();
crate::get_working_directory(workspace, cx, working_directory_strategy)
};
let window = cx.window_handle();
if let Some(terminal) = workspace.project().update(cx, |project, cx| {
project
.create_terminal(working_directory, window, cx)
.log_err()
}) {
let terminal = Box::new(cx.new_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
}));
pane.update(cx, |pane, cx| {
let focus = pane.has_focus(cx);
pane.add_item(terminal, true, focus, None, cx);
});
}
})?;
this.update(&mut cx, |this, cx| this.serialize(cx))?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn serialize(&mut self, cx: &mut ViewContext<Self>) {
let items = self
.pane
.read(cx)
.items()
.map(|item| item.item_id().as_u64())
.collect::<Vec<_>>();
let active_item_id = self
.pane
.read(cx)
.active_item()
.map(|item| item.item_id().as_u64());
let height = self.height;
let width = self.width;
self.pending_serialization = cx.background_executor().spawn(
async move {
KEY_VALUE_STORE
.write_kvp(
TERMINAL_PANEL_KEY.into(),
serde_json::to_string(&SerializedTerminalPanel {
items,
active_item_id,
height,
width,
})?,
)
.await?;
anyhow::Ok(())
}
.log_err(),
);
}
}
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div().size_full().child(self.pane.clone())
}
}
impl FocusableView for TerminalPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.pane.focus_handle(cx)
}
}
impl Panel for TerminalPanel {
fn position(&self, cx: &WindowContext) -> DockPosition {
match TerminalSettings::get_global(cx).dock {
TerminalDockPosition::Left => DockPosition::Left,
TerminalDockPosition::Bottom => DockPosition::Bottom,
TerminalDockPosition::Right => DockPosition::Right,
}
}
fn position_is_valid(&self, _: DockPosition) -> bool {
true
}
fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
settings::update_settings_file::<TerminalSettings>(self.fs.clone(), cx, move |settings| {
let dock = match position {
DockPosition::Left => TerminalDockPosition::Left,
DockPosition::Bottom => TerminalDockPosition::Bottom,
DockPosition::Right => TerminalDockPosition::Right,
};
settings.dock = Some(dock);
});
}
fn size(&self, cx: &WindowContext) -> Pixels {
let settings = TerminalSettings::get_global(cx);
match self.position(cx) {
DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or_else(|| settings.default_width)
}
DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
}
}
fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size,
}
self.serialize(cx);
cx.notify();
}
fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).is_zoomed()
}
fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
}
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
if active && self.pane.read(cx).items_len() == 0 {
self.add_terminal(None, cx)
}
}
fn icon_label(&self, cx: &WindowContext) -> Option<String> {
let count = self.pane.read(cx).items_len();
if count == 0 {
None
} else {
Some(count.to_string())
}
}
fn persistent_name() -> &'static str {
"TerminalPanel"
}
// todo!()
// fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
// ("Terminal Panel".into(), Some(Box::new(ToggleFocus)))
// }
fn icon(&self, _cx: &WindowContext) -> Option<Icon> {
Some(Icon::Terminal)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
Some("Terminal Panel")
}
fn toggle_action(&self) -> Box<dyn gpui::Action> {
Box::new(ToggleFocus)
}
}
#[derive(Serialize, Deserialize)]
struct SerializedTerminalPanel {
items: Vec<u64>,
active_item_id: Option<u64>,
width: Option<Pixels>,
height: Option<Pixels>,
}

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@ command_palette = { path = "../command_palette" }
editor = { package = "editor2", path = "../editor2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" }
workspace = { package = "workspace2", path = "../workspace2" }
theme = { package = "theme2", path = "../theme2" }

View File

@ -18,7 +18,7 @@ path = "src/main.rs"
ai = { package = "ai2", path = "../ai2"}
audio = { package = "audio2", path = "../audio2" }
activity_indicator = { path = "../activity_indicator"}
auto_update = { package = "auto_update2", path = "../auto_update2" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
call = { package = "call2", path = "../call2" }
channel = { package = "channel2", path = "../channel2" }
@ -36,7 +36,7 @@ db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
feedback = { package="feedback2", path = "../feedback2" }
file_finder = { path = "../file_finder" }
search = { package = "search2", path = "../search2" }
search = { path = "../search" }
fs = { package = "fs2", path = "../fs2" }
fsevent = { path = "../fsevent" }
go_to_line = { path = "../go_to_line" }
@ -47,7 +47,7 @@ language = { package = "language2", path = "../language2" }
language_selector = { path = "../language_selector" }
lsp = { package = "lsp2", path = "../lsp2" }
menu = { package = "menu2", path = "../menu2" }
language_tools = { package = "language_tools2", path = "../language_tools2" }
language_tools = { path = "../language_tools" }
node_runtime = { path = "../node_runtime" }
notifications = { package = "notifications2", path = "../notifications2" }
assistant = { package = "assistant2", path = "../assistant2" }
@ -65,7 +65,7 @@ feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
sum_tree = { path = "../sum_tree" }
shellexpand = "2.1.0"
text = { package = "text2", path = "../text2" }
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
terminal_view = { path = "../terminal_view" }
theme = { package = "theme2", path = "../theme2" }
theme_selector = { path = "../theme_selector" }
util = { path = "../util" }