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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,106 +1,56 @@
use crate::ViewReleaseNotes;
use gpui::{ use gpui::{
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text}, div, DismissEvent, EventEmitter, InteractiveElement, IntoElement, ParentElement, Render,
platform::{AppVersion, CursorStyle, MouseButton}, SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
Element, Entity, View, ViewContext,
}; };
use menu::Cancel; use menu::Cancel;
use util::channel::ReleaseChannel; use util::channel::ReleaseChannel;
use workspace::notifications::Notification; use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
pub struct UpdateNotification { pub struct UpdateNotification {
version: AppVersion, version: SemanticVersion,
} }
pub enum Event { impl EventEmitter<DismissEvent> for UpdateNotification {}
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 Render for UpdateNotification {
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
let app_name = cx.global::<ReleaseChannel>().display_name(); let app_name = cx.global::<ReleaseChannel>().display_name();
MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| { v_stack()
Flex::column() .on_action(cx.listener(UpdateNotification::dismiss))
.with_child( .elevation_3(cx)
Flex::row() .p_4()
.with_child( .child(
Text::new( h_stack()
format!("Updated to {app_name} {}", self.version), .justify_between()
theme.message.text.clone(), .child(Label::new(format!(
) "Updated to {app_name} {}",
.contained() self.version
.with_style(theme.message.container) )))
.aligned() .child(
.top() div()
.left() .id("cancel")
.flex(1., true), .child(IconElement::new(Icon::Close))
) .cursor_pointer()
.with_child( .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| { ),
let style = theme.dismiss_button.style_for(state); )
Svg::new("icons/x.svg") .child(
.with_color(style.color) div()
.constrained() .id("notes")
.with_width(style.icon_width) .child(Label::new("View the release notes"))
.aligned() .cursor_pointer()
.contained() .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
.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)
} }
} }
impl UpdateNotification { impl UpdateNotification {
pub fn new(version: AppVersion) -> Self { pub fn new(version: SemanticVersion) -> Self {
Self { version } Self { version }
} }
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { 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" } ui = { package = "ui2", path = "../ui2" }
language = { package = "language2", path = "../language2" } language = { package = "language2", path = "../language2" }
project = { package = "project2", path = "../project2" } project = { package = "project2", path = "../project2" }
search = { package = "search2", path = "../search2" } search = { path = "../search" }
settings = { package = "settings2", path = "../settings2" } settings = { package = "settings2", path = "../settings2" }
theme = { package = "theme2", path = "../theme2" } theme = { package = "theme2", path = "../theme2" }
workspace = { package = "workspace2", path = "../workspace2" } workspace = { package = "workspace2", path = "../workspace2" }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,16 +11,17 @@ doctest = false
[dependencies] [dependencies]
bitflags = "1" bitflags = "1"
collections = { path = "../collections" } collections = { path = "../collections" }
editor = { path = "../editor" } editor = { package = "editor2", path = "../editor2" }
gpui = { path = "../gpui" } gpui = { package = "gpui2", path = "../gpui2" }
language = { path = "../language" } language = { package = "language2", path = "../language2" }
menu = { path = "../menu" } menu = { package = "menu2", path = "../menu2" }
project = { path = "../project" } project = { package = "project2", path = "../project2" }
settings = { path = "../settings" } settings = { package = "settings2", path = "../settings2" }
theme = { path = "../theme" } theme = { package = "theme2", path = "../theme2" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } ui = {package = "ui2", path = "../ui2"}
semantic_index = { path = "../semantic_index" } workspace = { package = "workspace2", path = "../workspace2" }
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
anyhow.workspace = true anyhow.workspace = true
futures.workspace = true futures.workspace = true
log.workspace = true log.workspace = true
@ -31,9 +32,9 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
serde_json.workspace = true serde_json.workspace = true
[dev-dependencies] [dev-dependencies]
client = { path = "../client", features = ["test-support"] } client = { package = "client2", path = "../client2", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] } editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { path = "../gpui", 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 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}; use crate::{ActivateRegexMode, ActivateSemanticMode, ActivateTextMode};
// TODO: Update the default search mode to get from config // TODO: Update the default search mode to get from config
#[derive(Copy, Clone, Debug, Default, PartialEq)] #[derive(Copy, Clone, Debug, Default, PartialEq)]
pub enum SearchMode { pub enum SearchMode {
@ -10,12 +11,6 @@ pub enum SearchMode {
Regex, Regex,
} }
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum Side {
Left,
Right,
}
impl SearchMode { impl SearchMode {
pub(crate) fn label(&self) -> &'static str { pub(crate) fn label(&self) -> &'static str {
match self { match self {
@ -24,28 +19,14 @@ impl SearchMode {
SearchMode::Regex => "Regex", SearchMode::Regex => "Regex",
} }
} }
pub(crate) fn tooltip(&self) -> SharedString {
pub(crate) fn region_id(&self) -> usize { format!("Activate {} Mode", self.label()).into()
match self {
SearchMode::Text => 3,
SearchMode::Semantic => 4,
SearchMode::Regex => 5,
}
} }
pub(crate) fn action(&self) -> Box<dyn Action> {
pub(crate) fn tooltip_text(&self) -> &'static str {
match self { match self {
SearchMode::Text => "Activate Text Search", SearchMode::Text => ActivateTextMode.boxed_clone(),
SearchMode::Semantic => "Activate Semantic Search", SearchMode::Semantic => ActivateSemanticMode.boxed_clone(),
SearchMode::Regex => "Activate Regex Search", SearchMode::Regex => ActivateRegexMode.boxed_clone(),
}
}
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),
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

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 doctest = false
[dependencies] [dependencies]
context_menu = { path = "../context_menu" } editor = { package = "editor2", path = "../editor2" }
editor = { path = "../editor" } language = { package = "language2", path = "../language2" }
language = { path = "../language" } gpui = { package = "gpui2", path = "../gpui2" }
gpui = { path = "../gpui" } project = { package = "project2", path = "../project2" }
project = { path = "../project" } # search = { path = "../search" }
search = { path = "../search" } settings = { package = "settings2", path = "../settings2" }
settings = { path = "../settings" } theme = { package = "theme2", path = "../theme2" }
theme = { path = "../theme" }
util = { path = "../util" } util = { path = "../util" }
workspace = { path = "../workspace" } workspace = { package = "workspace2", path = "../workspace2" }
db = { path = "../db" } db = { package = "db2", path = "../db2" }
procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } 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 smallvec.workspace = true
smol.workspace = true smol.workspace = true
mio-extras = "2.0.6" mio-extras = "2.0.6"
@ -38,9 +38,9 @@ serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
[dev-dependencies] [dev-dependencies]
editor = { path = "../editor", features = ["test-support"] } editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
client = { path = "../client", features = ["test-support"]} client = { package = "client2", path = "../client2", features = ["test-support"]}
project = { path = "../project", features = ["test-support"]} project = { package = "project2", path = "../project2", features = ["test-support"]}
workspace = { path = "../workspace", features = ["test-support"] } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
rand.workspace = true 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 crate::TerminalView;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use gpui::{ use gpui::{
actions, anyhow::Result, elements::*, serde_json, Action, AppContext, AsyncAppContext, Entity, actions, div, serde_json, AppContext, AsyncWindowContext, Entity, EventEmitter, ExternalPaths,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, FocusHandle, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription,
Task, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
use project::Fs; use project::Fs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::SettingsStore; use settings::{Settings, SettingsStore};
use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings};
use ui::{h_stack, ButtonCommon, Clickable, IconButton, IconSize, Selectable, Tooltip};
use util::{ResultExt, TryFutureExt}; use util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel}, dock::{DockPosition, Panel, PanelEvent},
item::Item, item::Item,
pane, DraggedItem, Pane, Workspace, pane,
ui::Icon,
Pane, Workspace,
}; };
use anyhow::Result;
const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
actions!(terminal_panel, [ToggleFocus]); actions!(terminal_panel, [ToggleFocus]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(TerminalPanel::new_terminal); cx.observe_new_views(
cx.add_action(TerminalPanel::open_terminal); |workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
} workspace.register_action(TerminalPanel::new_terminal);
workspace.register_action(TerminalPanel::open_terminal);
#[derive(Debug)] workspace.register_action(|workspace, _: &ToggleFocus, cx| {
pub enum Event { workspace.toggle_panel_focus::<TerminalPanel>(cx);
Close, });
DockPositionChanged, },
ZoomIn, )
ZoomOut, .detach();
Focus,
} }
pub struct TerminalPanel { pub struct TerminalPanel {
pane: ViewHandle<Pane>, pane: View<Pane>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
workspace: WeakViewHandle<Workspace>, workspace: WeakView<Workspace>,
width: Option<f32>, width: Option<Pixels>,
height: Option<f32>, height: Option<Pixels>,
pending_serialization: Task<Option<()>>, pending_serialization: Task<Option<()>>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
impl TerminalPanel { impl TerminalPanel {
fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self { fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
let weak_self = cx.weak_handle(); let terminal_panel = cx.view().clone();
let pane = cx.add_view(|cx| { let pane = cx.new_view(|cx| {
let window = cx.window();
let mut pane = Pane::new( let mut pane = Pane::new(
workspace.weak_handle(), workspace.weak_handle(),
workspace.project().clone(), workspace.project().clone(),
workspace.app_state().background_actions,
Default::default(), 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, cx,
); );
pane.set_can_split(false, cx); pane.set_can_split(false, cx);
pane.set_can_navigate(false, cx); pane.set_can_navigate(false, cx);
pane.on_can_drop(move |drag_and_drop, cx| { pane.display_nav_history_buttons(false);
drag_and_drop
.currently_dragged::<DraggedItem>(window)
.map_or(false, |(_, item)| {
item.handle.act_as::<TerminalView>(cx).is_some()
})
});
pane.set_render_tab_bar_buttons(cx, move |pane, cx| { pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
let this = weak_self.clone(); h_stack()
Flex::row() .gap_2()
.with_child(Pane::render_tab_bar_button( .child(
0, IconButton::new("plus", Icon::Plus)
"icons/plus.svg", .icon_size(IconSize::Small)
false, .on_click(cx.listener_for(&terminal_panel, |terminal_panel, _, cx| {
Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))), terminal_panel.add_terminal(None, cx);
cx, }))
move |_, cx| { .tooltip(|cx| Tooltip::text("New Terminal", cx)),
let this = this.clone(); )
cx.window_context().defer(move |cx| { .child({
if let Some(this) = this.upgrade(cx) { let zoomed = pane.is_zoomed();
this.update(cx, |this, cx| { IconButton::new("toggle_zoom", Icon::Maximize)
this.add_terminal(None, cx); .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()
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()
}); });
let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); // let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
pane.toolbar() // pane.toolbar()
.update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); // .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
pane pane
}); });
let subscriptions = vec![ let subscriptions = vec![
@ -123,105 +120,103 @@ impl TerminalPanel {
_subscriptions: subscriptions, _subscriptions: subscriptions,
}; };
let mut old_dock_position = this.position(cx); 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); let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position { if new_dock_position != old_dock_position {
old_dock_position = new_dock_position; old_dock_position = new_dock_position;
cx.emit(Event::DockPositionChanged); cx.emit(PanelEvent::ChangePosition);
} }
}) })
.detach(); .detach();
this this
} }
pub fn load( pub async fn load(
workspace: WeakViewHandle<Workspace>, workspace: WeakView<Workspace>,
cx: AsyncAppContext, mut cx: AsyncWindowContext,
) -> Task<Result<ViewHandle<Self>>> { ) -> Result<View<Self>> {
cx.spawn(|mut cx| async move { let serialized_panel = cx
let serialized_panel = if let Some(panel) = cx .background_executor()
.background() .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
.spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) .await
.await .log_err()
.log_err() .flatten()
.flatten() .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))
{ .transpose()
Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?) .log_err()
} else { .flatten();
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)
})?;
let pane = pane.downgrade(); let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
let items = futures::future::join_all(items).await; let panel = cx.new_view(|cx| TerminalPanel::new(workspace, cx));
pane.update(&mut cx, |pane, cx| { let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
let active_item_id = serialized_panel panel.update(cx, |panel, cx| {
.as_ref() cx.notify();
.and_then(|panel| panel.active_item_id); panel.height = serialized_panel.height;
let mut active_ix = None; panel.width = serialized_panel.width;
for item in items { panel.pane.update(cx, |_, cx| {
if let Some(item) = item.log_err() { serialized_panel
let item_id = item.id(); .items
pane.add_item(Box::new(item), false, false, None, cx); .iter()
if Some(item_id) == active_item_id { .map(|item_id| {
active_ix = Some(pane.items_len() - 1); 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 { if let Some(active_ix) = active_ix {
pane.activate_item(active_ix, false, false, cx) pane.activate_item(active_ix, false, false, cx)
} }
})?; })?;
Ok(panel) Ok(panel)
})
} }
fn handle_pane_event( fn handle_pane_event(
&mut self, &mut self,
_pane: ViewHandle<Pane>, _pane: View<Pane>,
event: &pane::Event, event: &pane::Event,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
match event { match event {
pane::Event::ActivateItem { .. } => self.serialize(cx), pane::Event::ActivateItem { .. } => self.serialize(cx),
pane::Event::RemoveItem { .. } => self.serialize(cx), pane::Event::RemoveItem { .. } => self.serialize(cx),
pane::Event::Remove => cx.emit(Event::Close), pane::Event::Remove => cx.emit(PanelEvent::Close),
pane::Event::ZoomIn => cx.emit(Event::ZoomIn), pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
pane::Event::ZoomOut => cx.emit(Event::ZoomOut), pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
pane::Event::Focus => cx.emit(Event::Focus), pane::Event::Focus => cx.emit(PanelEvent::Focus),
pane::Event::AddItem { item } => { pane::Event::AddItem { item } => {
if let Some(workspace) = self.workspace.upgrade(cx) { if let Some(workspace) = self.workspace.upgrade() {
let pane = self.pane.clone(); let pane = self.pane.clone();
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) 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>) { fn add_terminal(&mut self, working_directory: Option<PathBuf>, cx: &mut ViewContext<Self>) {
let workspace = self.workspace.clone(); let workspace = self.workspace.clone();
cx.spawn(|this, mut cx| async move { 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| { workspace.update(&mut cx, |workspace, cx| {
let working_directory = if let Some(working_directory) = working_directory { let working_directory = if let Some(working_directory) = working_directory {
Some(working_directory) Some(working_directory)
} else { } else {
let working_directory_strategy = settings::get::<TerminalSettings>(cx) let working_directory_strategy =
.working_directory TerminalSettings::get_global(cx).working_directory.clone();
.clone();
crate::get_working_directory(workspace, cx, working_directory_strategy) 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| { if let Some(terminal) = workspace.project().update(cx, |project, cx| {
project project
.create_terminal(working_directory, window, cx) .create_terminal(working_directory, window, cx)
.log_err() .log_err()
}) { }) {
let terminal = Box::new(cx.add_view(|cx| { let terminal = Box::new(cx.new_view(|cx| {
TerminalView::new( TerminalView::new(
terminal, terminal,
workspace.weak_handle(), workspace.weak_handle(),
@ -287,7 +281,7 @@ impl TerminalPanel {
) )
})); }));
pane.update(cx, |pane, cx| { pane.update(cx, |pane, cx| {
let focus = pane.has_focus(); let focus = pane.has_focus(cx);
pane.add_item(terminal, true, focus, None, cx); pane.add_item(terminal, true, focus, None, cx);
}); });
} }
@ -303,12 +297,16 @@ impl TerminalPanel {
.pane .pane
.read(cx) .read(cx)
.items() .items()
.map(|item| item.id()) .map(|item| item.item_id().as_u64())
.collect::<Vec<_>>(); .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 height = self.height;
let width = self.width; let width = self.width;
self.pending_serialization = cx.background().spawn( self.pending_serialization = cx.background_executor().spawn(
async move { async move {
KEY_VALUE_STORE KEY_VALUE_STORE
.write_kvp( .write_kvp(
@ -328,29 +326,23 @@ impl TerminalPanel {
} }
} }
impl Entity for TerminalPanel { impl EventEmitter<PanelEvent> for TerminalPanel {}
type Event = Event;
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 { impl FocusableView for TerminalPanel {
fn ui_name() -> &'static str { fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
"TerminalPanel" self.pane.focus_handle(cx)
}
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 Panel for TerminalPanel { impl Panel for TerminalPanel {
fn position(&self, cx: &WindowContext) -> DockPosition { fn position(&self, cx: &WindowContext) -> DockPosition {
match settings::get::<TerminalSettings>(cx).dock { match TerminalSettings::get_global(cx).dock {
TerminalDockPosition::Left => DockPosition::Left, TerminalDockPosition::Left => DockPosition::Left,
TerminalDockPosition::Bottom => DockPosition::Bottom, TerminalDockPosition::Bottom => DockPosition::Bottom,
TerminalDockPosition::Right => DockPosition::Right, TerminalDockPosition::Right => DockPosition::Right,
@ -372,8 +364,8 @@ impl Panel for TerminalPanel {
}); });
} }
fn size(&self, cx: &WindowContext) -> f32 { fn size(&self, cx: &WindowContext) -> Pixels {
let settings = settings::get::<TerminalSettings>(cx); let settings = TerminalSettings::get_global(cx);
match self.position(cx) { match self.position(cx) {
DockPosition::Left | DockPosition::Right => { DockPosition::Left | DockPosition::Right => {
self.width.unwrap_or_else(|| settings.default_width) 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) { match self.position(cx) {
DockPosition::Left | DockPosition::Right => self.width = size, DockPosition::Left | DockPosition::Right => self.width = size,
DockPosition::Bottom => self.height = size, DockPosition::Bottom => self.height = size,
@ -391,14 +383,6 @@ impl Panel for TerminalPanel {
cx.notify(); 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 { fn is_zoomed(&self, cx: &WindowContext) -> bool {
self.pane.read(cx).is_zoomed() 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> { fn icon_label(&self, cx: &WindowContext) -> Option<String> {
let count = self.pane.read(cx).items_len(); let count = self.pane.read(cx).items_len();
if count == 0 { if count == 0 {
@ -430,31 +406,32 @@ impl Panel for TerminalPanel {
} }
} }
fn should_change_position_on_event(event: &Self::Event) -> bool { fn persistent_name() -> &'static str {
matches!(event, Event::DockPositionChanged) "TerminalPanel"
} }
fn should_activate_on_event(_: &Self::Event) -> bool { // todo!()
false // 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 { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
matches!(event, Event::Close) Some("Terminal Panel")
} }
fn has_focus(&self, cx: &WindowContext) -> bool { fn toggle_action(&self) -> Box<dyn gpui::Action> {
self.pane.read(cx).has_focus() Box::new(ToggleFocus)
}
fn is_focus_event(event: &Self::Event) -> bool {
matches!(event, Event::Focus)
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct SerializedTerminalPanel { struct SerializedTerminalPanel {
items: Vec<usize>, items: Vec<u64>,
active_item_id: Option<usize>, active_item_id: Option<u64>,
width: Option<f32>, width: Option<Pixels>,
height: Option<f32>, height: Option<Pixels>,
} }

View File

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

View File

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