mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 07:32:02 +03:00
Merge branch 'main' into copilot-disabled-globs
This commit is contained in:
commit
9d41f83b1b
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1973,6 +1973,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
@ -2156,6 +2157,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sysinfo",
|
||||
"theme",
|
||||
"tree-sitter-markdown",
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
|
||||
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
|
||||
|
||||
## Development tips
|
||||
|
||||
@ -31,7 +31,8 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
|
||||
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
|
||||
Create a personal GitHub token to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
|
||||
Then delete that token.
|
||||
|
||||
```
|
||||
GITHUB_TOKEN=<$token> script/bootstrap
|
||||
|
@ -5,7 +5,7 @@ use gpui::{
|
||||
actions, anyhow,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
|
||||
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
@ -45,7 +45,7 @@ struct PendingWork<'a> {
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@ -199,7 +199,7 @@ impl ActivityIndicator {
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
@ -230,7 +230,7 @@ impl ActivityIndicator {
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
@ -244,7 +244,7 @@ impl ActivityIndicator {
|
||||
""
|
||||
}
|
||||
),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return Content {
|
||||
@ -254,7 +254,9 @@ impl ActivityIndicator {
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
action: Some(Box::new(ShowErrorMessage)),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@ -264,27 +266,31 @@ impl ActivityIndicator {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
action: Some(Box::new(workspace::Restart)),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
action: Some(Box::new(DismissErrorMessage)),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
@ -294,7 +300,7 @@ impl ActivityIndicator {
|
||||
return Content {
|
||||
icon: None,
|
||||
message: most_recent_active_task.to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
@ -315,7 +321,7 @@ impl View for ActivityIndicator {
|
||||
let Content {
|
||||
icon,
|
||||
message,
|
||||
action,
|
||||
on_click,
|
||||
} = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
|
||||
@ -325,7 +331,7 @@ impl View for ActivityIndicator {
|
||||
.workspace
|
||||
.status_bar
|
||||
.lsp_status;
|
||||
let style = if state.hovered() && action.is_some() {
|
||||
let style = if state.hovered() && on_click.is_some() {
|
||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||
} else {
|
||||
&theme.default
|
||||
@ -353,12 +359,10 @@ impl View for ActivityIndicator {
|
||||
.aligned()
|
||||
});
|
||||
|
||||
if let Some(action) = action {
|
||||
if let Some(on_click) = on_click.clone() {
|
||||
element = element
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
});
|
||||
.on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
|
||||
}
|
||||
|
||||
element.into_any()
|
||||
|
@ -1,13 +1,15 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use client::{Client, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
Task, WeakViewHandle,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use settings::Settings;
|
||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
@ -21,6 +23,13 @@ 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,
|
||||
@ -51,9 +60,8 @@ impl Entity for AutoUpdater {
|
||||
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
|
||||
let server_url = server_url;
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url.clone());
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
let mut update_subscription = cx
|
||||
.global::<Settings>()
|
||||
@ -74,25 +82,32 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
cx.add_global_action(|_: &Check, cx| {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
}
|
||||
});
|
||||
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
});
|
||||
cx.add_global_action(check);
|
||||
cx.add_global_action(view_release_notes);
|
||||
cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let server_url = &auto_updater.read(cx).server_url;
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
@ -241,7 +256,24 @@ impl AutoUpdater {
|
||||
mounted_app_path.push("/");
|
||||
|
||||
let mut dmg_file = File::create(&dmg_path).await?;
|
||||
let mut response = client.get(&release.url, Default::default(), true).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.read(|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 = cx.global::<Settings>().telemetry().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.post_json(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", dmg_path);
|
||||
|
||||
|
@ -63,8 +63,8 @@ impl View for UpdateNotification {
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(Cancel)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx)
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
@ -84,7 +84,7 @@ impl View for UpdateNotification {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(ViewReleaseNotes)
|
||||
crate::view_release_notes(&Default::default(), cx)
|
||||
})
|
||||
.into_any_named("update notification")
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
use gpui::{
|
||||
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
|
||||
ViewHandle,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use search::ProjectSearchView;
|
||||
use settings::Settings;
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView,
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
@ -19,15 +19,17 @@ pub struct Breadcrumbs {
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
project_search: Option<ViewHandle<ProjectSearchView>>,
|
||||
subscription: Option<Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
project_search: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -85,8 +87,12 @@ impl View for Breadcrumbs {
|
||||
let style = style.style_for(state, false);
|
||||
crumbs.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(outline::Toggle);
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
outline::toggle(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Breadcrumbs>(
|
||||
0,
|
||||
|
@ -17,7 +17,7 @@ use futures::{
|
||||
use gpui::{
|
||||
actions,
|
||||
platform::AppVersion,
|
||||
serde_json::{self, Value},
|
||||
serde_json::{self},
|
||||
AnyModelHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity,
|
||||
ModelHandle, Task, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
@ -27,7 +27,7 @@ use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, TelemetrySettings};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
@ -47,6 +47,7 @@ use util::http::HttpClient;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use rpc::*;
|
||||
pub use telemetry::ClickhouseEvent;
|
||||
pub use user::*;
|
||||
|
||||
lazy_static! {
|
||||
@ -736,7 +737,7 @@ impl Client {
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
cx.read(|cx| {
|
||||
self.report_event(
|
||||
self.telemetry().report_mixpanel_event(
|
||||
"read credentials from keychain",
|
||||
Default::default(),
|
||||
cx.global::<Settings>().telemetry(),
|
||||
@ -1116,7 +1117,7 @@ impl Client {
|
||||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_event(
|
||||
telemetry.report_mixpanel_event(
|
||||
"authenticate with browser",
|
||||
Default::default(),
|
||||
metrics_enabled,
|
||||
@ -1338,30 +1339,8 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_telemetry(&self) {
|
||||
self.telemetry.start();
|
||||
}
|
||||
|
||||
pub fn report_event(
|
||||
&self,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
self.telemetry
|
||||
.report_event(kind, properties.clone(), telemetry_settings);
|
||||
}
|
||||
|
||||
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||
self.telemetry.log_file_path()
|
||||
}
|
||||
|
||||
pub fn metrics_id(&self) -> Option<Arc<str>> {
|
||||
self.telemetry.metrics_id()
|
||||
}
|
||||
|
||||
pub fn is_staff(&self) -> Option<bool> {
|
||||
self.telemetry.is_staff()
|
||||
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
||||
&self.telemetry
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::{ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
@ -29,26 +30,62 @@ pub struct Telemetry {
|
||||
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
queue: Vec<MixpanelEvent>,
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
mixpanel_events_queue: Vec<MixpanelEvent>,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
next_mixpanel_event_id: usize,
|
||||
flush_mixpanel_events_task: Option<Task<()>>,
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
|
||||
lazy_static! {
|
||||
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
|
||||
static ref CLICKHOUSE_EVENTS_URL: String =
|
||||
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
events: Vec<ClickhouseEventWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventWrapper {
|
||||
time: u128,
|
||||
signed_in: bool,
|
||||
#[serde(flatten)]
|
||||
event: ClickhouseEvent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClickhouseEvent {
|
||||
Editor {
|
||||
operation: &'static str,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@ -63,7 +100,8 @@ struct MixpanelEventProperties {
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
token: &'static str,
|
||||
time: u128,
|
||||
distinct_id: Option<Arc<str>>,
|
||||
#[serde(rename = "distinct_id")]
|
||||
installation_id: Option<Arc<str>>,
|
||||
#[serde(rename = "$insert_id")]
|
||||
insert_id: usize,
|
||||
// Custom fields
|
||||
@ -86,7 +124,7 @@ struct MixpanelEngageRequest {
|
||||
#[serde(rename = "$token")]
|
||||
token: &'static str,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
distinct_id: Arc<str>,
|
||||
installation_id: Arc<str>,
|
||||
#[serde(rename = "$set")]
|
||||
set: Value,
|
||||
}
|
||||
@ -119,11 +157,13 @@ impl Telemetry {
|
||||
os_name: platform.os_name().into(),
|
||||
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
|
||||
release_channel,
|
||||
device_id: None,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
queue: Default::default(),
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
mixpanel_events_queue: Default::default(),
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_mixpanel_events_task: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
next_mixpanel_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
@ -154,29 +194,38 @@ impl Telemetry {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let device_id =
|
||||
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
device_id
|
||||
let installation_id =
|
||||
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
installation_id
|
||||
} else {
|
||||
let device_id = Uuid::new_v4().to_string();
|
||||
let installation_id = Uuid::new_v4().to_string();
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp("device_id".to_string(), device_id.clone())
|
||||
.write_kvp("device_id".to_string(), installation_id.clone())
|
||||
.await?;
|
||||
device_id
|
||||
installation_id
|
||||
};
|
||||
|
||||
let device_id: Arc<str> = device_id.into();
|
||||
let installation_id: Arc<str> = installation_id.into();
|
||||
let mut state = this.state.lock();
|
||||
state.device_id = Some(device_id.clone());
|
||||
for event in &mut state.queue {
|
||||
state.installation_id = Some(installation_id.clone());
|
||||
|
||||
for event in &mut state.mixpanel_events_queue {
|
||||
event
|
||||
.properties
|
||||
.distinct_id
|
||||
.get_or_insert_with(|| device_id.clone());
|
||||
.installation_id
|
||||
.get_or_insert_with(|| installation_id.clone());
|
||||
}
|
||||
if !state.queue.is_empty() {
|
||||
drop(state);
|
||||
this.flush();
|
||||
|
||||
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_mixpanel_events {
|
||||
this.flush_mixpanel_events();
|
||||
}
|
||||
|
||||
if has_clickhouse_events {
|
||||
this.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
@ -200,19 +249,19 @@ impl Telemetry {
|
||||
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let device_id = state.device_id.clone();
|
||||
let installation_id = state.installation_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
|
||||
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
|
||||
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
distinct_id: device_id,
|
||||
installation_id,
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
@ -221,7 +270,7 @@ impl Telemetry {
|
||||
}])?;
|
||||
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into())
|
||||
.post_json(MIXPANEL_ENGAGE_URL, json_bytes.into(), false)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@ -231,7 +280,42 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_event(
|
||||
pub fn report_clickhouse_event(
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let signed_in = state.metrics_id.is_some();
|
||||
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
signed_in,
|
||||
event,
|
||||
});
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush_clickhouse_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_mixpanel_event(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
@ -243,15 +327,15 @@ impl Telemetry {
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = MixpanelEvent {
|
||||
event: kind.to_string(),
|
||||
event: kind.into(),
|
||||
properties: MixpanelEventProperties {
|
||||
token: "",
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
distinct_id: state.device_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_event_id),
|
||||
installation_id: state.installation_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_mixpanel_event_id),
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
@ -264,17 +348,17 @@ impl Telemetry {
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
},
|
||||
};
|
||||
state.queue.push(event);
|
||||
if state.device_id.is_some() {
|
||||
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||
state.mixpanel_events_queue.push(event);
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush();
|
||||
self.flush_mixpanel_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_task = Some(self.executor.spawn(async move {
|
||||
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush();
|
||||
this.flush_mixpanel_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -284,14 +368,18 @@ impl Telemetry {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().installation_id.clone()
|
||||
}
|
||||
|
||||
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
fn flush_mixpanel_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.queue);
|
||||
state.flush_task.take();
|
||||
let mut events = mem::take(&mut state.mixpanel_events_queue);
|
||||
state.flush_mixpanel_events_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
|
||||
@ -316,7 +404,7 @@ impl Telemetry {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &events)?;
|
||||
this.http_client
|
||||
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into())
|
||||
.post_json(MIXPANEL_EVENTS_URL, json_bytes.into(), false)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@ -325,4 +413,53 @@ impl Telemetry {
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
state.flush_clickhouse_events_task.take();
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(
|
||||
&mut json_bytes,
|
||||
&ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into(), false)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
ToggleScreenSharing,
|
||||
toggle_screen_sharing, ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
@ -18,6 +17,7 @@ use gpui::{
|
||||
AppContext, Entity, ImageData, ModelHandle, SceneBuilder, Subscription, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::{AvatarStyle, Theme};
|
||||
@ -27,7 +27,6 @@ use workspace::{FollowNextCollaborator, Workspace};
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleCollaboratorList,
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ShareProject,
|
||||
@ -36,7 +35,6 @@ actions!(
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
@ -44,11 +42,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub struct CollabTitlebarItem {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@ -68,7 +67,7 @@ impl View for CollabTitlebarItem {
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let project = self.project.read(cx);
|
||||
let mut project_title = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
@ -93,8 +92,8 @@ impl View for CollabTitlebarItem {
|
||||
.left(),
|
||||
);
|
||||
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
let peer_id = workspace.read(cx).client().peer_id();
|
||||
let user = self.user_store.read(cx).current_user();
|
||||
let peer_id = self.client.peer_id();
|
||||
if let Some(((user, peer_id), room)) = user
|
||||
.zip(peer_id)
|
||||
.zip(ActiveCall::global(cx).read(cx).room().cloned())
|
||||
@ -128,13 +127,16 @@ impl View for CollabTitlebarItem {
|
||||
|
||||
impl CollabTitlebarItem {
|
||||
pub fn new(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: &Workspace,
|
||||
workspace_handle: &ViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let project = workspace.project().clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||
this.window_activation_changed(active, cx)
|
||||
@ -164,30 +166,29 @@ impl CollabTitlebarItem {
|
||||
);
|
||||
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
user_store: user_store.clone(),
|
||||
workspace: workspace.weak_handle(),
|
||||
project,
|
||||
user_store,
|
||||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
collaborator_list_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = if active {
|
||||
Some(workspace.read(cx).project().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
let project = if active {
|
||||
Some(self.project.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -198,71 +199,42 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
|
||||
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.share_project(project, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = self.project.clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.share_project(project, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.unshare_project(project, cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_collaborator_list_popover(
|
||||
&mut self,
|
||||
_: &ToggleCollaboratorList,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match self.collaborator_list_popover.take() {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
|
||||
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
collaborator_list_popover::Event::Dismissed => {
|
||||
this.collaborator_list_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.collaborator_list_popover = Some(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = self.project.clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.unshare_project(project, cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.contacts_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(&workspace, cx));
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
let view = cx.add_view(|cx| {
|
||||
ContactsPopover::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@ -357,8 +329,8 @@ impl CollabTitlebarItem {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_contacts_popover(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu>(
|
||||
0,
|
||||
@ -405,7 +377,7 @@ impl CollabTitlebarItem {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
toggle_screen_sharing(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleScreenSharing>(
|
||||
0,
|
||||
@ -451,11 +423,11 @@ impl CollabTitlebarItem {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if is_shared {
|
||||
cx.dispatch_action(UnshareProject);
|
||||
this.unshare_project(&Default::default(), cx);
|
||||
} else {
|
||||
cx.dispatch_action(ShareProject);
|
||||
this.share_project(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ShareUnshare>(
|
||||
@ -496,8 +468,8 @@ impl CollabTitlebarItem {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleUserMenu);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_user_menu(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleUserMenu>(
|
||||
0,
|
||||
@ -527,8 +499,11 @@ impl CollabTitlebarItem {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(SignIn);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let client = this.client.clone();
|
||||
cx.app_context()
|
||||
.spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
@ -862,7 +837,7 @@ impl CollabTitlebarItem {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(auto_update::Check);
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
),
|
||||
|
@ -1,5 +1,4 @@
|
||||
mod collab_titlebar_item;
|
||||
mod collaborator_list_popover;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
|
@ -1,161 +0,0 @@
|
||||
use call::ActiveCall;
|
||||
use client::UserStore;
|
||||
use gpui::Action;
|
||||
use gpui::{actions, elements::*, platform::MouseButton, Entity, ModelHandle, View, ViewContext};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::collab_titlebar_item::ToggleCollaboratorList;
|
||||
|
||||
pub(crate) enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Collaborator {
|
||||
SelfUser { username: String },
|
||||
RemoteUser { username: String },
|
||||
}
|
||||
|
||||
actions!(collaborator_list_popover, [NoOp]);
|
||||
|
||||
pub(crate) struct CollaboratorListPopover {
|
||||
list_state: ListState<Self>,
|
||||
}
|
||||
|
||||
impl Entity for CollaboratorListPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for CollaboratorListPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"CollaboratorListPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
MouseEventHandler::<Self, Self>::new(0, cx, |_, _| {
|
||||
List::new(self.list_state.clone())
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleCollaboratorList);
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
impl CollaboratorListPopover {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut collaborators = user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|u| Collaborator::SelfUser {
|
||||
username: u.github_login.clone(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
//TODO: What should the canonical sort here look like, consult contacts list implementation
|
||||
if let Some(room) = active_call.read(cx).room() {
|
||||
for participant in room.read(cx).remote_participants() {
|
||||
collaborators.push(Collaborator::RemoteUser {
|
||||
username: participant.1.user.github_login.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
list_state: ListState::new(
|
||||
collaborators.len(),
|
||||
Orientation::Top,
|
||||
0.,
|
||||
move |_, index, cx| match &collaborators[index] {
|
||||
Collaborator::SelfUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
None::<NoOp>,
|
||||
None,
|
||||
Svg::new("icons/chevron_right_12.svg"),
|
||||
NoOp,
|
||||
"Leave call".to_owned(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
Some(NoOp),
|
||||
Some(format!("Follow {username}")),
|
||||
Svg::new("icons/x_mark_12.svg"),
|
||||
NoOp,
|
||||
format!("Remove {username} from call"),
|
||||
cx,
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
|
||||
index: usize,
|
||||
username: &str,
|
||||
username_action: Option<UA>,
|
||||
username_tooltip: Option<String>,
|
||||
icon: Svg,
|
||||
icon_action: IA,
|
||||
icon_tooltip: String,
|
||||
cx: &mut ViewContext<CollaboratorListPopover>,
|
||||
) -> AnyElement<CollaboratorListPopover> {
|
||||
enum Username {}
|
||||
enum UsernameTooltip {}
|
||||
enum Icon {}
|
||||
enum IconTooltip {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let username_theme = theme.contact_list.contact_username.text.clone();
|
||||
let tooltip_theme = theme.tooltip.clone();
|
||||
|
||||
let username =
|
||||
MouseEventHandler::<Username, CollaboratorListPopover>::new(index, cx, |_, _| {
|
||||
Label::new(username.to_owned(), username_theme.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
if let Some(username_action) = username_action.clone() {
|
||||
cx.dispatch_action(username_action);
|
||||
}
|
||||
});
|
||||
|
||||
Flex::row()
|
||||
.with_child(if let Some(username_tooltip) = username_tooltip {
|
||||
username
|
||||
.with_tooltip::<UsernameTooltip>(
|
||||
index,
|
||||
username_tooltip,
|
||||
None,
|
||||
tooltip_theme.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
username.into_any()
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::<Icon, CollaboratorListPopover>::new(index, cx, |_, _| icon)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(icon_action.clone())
|
||||
})
|
||||
.with_tooltip::<IconTooltip>(index, icon_tooltip, None, tooltip_theme, cx),
|
||||
)
|
||||
.into_any()
|
||||
}
|
@ -23,6 +23,7 @@ pub fn build_contact_finder(
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
}
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
use client::{proto::PeerId, Contact, User, UserStore};
|
||||
use editor::{Cancel, Editor};
|
||||
@ -140,6 +139,7 @@ pub struct RespondToContactRequest {
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
ToggleContactFinder,
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
@ -157,7 +157,12 @@ pub struct ContactList {
|
||||
}
|
||||
|
||||
impl ContactList {
|
||||
pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let filter_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(Arc::new(|theme| {
|
||||
@ -262,7 +267,6 @@ impl ContactList {
|
||||
});
|
||||
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx)));
|
||||
@ -275,8 +279,8 @@ impl ContactList {
|
||||
match_candidates: Default::default(),
|
||||
filter_editor,
|
||||
_subscriptions: subscriptions,
|
||||
project: workspace.read(cx).project().clone(),
|
||||
workspace: workspace.downgrade(),
|
||||
project,
|
||||
workspace,
|
||||
user_store,
|
||||
};
|
||||
this.update_entries(cx);
|
||||
@ -1116,11 +1120,14 @@ impl ContactList {
|
||||
)
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_contact(
|
||||
&RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.flex_float(),
|
||||
)
|
||||
@ -1203,11 +1210,14 @@ impl ContactList {
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.respond_to_contact_request(
|
||||
&RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.contained()
|
||||
.with_margin_right(button_spacing),
|
||||
@ -1225,11 +1235,14 @@ impl ContactList {
|
||||
.flex_float()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.respond_to_contact_request(
|
||||
&RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
@ -1246,11 +1259,14 @@ impl ContactList {
|
||||
})
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_contact(
|
||||
&RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.flex_float(),
|
||||
);
|
||||
@ -1318,7 +1334,7 @@ impl View for ContactList {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(contacts_popover::ToggleContactFinder)
|
||||
cx.emit(Event::ToggleContactFinder)
|
||||
})
|
||||
.with_tooltip::<AddContact>(
|
||||
0,
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
contact_finder::{build_contact_finder, ContactFinder},
|
||||
contact_list::ContactList,
|
||||
ToggleContactsMenu,
|
||||
};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
@ -9,6 +8,7 @@ use gpui::{
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use picker::PickerEvent;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::Workspace;
|
||||
|
||||
@ -29,17 +29,26 @@ enum Child {
|
||||
|
||||
pub struct ContactsPopover {
|
||||
child: Child,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
impl ContactsPopover {
|
||||
pub fn new(workspace: &ViewHandle<Workspace>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
child: Child::ContactList(cx.add_view(|cx| ContactList::new(workspace, cx))),
|
||||
user_store: workspace.read(cx).user_store().clone(),
|
||||
workspace: workspace.downgrade(),
|
||||
child: Child::ContactList(cx.add_view(|cx| {
|
||||
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
|
||||
})),
|
||||
project,
|
||||
user_store,
|
||||
workspace,
|
||||
_subscription: None,
|
||||
};
|
||||
this.show_contact_list(String::new(), cx);
|
||||
@ -68,16 +77,24 @@ impl ContactsPopover {
|
||||
}
|
||||
|
||||
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let child = cx
|
||||
.add_view(|cx| ContactList::new(&workspace, cx).with_editor_text(editor_text, cx));
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
}));
|
||||
self.child = Child::ContactList(child);
|
||||
cx.notify();
|
||||
}
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactList::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
crate::contact_list::Event::ToggleContactFinder => {
|
||||
this.toggle_contact_finder(&Default::default(), cx)
|
||||
}
|
||||
}));
|
||||
self.child = Child::ContactList(child);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,9 +123,7 @@ impl View for ContactsPopover {
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
|
@ -78,24 +78,26 @@ impl IncomingCallNotification {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(app_state) = this.app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let app_state = self.app_state.clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, _| {
|
||||
active_call.decline_incoming().log_err();
|
||||
|
@ -58,14 +58,14 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
room::Event::RemoteProjectUnshared { project_id } => {
|
||||
if let Some(window_ids) = notification_windows.remove(&project_id) {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
room::Event::Left => {
|
||||
for (_, window_ids) in notification_windows.drain() {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::toggle_screen_sharing;
|
||||
use call::ActiveCall;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
@ -7,8 +8,6 @@ use gpui::{
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::ToggleScreenSharing;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
@ -20,10 +19,10 @@ pub fn init(cx: &mut AppContext) {
|
||||
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
|
||||
}
|
||||
} else if let Some((window_id, _)) = status_indicator.take() {
|
||||
cx.remove_status_bar_item(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
} else if let Some((window_id, _)) = status_indicator.take() {
|
||||
cx.remove_status_bar_item(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@ -54,7 +53,7 @@ impl View for SharingStatusIndicator {
|
||||
.aligned()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
toggle_screen_sharing(&Default::default(), cx)
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
@ -167,9 +167,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let focused_view_id = self.focused_view_id;
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let action = self.actions.remove(action_ix).action;
|
||||
cx.defer(move |_, cx| {
|
||||
cx.dispatch_any_action_at(window_id, focused_view_id, action);
|
||||
});
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
cx.dispatch_action(window_id, focused_view_id, action.as_ref())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
@ -266,9 +268,11 @@ impl std::fmt::Debug for Command {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
@ -289,7 +293,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
deterministic.forbid_parking();
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
@ -331,7 +336,7 @@ mod tests {
|
||||
assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
|
||||
palette.confirm(&Default::default(), cx);
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "ab");
|
||||
});
|
||||
|
@ -227,11 +227,13 @@ impl ContextMenu {
|
||||
match action {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
let window_id = cx.window_id();
|
||||
cx.dispatch_any_action_at(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
);
|
||||
let view_id = self.parent_view_id;
|
||||
let action = action.boxed_clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
cx.dispatch_action(window_id, view_id, action.as_ref())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ContextMenuItemAction::Handler(handler) => handler(cx),
|
||||
}
|
||||
@ -459,11 +461,16 @@ impl ContextMenu {
|
||||
let window_id = cx.window_id();
|
||||
match &action {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
cx.dispatch_any_action_at(
|
||||
window_id,
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
);
|
||||
let action = action.boxed_clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
cx.dispatch_action(
|
||||
window_id,
|
||||
view_id,
|
||||
action.as_ref(),
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ContextMenuItemAction::Handler(handler) => handler(cx),
|
||||
}
|
||||
@ -485,7 +492,11 @@ impl ContextMenu {
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_down_out(MouseButton::Left, |_, _, cx| cx.dispatch_action(Cancel))
|
||||
.on_down_out(MouseButton::Right, |_, _, cx| cx.dispatch_action(Cancel))
|
||||
.on_down_out(MouseButton::Left, |_, this, cx| {
|
||||
this.cancel(&Default::default(), cx);
|
||||
})
|
||||
.on_down_out(MouseButton::Right, |_, this, cx| {
|
||||
this.cancel(&Default::default(), cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -560,7 +560,7 @@ impl Copilot {
|
||||
}
|
||||
}
|
||||
|
||||
fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
|
||||
pub fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
|
||||
let start_task = cx
|
||||
.spawn({
|
||||
let http = self.http.clone();
|
||||
@ -932,7 +932,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
|
||||
///Check for the latest copilot language server and download it if we haven't already
|
||||
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
let release = latest_github_release("zed-industries/copilot", http.clone()).await?;
|
||||
let release = latest_github_release("zed-industries/copilot", false, http.clone()).await?;
|
||||
|
||||
let version_dir = &*paths::COPILOT_DIR.join(format!("copilot-{}", release.name));
|
||||
|
||||
|
@ -27,14 +27,13 @@ pub fn init(cx: &mut AppContext) {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(code_verification_handle) = code_verification.as_mut() {
|
||||
let window_id = code_verification_handle.window_id();
|
||||
if cx.has_window(window_id) {
|
||||
cx.update_window(window_id, |cx| {
|
||||
code_verification_handle.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status, cx)
|
||||
});
|
||||
cx.activate_window();
|
||||
let updated = cx.update_window(window_id, |cx| {
|
||||
code_verification_handle.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status.clone(), cx)
|
||||
});
|
||||
} else {
|
||||
cx.activate_window();
|
||||
});
|
||||
if updated.is_none() {
|
||||
code_verification = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
@ -56,7 +55,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = code_verification.take() {
|
||||
cx.remove_window(code_verification.window_id());
|
||||
cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -196,7 +195,7 @@ impl CopilotCodeVerification {
|
||||
.contained()
|
||||
.with_style(style.auth.prompting.hint.container.clone()),
|
||||
)
|
||||
.with_child(theme::ui::cta_button_with_click::<ConnectButton, _, _, _>(
|
||||
.with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
@ -250,7 +249,7 @@ impl CopilotCodeVerification {
|
||||
.contained()
|
||||
.with_style(enabled_style.hint.container),
|
||||
)
|
||||
.with_child(theme::ui::cta_button_with_click::<DoneButton, _, _, _>(
|
||||
.with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
"Done",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
@ -304,7 +303,7 @@ impl CopilotCodeVerification {
|
||||
.contained()
|
||||
.with_style(unauthorized_style.warning.container),
|
||||
)
|
||||
.with_child(theme::ui::cta_button_with_click::<Self, _, _, _>(
|
||||
.with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
"Subscribe on GitHub",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use copilot::{Copilot, Reinstall, SignOut, Status};
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
@ -13,7 +13,7 @@ use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::{
|
||||
create_and_open_local_file, item::ItemHandle,
|
||||
notifications::simple_message_notification::OsOpen, AppState, StatusItemView, Toast, Workspace,
|
||||
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
|
||||
};
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
@ -21,7 +21,6 @@ const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||
|
||||
pub struct CopilotButton {
|
||||
app_state: Arc<AppState>,
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
@ -106,11 +105,21 @@ impl View for CopilotButton {
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new_action(
|
||||
Toast::new(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
Reinstall,
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.reinstall(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
@ -134,7 +143,7 @@ impl View for CopilotButton {
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let menu = cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
@ -149,7 +158,6 @@ impl CopilotButton {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
app_state,
|
||||
popup_menu: menu,
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
@ -197,7 +205,6 @@ impl CopilotButton {
|
||||
|
||||
if let Some(path) = self.path.as_ref() {
|
||||
let path_enabled = settings.copilot_enabled_for_path(path);
|
||||
let app_state = Arc::downgrade(&self.app_state);
|
||||
let path = path.clone();
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
@ -205,17 +212,11 @@ impl CopilotButton {
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
move |cx| {
|
||||
if let Some((workspace, app_state)) = cx
|
||||
.root_view()
|
||||
.clone()
|
||||
.downcast::<Workspace>()
|
||||
.zip(app_state.upgrade())
|
||||
{
|
||||
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|_, cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
app_state,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
@ -302,13 +303,12 @@ impl StatusItemView for CopilotButton {
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
app_state: Arc<AppState>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, app_state, cx, || {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
Settings::initial_user_settings_content(&assets::Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
|
@ -677,7 +677,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
|
||||
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
|
||||
Arc::new(move |cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = &settings.theme.editor;
|
||||
@ -697,8 +697,18 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
icon.constrained()
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained(),
|
||||
.contained()
|
||||
.with_margin_right(cx.gutter_padding),
|
||||
)
|
||||
.with_children(diagnostic.source.as_ref().map(|source| {
|
||||
Label::new(
|
||||
format!("{source}: "),
|
||||
style.source.label.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.clone(),
|
||||
@ -707,7 +717,6 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
.with_highlights(highlights.clone())
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.with_margin_left(cx.gutter_padding)
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(diagnostic.code.clone().map(|code| {
|
||||
|
@ -3,18 +3,19 @@ use editor::{Editor, GoToDiagnostic};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json, AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use lsp::LanguageServerId;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
@ -25,7 +26,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
impl DiagnosticIndicator {
|
||||
pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
@ -46,6 +48,7 @@ impl DiagnosticIndicator {
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
@ -163,8 +166,12 @@ impl View for DiagnosticIndicator {
|
||||
})
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(crate::Deploy)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Summary>(
|
||||
0,
|
||||
@ -200,8 +207,8 @@ impl View for DiagnosticIndicator {
|
||||
.with_margin_left(item_spacing)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.go_to_next_diagnostic(&Default::default(), cx)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
copilot = { path = "../copilot" }
|
||||
db = { path = "../db" }
|
||||
|
@ -22,6 +22,7 @@ pub mod test;
|
||||
use aho_corasick::AhoCorasick;
|
||||
use anyhow::{anyhow, Result};
|
||||
use blink_manager::BlinkManager;
|
||||
use client::ClickhouseEvent;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
||||
use copilot::Copilot;
|
||||
@ -51,8 +52,8 @@ use itertools::Itertools;
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||
Diagnostic, DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16,
|
||||
Point, Selection, SelectionGoal, TransactionId,
|
||||
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
|
||||
};
|
||||
use link_go_to_definition::{
|
||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||
@ -808,10 +809,13 @@ impl CompletionsMenu {
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
});
|
||||
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||
this.confirm_completion(
|
||||
&ConfirmCompletion {
|
||||
item_ix: Some(item_ix),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any(),
|
||||
);
|
||||
@ -969,9 +973,23 @@ impl CodeActionsMenu {
|
||||
.with_style(item_style)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ConfirmCodeAction {
|
||||
item_ix: Some(item_ix),
|
||||
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||
let workspace = this
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx));
|
||||
cx.window_context().defer(move |cx| {
|
||||
if let Some(workspace) = workspace {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(task) = Editor::confirm_code_action(
|
||||
workspace,
|
||||
&Default::default(),
|
||||
cx,
|
||||
) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.into_any(),
|
||||
@ -1295,7 +1313,7 @@ impl Editor {
|
||||
cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
|
||||
}
|
||||
|
||||
this.report_event("open editor", cx);
|
||||
this.report_editor_event("open", cx);
|
||||
this
|
||||
}
|
||||
|
||||
@ -1330,6 +1348,10 @@ impl Editor {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
fn workspace(&self, cx: &AppContext) -> Option<ViewHandle<Workspace>> {
|
||||
self.workspace.as_ref()?.0.upgrade(cx)
|
||||
}
|
||||
|
||||
pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
|
||||
self.buffer().read(cx).title(cx)
|
||||
}
|
||||
@ -1356,6 +1378,10 @@ impl Editor {
|
||||
self.buffer.read(cx).language_at(point, cx)
|
||||
}
|
||||
|
||||
pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
|
||||
self.buffer.read(cx).read(cx).file_at(point).cloned()
|
||||
}
|
||||
|
||||
pub fn active_excerpt(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
@ -3148,10 +3174,13 @@ impl Editor {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
.on_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
});
|
||||
.on_down(MouseButton::Left, |_, this, cx| {
|
||||
this.toggle_code_actions(
|
||||
&ToggleCodeActions {
|
||||
deployed_from_indicator: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
@ -3209,11 +3238,13 @@ impl Editor {
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(3.))
|
||||
.on_click(MouseButton::Left, {
|
||||
move |_, _, cx| {
|
||||
cx.dispatch_any_action(match fold_status {
|
||||
FoldStatus::Folded => Box::new(UnfoldAt { buffer_row }),
|
||||
FoldStatus::Foldable => Box::new(FoldAt { buffer_row }),
|
||||
});
|
||||
move |_, editor, cx| match fold_status {
|
||||
FoldStatus::Folded => {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx);
|
||||
}
|
||||
FoldStatus::Foldable => {
|
||||
editor.fold_at(&FoldAt { buffer_row }, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
@ -5572,93 +5603,77 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_definition(
|
||||
workspace: &mut Workspace,
|
||||
_: &GoToDefinition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Self::go_to_definition_of_kind(GotoDefinitionKind::Symbol, workspace, cx);
|
||||
pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_type_definition(
|
||||
workspace: &mut Workspace,
|
||||
_: &GoToTypeDefinition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Self::go_to_definition_of_kind(GotoDefinitionKind::Type, workspace, cx);
|
||||
pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
|
||||
self.go_to_definition_of_kind(GotoDefinitionKind::Type, cx);
|
||||
}
|
||||
|
||||
fn go_to_definition_of_kind(
|
||||
kind: GotoDefinitionKind,
|
||||
workspace: &mut Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let active_item = workspace.active_item(cx);
|
||||
let editor_handle = if let Some(editor) = active_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.act_as::<Self>(cx))
|
||||
{
|
||||
editor
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let editor = editor_handle.read(cx);
|
||||
let buffer = editor.buffer.read(cx);
|
||||
let head = editor.selections.newest::<usize>(cx).head();
|
||||
fn go_to_definition_of_kind(&mut self, kind: GotoDefinitionKind, cx: &mut ViewContext<Self>) {
|
||||
let Some(workspace) = self.workspace(cx) else { return };
|
||||
let buffer = self.buffer.read(cx);
|
||||
let head = self.selections.newest::<usize>(cx).head();
|
||||
let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
|
||||
text_anchor
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project().clone();
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let definitions = project.update(cx, |project, cx| match kind {
|
||||
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
|
||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||
});
|
||||
|
||||
cx.spawn_labeled("Fetching Definition...", |workspace, mut cx| async move {
|
||||
cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
|
||||
let definitions = definitions.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
Editor::navigate_to_definitions(workspace, editor_handle, definitions, cx);
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.navigate_to_definitions(definitions, cx);
|
||||
})?;
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn navigate_to_definitions(
|
||||
workspace: &mut Workspace,
|
||||
editor_handle: ViewHandle<Editor>,
|
||||
definitions: Vec<LocationLink>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
&mut self,
|
||||
mut definitions: Vec<LocationLink>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let pane = workspace.active_pane().clone();
|
||||
let Some(workspace) = self.workspace(cx) else { return };
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
// If there is one definition, just open it directly
|
||||
if let [definition] = definitions.as_slice() {
|
||||
if definitions.len() == 1 {
|
||||
let definition = definitions.pop().unwrap();
|
||||
let range = definition
|
||||
.target
|
||||
.range
|
||||
.to_offset(definition.target.buffer.read(cx));
|
||||
|
||||
let target_editor_handle =
|
||||
workspace.open_project_item(definition.target.buffer.clone(), cx);
|
||||
target_editor_handle.update(cx, |target_editor, cx| {
|
||||
// When selecting a definition in a different buffer, disable the nav history
|
||||
// to avoid creating a history entry at the previous cursor location.
|
||||
if editor_handle != target_editor_handle {
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
}
|
||||
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
|
||||
pane.update(cx, |pane, _| pane.enable_history());
|
||||
});
|
||||
} else {
|
||||
cx.window_context().defer(move |cx| {
|
||||
let target_editor: ViewHandle<Self> = workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_project_item(definition.target.buffer.clone(), cx)
|
||||
});
|
||||
target_editor.update(cx, |target_editor, cx| {
|
||||
// When selecting a definition in a different buffer, disable the nav history
|
||||
// to avoid creating a history entry at the previous cursor location.
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
pane.update(cx, |pane, _| pane.enable_history());
|
||||
});
|
||||
});
|
||||
}
|
||||
} else if !definitions.is_empty() {
|
||||
let replica_id = editor_handle.read(cx).replica_id(cx);
|
||||
let replica_id = self.replica_id(cx);
|
||||
let title = definitions
|
||||
.iter()
|
||||
.find(|definition| definition.origin.is_some())
|
||||
@ -5678,7 +5693,9 @@ impl Editor {
|
||||
.into_iter()
|
||||
.map(|definition| definition.target)
|
||||
.collect();
|
||||
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Self::open_locations_in_multibuffer(workspace, locations, replica_id, title, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -6834,7 +6851,7 @@ impl Editor {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn report_event(&self, name: &str, cx: &AppContext) {
|
||||
fn report_editor_event(&self, name: &'static str, cx: &AppContext) {
|
||||
if let Some((project, file)) = self.project.as_ref().zip(
|
||||
self.buffer
|
||||
.read(cx)
|
||||
@ -6846,11 +6863,31 @@ impl Editor {
|
||||
let extension = Path::new(file.file_name(cx))
|
||||
.extension()
|
||||
.and_then(|e| e.to_str());
|
||||
project.read(cx).client().report_event(
|
||||
name,
|
||||
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode }),
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
telemetry.report_mixpanel_event(
|
||||
match name {
|
||||
"open" => "open editor",
|
||||
"save" => "save editor",
|
||||
_ => name,
|
||||
},
|
||||
json!({ "File Extension": extension, "Vim Mode": settings.vim_mode, "In Clickhouse": true }),
|
||||
settings.telemetry(),
|
||||
);
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension: extension.map(ToString::to_string),
|
||||
vim_mode: settings.vim_mode,
|
||||
operation: name,
|
||||
copilot_enabled: settings.features.copilot,
|
||||
copilot_enabled_for_language: settings.show_copilot_suggestions(
|
||||
self.language_at(0, cx)
|
||||
.map(|language| language.name())
|
||||
.as_deref(),
|
||||
self.file_at(0, cx)
|
||||
.map(|file| file.path().clone())
|
||||
.as_deref(),
|
||||
),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, settings.telemetry())
|
||||
}
|
||||
}
|
||||
|
||||
@ -7494,8 +7531,16 @@ impl Deref for EditorStyle {
|
||||
|
||||
pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
|
||||
let mut highlighted_lines = Vec::new();
|
||||
for line in diagnostic.message.lines() {
|
||||
highlighted_lines.push(highlight_diagnostic_message(line));
|
||||
for (index, line) in diagnostic.message.lines().enumerate() {
|
||||
let line = match &diagnostic.source {
|
||||
Some(source) if index == 0 => {
|
||||
let source_highlight = Vec::from_iter(0..source.len());
|
||||
highlight_diagnostic_message(source_highlight, &format!("{source}: {line}"))
|
||||
}
|
||||
|
||||
_ => highlight_diagnostic_message(Vec::new(), line),
|
||||
};
|
||||
highlighted_lines.push(line);
|
||||
}
|
||||
|
||||
Arc::new(move |cx: &mut BlockContext| {
|
||||
@ -7519,11 +7564,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
})
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(message: &str) -> (String, Vec<usize>) {
|
||||
pub fn highlight_diagnostic_message(
|
||||
inital_highlights: Vec<usize>,
|
||||
message: &str,
|
||||
) -> (String, Vec<usize>) {
|
||||
let mut message_without_backticks = String::new();
|
||||
let mut prev_offset = 0;
|
||||
let mut inside_block = false;
|
||||
let mut highlights = Vec::new();
|
||||
let mut highlights = inital_highlights;
|
||||
for (match_ix, (offset, _)) in message
|
||||
.match_indices('`')
|
||||
.chain([(message.len(), "")])
|
||||
|
@ -211,10 +211,13 @@ impl EditorElement {
|
||||
enum GutterHandlers {}
|
||||
scene.push_mouse_region(
|
||||
MouseRegion::new::<GutterHandlers>(cx.view_id(), cx.view_id() + 1, gutter_bounds)
|
||||
.on_hover(|hover, _: &mut Editor, cx| {
|
||||
cx.dispatch_action(GutterHover {
|
||||
hovered: hover.started,
|
||||
})
|
||||
.on_hover(|hover, editor: &mut Editor, cx| {
|
||||
editor.gutter_hover(
|
||||
&GutterHover {
|
||||
hovered: hover.started,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
}
|
||||
@ -309,25 +312,17 @@ impl EditorElement {
|
||||
editor.select(SelectPhase::End, cx);
|
||||
}
|
||||
|
||||
if let Some(workspace) = editor
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|(workspace, _)| workspace.upgrade(cx))
|
||||
{
|
||||
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
|
||||
if point == target_point {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if shift {
|
||||
go_to_fetched_type_definition(workspace, point, cx);
|
||||
} else {
|
||||
go_to_fetched_definition(workspace, point, cx);
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
if point == target_point {
|
||||
if shift {
|
||||
go_to_fetched_type_definition(editor, point, cx);
|
||||
} else {
|
||||
go_to_fetched_definition(editor, point, cx);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -762,8 +757,8 @@ impl EditorElement {
|
||||
|
||||
scene.push_mouse_region(
|
||||
MouseRegion::new::<FoldMarkers>(cx.view_id(), *id as usize, bound)
|
||||
.on_click(MouseButton::Left, move |_, _: &mut Editor, cx| {
|
||||
cx.dispatch_action(UnfoldAt { buffer_row })
|
||||
.on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
|
||||
editor.unfold_at(&UnfoldAt { buffer_row }, cx)
|
||||
})
|
||||
.with_notify_on_hover(true)
|
||||
.with_notify_on_click(true),
|
||||
|
@ -1,3 +1,7 @@
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||
EditorStyle, RangeToAnchorExt,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
@ -12,11 +16,6 @@ use settings::Settings;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
|
||||
};
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
@ -668,8 +667,8 @@ impl DiagnosticPopover {
|
||||
..Default::default()
|
||||
})
|
||||
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.go_to_diagnostic(&Default::default(), cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<PrimaryDiagnostic>(
|
||||
|
@ -636,7 +636,7 @@ impl Item for Editor {
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
self.report_editor_event("save", cx);
|
||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
|
@ -1,15 +1,11 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
|
||||
use gpui::{Task, ViewContext};
|
||||
use language::{Bias, ToOffset};
|
||||
use project::LocationLink;
|
||||
use settings::Settings;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, SelectPhase,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinkGoToDefinitionState {
|
||||
@ -250,70 +246,51 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
}
|
||||
|
||||
pub fn go_to_fetched_definition(
|
||||
workspace: &mut Workspace,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_fetched_type_definition(
|
||||
workspace: &mut Workspace,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
|
||||
}
|
||||
|
||||
fn go_to_fetched_definition_of_kind(
|
||||
kind: LinkDefinitionKind,
|
||||
workspace: &mut Workspace,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let active_item = workspace.active_item(cx);
|
||||
let editor_handle = if let Some(editor) = active_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
{
|
||||
editor
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
|
||||
let definitions = editor.link_go_to_definition_state.definitions.clone();
|
||||
hide_link_definition(editor, cx);
|
||||
(definitions, editor.link_go_to_definition_state.kind)
|
||||
});
|
||||
let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
|
||||
hide_link_definition(editor, cx);
|
||||
let cached_definitions_kind = editor.link_go_to_definition_state.kind;
|
||||
|
||||
let is_correct_kind = cached_definitions_kind == Some(kind);
|
||||
if !cached_definitions.is_empty() && is_correct_kind {
|
||||
editor_handle.update(cx, |editor, cx| {
|
||||
if !editor.focused {
|
||||
cx.focus_self();
|
||||
}
|
||||
});
|
||||
if !editor.focused {
|
||||
cx.focus_self();
|
||||
}
|
||||
|
||||
Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
|
||||
editor.navigate_to_definitions(cached_definitions, cx);
|
||||
} else {
|
||||
editor_handle.update(cx, |editor, cx| {
|
||||
editor.select(
|
||||
SelectPhase::Begin {
|
||||
position: point,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor.select(
|
||||
SelectPhase::Begin {
|
||||
position: point,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
match kind {
|
||||
LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
|
||||
|
||||
LinkDefinitionKind::Type => {
|
||||
Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
|
||||
}
|
||||
LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
|
||||
LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -426,8 +403,8 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_type_definition(workspace, hover_point, cx);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_type_definition(editor, hover_point, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@ -635,8 +612,8 @@ mod tests {
|
||||
"});
|
||||
|
||||
// Cmd click with existing definition doesn't re-request and dismisses highlight
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, hover_point, cx);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_definition(editor, hover_point, cx);
|
||||
});
|
||||
// Assert selection moved to to definition
|
||||
cx.lsp
|
||||
@ -676,8 +653,8 @@ mod tests {
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, hover_point, cx);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_definition(editor, hover_point, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
|
@ -11,25 +11,27 @@ path = "src/feedback.rs"
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client = { path = "../client" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
log.workspace = true
|
||||
futures.workspace = true
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
isahc = "1.7"
|
||||
lazy_static.workspace = true
|
||||
postage.workspace = true
|
||||
project = { path = "../project" }
|
||||
search = { path = "../search" }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
settings = { path = "../settings" }
|
||||
sysinfo = "0.27.1"
|
||||
theme = { path = "../theme" }
|
||||
tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" }
|
||||
urlencoding = "2.1.2"
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
@ -1,15 +1,16 @@
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Entity, View, ViewContext,
|
||||
Entity, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
use crate::feedback_editor::{FeedbackEditor, GiveFeedback};
|
||||
|
||||
pub struct DeployFeedbackButton {
|
||||
active: bool,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl Entity for DeployFeedbackButton {
|
||||
@ -17,8 +18,11 @@ impl Entity for DeployFeedbackButton {
|
||||
}
|
||||
|
||||
impl DeployFeedbackButton {
|
||||
pub fn new() -> Self {
|
||||
DeployFeedbackButton { active: false }
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
DeployFeedbackButton {
|
||||
active: false,
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,9 +56,12 @@ impl View for DeployFeedbackButton {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
cx.dispatch_action(GiveFeedback)
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| FeedbackEditor::deploy(workspace, cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
|
@ -3,20 +3,10 @@ pub mod feedback_editor;
|
||||
pub mod feedback_info_text;
|
||||
pub mod submit_feedback_button;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
mod system_specs;
|
||||
use gpui::{actions, impl_actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
|
||||
use serde::Deserialize;
|
||||
use gpui::{actions, platform::PromptLevel, AppContext, ClipboardItem, ViewContext};
|
||||
use system_specs::SystemSpecs;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenBrowser {
|
||||
pub url: Arc<str>,
|
||||
}
|
||||
|
||||
impl_actions!(zed, [OpenBrowser]);
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
@ -28,29 +18,20 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
let system_specs = SystemSpecs::new(&cx);
|
||||
let system_specs_text = system_specs.to_string();
|
||||
|
||||
feedback_editor::init(system_specs, app_state, cx);
|
||||
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
|
||||
let url = format!(
|
||||
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||
urlencoding::encode(&system_specs_text)
|
||||
);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
feedback_editor::init(cx);
|
||||
|
||||
cx.add_action(
|
||||
move |_: &mut Workspace,
|
||||
_: &CopySystemSpecsIntoClipboard,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
let specs = SystemSpecs::new(&cx).to_string();
|
||||
cx.prompt(
|
||||
PromptLevel::Info,
|
||||
&format!("Copied into clipboard:\n\n{system_specs_text}"),
|
||||
&format!("Copied into clipboard:\n\n{specs}"),
|
||||
&["OK"],
|
||||
);
|
||||
let item = ClipboardItem::new(system_specs_text.clone());
|
||||
let item = ClipboardItem::new(specs.clone());
|
||||
cx.write_to_clipboard(item);
|
||||
},
|
||||
);
|
||||
@ -58,24 +39,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
|
||||
let url = "https://github.com/zed-industries/community/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
|
||||
cx.dispatch_action(OpenBrowser {
|
||||
url: url.into(),
|
||||
});
|
||||
cx.platform().open_url(url);
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(
|
||||
move |_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
|
||||
cx.dispatch_action(OpenBrowser {
|
||||
url: url.clone().into(),
|
||||
});
|
||||
let url = format!(
|
||||
"https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||
urlencoding::encode(&SystemSpecs::new(&cx).to_string())
|
||||
);
|
||||
cx.platform().open_url(&url);
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(
|
||||
|_: &mut Workspace, _: &OpenZedCommunityRepo, cx: &mut ViewContext<Workspace>| {
|
||||
let url = "https://github.com/zed-industries/community";
|
||||
cx.dispatch_action(OpenBrowser { url: url.into() });
|
||||
},
|
||||
);
|
||||
cx.add_global_action(open_zed_community_repo);
|
||||
}
|
||||
|
||||
pub fn open_zed_community_repo(_: &OpenZedCommunityRepo, cx: &mut AppContext) {
|
||||
let url = "https://github.com/zed-industries/community";
|
||||
cx.platform().open_url(&url);
|
||||
}
|
||||
|
@ -1,10 +1,4 @@
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::system_specs::SystemSpecs;
|
||||
use anyhow::bail;
|
||||
use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use editor::{Anchor, Editor};
|
||||
@ -19,46 +13,41 @@ use gpui::{
|
||||
use isahc::Request;
|
||||
use language::Buffer;
|
||||
use postage::prelude::Stream;
|
||||
|
||||
use project::Project;
|
||||
use serde::Serialize;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
item::{Item, ItemEvent, ItemHandle},
|
||||
searchable::{SearchableItem, SearchableItemHandle},
|
||||
AppState, Workspace,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use crate::{submit_feedback_button::SubmitFeedbackButton, system_specs::SystemSpecs};
|
||||
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<usize> = 10..=5000;
|
||||
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
|
||||
"Feedback failed to submit, see error log for details.";
|
||||
|
||||
actions!(feedback, [GiveFeedback, SubmitFeedback]);
|
||||
|
||||
pub fn init(system_specs: SystemSpecs, app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action({
|
||||
move |workspace: &mut Workspace, _: &GiveFeedback, cx: &mut ViewContext<Workspace>| {
|
||||
FeedbackEditor::deploy(system_specs.clone(), workspace, app_state.clone(), cx);
|
||||
FeedbackEditor::deploy(workspace, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_async_action(
|
||||
|submit_feedback_button: &mut SubmitFeedbackButton, _: &SubmitFeedback, cx| {
|
||||
if let Some(active_item) = submit_feedback_button.active_item.as_ref() {
|
||||
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.handle_save(cx)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FeedbackRequestBody<'a> {
|
||||
feedback_text: &'a str,
|
||||
metrics_id: Option<Arc<str>>,
|
||||
installation_id: Option<Arc<str>>,
|
||||
system_specs: SystemSpecs,
|
||||
is_staff: bool,
|
||||
token: &'a str,
|
||||
@ -94,7 +83,7 @@ impl FeedbackEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_save(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
pub fn submit(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
let feedback_text = self.editor.read(cx).text(cx);
|
||||
let feedback_char_count = feedback_text.chars().count();
|
||||
let feedback_text = feedback_text.trim().to_string();
|
||||
@ -133,10 +122,8 @@ impl FeedbackEditor {
|
||||
if answer == Some(0) {
|
||||
match FeedbackEditor::submit_feedback(&feedback_text, client, specs).await {
|
||||
Ok(_) => {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.dispatch_action(workspace::CloseActiveItem);
|
||||
})
|
||||
.log_err();
|
||||
this.update(&mut cx, |_, cx| cx.emit(editor::Event::Closed))
|
||||
.log_err();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("{}", error);
|
||||
@ -164,13 +151,16 @@ impl FeedbackEditor {
|
||||
) -> anyhow::Result<()> {
|
||||
let feedback_endpoint = format!("{}/api/feedback", *ZED_SERVER_URL);
|
||||
|
||||
let metrics_id = zed_client.metrics_id();
|
||||
let is_staff = zed_client.is_staff();
|
||||
let telemetry = zed_client.telemetry();
|
||||
let metrics_id = telemetry.metrics_id();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let http_client = zed_client.http_client();
|
||||
|
||||
let request = FeedbackRequestBody {
|
||||
feedback_text: &feedback_text,
|
||||
metrics_id,
|
||||
installation_id,
|
||||
system_specs,
|
||||
is_staff: is_staff.unwrap_or(false),
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
@ -197,22 +187,21 @@ impl FeedbackEditor {
|
||||
}
|
||||
|
||||
impl FeedbackEditor {
|
||||
pub fn deploy(
|
||||
system_specs: SystemSpecs,
|
||||
_: &mut Workspace,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let markdown = app_state.languages.language_for_name("Markdown");
|
||||
pub fn deploy(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer("", markdown, cx))
|
||||
.expect("creating buffers on a local workspace always succeeds");
|
||||
let system_specs = SystemSpecs::new(cx);
|
||||
let feedback_editor = cx
|
||||
.add_view(|cx| FeedbackEditor::new(system_specs, project, buffer, cx));
|
||||
workspace.add_item(Box::new(feedback_editor), cx);
|
||||
@ -290,7 +279,7 @@ impl Item for FeedbackEditor {
|
||||
_: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.handle_save(cx)
|
||||
self.submit(cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
@ -299,7 +288,7 @@ impl Item for FeedbackEditor {
|
||||
_: std::path::PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.handle_save(cx)
|
||||
self.submit(cx)
|
||||
}
|
||||
|
||||
fn reload(
|
||||
@ -352,6 +341,10 @@ impl Item for FeedbackEditor {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for FeedbackEditor {
|
||||
|
@ -6,7 +6,7 @@ use gpui::{
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
use crate::{feedback_editor::FeedbackEditor, OpenZedCommunityRepo};
|
||||
use crate::{feedback_editor::FeedbackEditor, open_zed_community_repo, OpenZedCommunityRepo};
|
||||
|
||||
pub struct FeedbackInfoText {
|
||||
active_item: Option<ViewHandle<FeedbackEditor>>,
|
||||
@ -57,7 +57,7 @@ impl View for FeedbackInfoText {
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(OpenZedCommunityRepo)
|
||||
open_zed_community_repo(&Default::default(), cx)
|
||||
}),
|
||||
)
|
||||
.with_child(
|
||||
|
@ -1,12 +1,16 @@
|
||||
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
elements::{Label, MouseEventHandler},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, Element, Entity, View, ViewContext, ViewHandle,
|
||||
AnyElement, AppContext, Element, Entity, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
use crate::feedback_editor::{FeedbackEditor, SubmitFeedback};
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_async_action(SubmitFeedbackButton::submit);
|
||||
}
|
||||
|
||||
pub struct SubmitFeedbackButton {
|
||||
pub(crate) active_item: Option<ViewHandle<FeedbackEditor>>,
|
||||
@ -18,6 +22,18 @@ impl SubmitFeedbackButton {
|
||||
active_item: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit(
|
||||
&mut self,
|
||||
_: &SubmitFeedback,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
if let Some(active_item) = self.active_item.as_ref() {
|
||||
Some(active_item.update(cx, |feedback_editor, cx| feedback_editor.submit(cx)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for SubmitFeedbackButton {
|
||||
@ -39,8 +55,8 @@ impl View for SubmitFeedbackButton {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(SubmitFeedback)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.submit(&Default::default(), cx);
|
||||
})
|
||||
.aligned()
|
||||
.contained()
|
||||
|
@ -43,6 +43,7 @@ use window_input_handler::WindowInputHandler;
|
||||
use crate::{
|
||||
elements::{AnyElement, AnyRootElement, RootElement},
|
||||
executor::{self, Task},
|
||||
json,
|
||||
keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult},
|
||||
platform::{
|
||||
self, FontSystem, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton,
|
||||
@ -301,6 +302,14 @@ impl AsyncAppContext {
|
||||
self.0.borrow_mut().update(callback)
|
||||
}
|
||||
|
||||
pub fn read_window<T, F: FnOnce(&WindowContext) -> T>(
|
||||
&self,
|
||||
window_id: usize,
|
||||
callback: F,
|
||||
) -> Option<T> {
|
||||
self.0.borrow_mut().read_window(window_id, callback)
|
||||
}
|
||||
|
||||
pub fn update_window<T, F: FnOnce(&mut WindowContext) -> T>(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
@ -309,6 +318,44 @@ impl AsyncAppContext {
|
||||
self.0.borrow_mut().update_window(window_id, callback)
|
||||
}
|
||||
|
||||
pub fn debug_elements(&self, window_id: usize) -> Option<json::Value> {
|
||||
self.0.borrow().read_window(window_id, |cx| {
|
||||
let root_view = cx.window.root_view();
|
||||
let root_element = cx.window.rendered_views.get(&root_view.id())?;
|
||||
root_element.debug(cx).log_err()
|
||||
})?
|
||||
}
|
||||
|
||||
pub fn dispatch_action(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
action: &dyn Action,
|
||||
) -> Result<()> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.update_window(window_id, |window| {
|
||||
window.handle_dispatch_action_from_effect(Some(view_id), action);
|
||||
})
|
||||
.ok_or_else(|| anyhow!("window not found"))
|
||||
}
|
||||
|
||||
pub fn has_window(&self, window_id: usize) -> bool {
|
||||
self.read(|cx| cx.windows.contains_key(&window_id))
|
||||
}
|
||||
|
||||
pub fn window_is_active(&self, window_id: usize) -> bool {
|
||||
self.read(|cx| cx.windows.get(&window_id).map_or(false, |w| w.is_active))
|
||||
}
|
||||
|
||||
pub fn root_view(&self, window_id: usize) -> Option<AnyViewHandle> {
|
||||
self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone()))
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> Vec<usize> {
|
||||
self.read(|cx| cx.windows.keys().copied().collect())
|
||||
}
|
||||
|
||||
pub fn add_model<T, F>(&mut self, build_model: F) -> ModelHandle<T>
|
||||
where
|
||||
T: Entity,
|
||||
@ -330,7 +377,7 @@ impl AsyncAppContext {
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: usize) {
|
||||
self.update(|cx| cx.remove_window(window_id))
|
||||
self.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
|
||||
pub fn activate_window(&mut self, window_id: usize) {
|
||||
@ -529,7 +576,7 @@ impl AppContext {
|
||||
App(self.weak_self.as_ref().unwrap().upgrade().unwrap())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
fn quit(&mut self) {
|
||||
let mut futures = Vec::new();
|
||||
|
||||
self.update(|cx| {
|
||||
@ -546,7 +593,8 @@ impl AppContext {
|
||||
}
|
||||
});
|
||||
|
||||
self.remove_all_windows();
|
||||
self.windows.clear();
|
||||
self.flush_effects();
|
||||
|
||||
let futures = futures::future::join_all(futures);
|
||||
if self
|
||||
@ -558,11 +606,6 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_all_windows(&mut self) {
|
||||
self.windows.clear();
|
||||
self.flush_effects();
|
||||
}
|
||||
|
||||
pub fn foreground(&self) -> &Rc<executor::Foreground> {
|
||||
&self.foreground
|
||||
}
|
||||
@ -679,24 +722,6 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_window(&self, window_id: usize) -> bool {
|
||||
self.window_ids()
|
||||
.find(|window| window == &window_id)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn window_is_active(&self, window_id: usize) -> bool {
|
||||
self.windows.get(&window_id).map_or(false, |w| w.is_active)
|
||||
}
|
||||
|
||||
pub fn root_view(&self, window_id: usize) -> Option<&AnyViewHandle> {
|
||||
self.windows.get(&window_id).map(|w| w.root_view())
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.windows.keys().copied()
|
||||
}
|
||||
|
||||
pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> {
|
||||
Some(self.views.get(&(window_id, view_id))?.ui_name())
|
||||
}
|
||||
@ -1048,10 +1073,6 @@ impl AppContext {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch_global_action<A: Action>(&mut self, action: A) {
|
||||
self.dispatch_global_action_any(&action);
|
||||
}
|
||||
|
||||
fn dispatch_global_action_any(&mut self, action: &dyn Action) -> bool {
|
||||
self.update(|this| {
|
||||
if let Some((name, mut handler)) = this.global_actions.remove_entry(&action.id()) {
|
||||
@ -1266,15 +1287,6 @@ impl AppContext {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_status_bar_item(&mut self, id: usize) {
|
||||
self.remove_window(id);
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: usize) {
|
||||
self.windows.remove(&window_id);
|
||||
self.flush_effects();
|
||||
}
|
||||
|
||||
pub fn build_window<V, F>(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
@ -1333,7 +1345,7 @@ impl AppContext {
|
||||
{
|
||||
let mut app = self.upgrade();
|
||||
platform_window.on_close(Box::new(move || {
|
||||
app.update(|cx| cx.remove_window(window_id));
|
||||
app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window()));
|
||||
}));
|
||||
}
|
||||
|
||||
@ -1619,17 +1631,7 @@ impl AppContext {
|
||||
Effect::RefreshWindows => {
|
||||
refreshing = true;
|
||||
}
|
||||
Effect::DispatchActionFrom {
|
||||
window_id,
|
||||
view_id,
|
||||
action,
|
||||
} => {
|
||||
self.handle_dispatch_action_from_effect(
|
||||
window_id,
|
||||
Some(view_id),
|
||||
action.as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
Effect::ActionDispatchNotification { action_id } => {
|
||||
self.handle_action_dispatch_notification_effect(action_id)
|
||||
}
|
||||
@ -1745,23 +1747,6 @@ impl AppContext {
|
||||
self.pending_effects.push_back(Effect::RefreshWindows);
|
||||
}
|
||||
|
||||
pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: impl Action) {
|
||||
self.dispatch_any_action_at(window_id, view_id, Box::new(action));
|
||||
}
|
||||
|
||||
pub fn dispatch_any_action_at(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
action: Box<dyn Action>,
|
||||
) {
|
||||
self.pending_effects.push_back(Effect::DispatchActionFrom {
|
||||
window_id,
|
||||
view_id,
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
fn perform_window_refresh(&mut self) {
|
||||
let window_ids = self.windows.keys().cloned().collect::<Vec<_>>();
|
||||
for window_id in window_ids {
|
||||
@ -1920,17 +1905,6 @@ impl AppContext {
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_dispatch_action_from_effect(
|
||||
&mut self,
|
||||
window_id: usize,
|
||||
view_id: Option<usize>,
|
||||
action: &dyn Action,
|
||||
) {
|
||||
self.update_window(window_id, |cx| {
|
||||
cx.handle_dispatch_action_from_effect(view_id, action)
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_action_dispatch_notification_effect(&mut self, action_id: TypeId) {
|
||||
self.action_dispatch_observations
|
||||
.clone()
|
||||
@ -2159,11 +2133,6 @@ pub enum Effect {
|
||||
result: MatchResult,
|
||||
},
|
||||
RefreshWindows,
|
||||
DispatchActionFrom {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
ActionDispatchNotification {
|
||||
action_id: TypeId,
|
||||
},
|
||||
@ -2252,13 +2221,6 @@ impl Debug for Effect {
|
||||
.field("view_id", view_id)
|
||||
.field("subscription_id", subscription_id)
|
||||
.finish(),
|
||||
Effect::DispatchActionFrom {
|
||||
window_id, view_id, ..
|
||||
} => f
|
||||
.debug_struct("Effect::DispatchActionFrom")
|
||||
.field("window_id", window_id)
|
||||
.field("view_id", view_id)
|
||||
.finish(),
|
||||
Effect::ActionDispatchNotification { action_id, .. } => f
|
||||
.debug_struct("Effect::ActionDispatchNotification")
|
||||
.field("action_id", action_id)
|
||||
@ -3189,20 +3151,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
|
||||
self.window_context.notify_view(window_id, view_id);
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: impl Action) {
|
||||
let window_id = self.window_id;
|
||||
let view_id = self.view_id;
|
||||
self.window_context
|
||||
.dispatch_action_at(window_id, view_id, action)
|
||||
}
|
||||
|
||||
pub fn dispatch_any_action(&mut self, action: Box<dyn Action>) {
|
||||
let window_id = self.window_id;
|
||||
let view_id = self.view_id;
|
||||
self.window_context
|
||||
.dispatch_any_action_at(window_id, view_id, action)
|
||||
}
|
||||
|
||||
pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext<V>)) {
|
||||
let handle = self.handle();
|
||||
self.window_context
|
||||
@ -4708,7 +4656,7 @@ mod tests {
|
||||
assert!(model_release_observed.get());
|
||||
|
||||
drop(view);
|
||||
cx.remove_window(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
assert!(view_released.get());
|
||||
assert!(view_release_observed.get());
|
||||
}
|
||||
|
@ -72,14 +72,16 @@ impl TestAppContext {
|
||||
}
|
||||
|
||||
pub fn dispatch_action<A: Action>(&self, window_id: usize, action: A) {
|
||||
let mut cx = self.cx.borrow_mut();
|
||||
if let Some(view_id) = cx.windows.get(&window_id).and_then(|w| w.focused_view_id) {
|
||||
cx.handle_dispatch_action_from_effect(window_id, Some(view_id), &action);
|
||||
}
|
||||
self.cx
|
||||
.borrow_mut()
|
||||
.update_window(window_id, |window| {
|
||||
window.handle_dispatch_action_from_effect(window.focused_view_id(), &action);
|
||||
})
|
||||
.expect("window not found");
|
||||
}
|
||||
|
||||
pub fn dispatch_global_action<A: Action>(&self, action: A) {
|
||||
self.cx.borrow_mut().dispatch_global_action(action);
|
||||
self.cx.borrow_mut().dispatch_global_action_any(&action);
|
||||
}
|
||||
|
||||
pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) {
|
||||
@ -180,7 +182,11 @@ impl TestAppContext {
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> Vec<usize> {
|
||||
self.cx.borrow().window_ids().collect()
|
||||
self.cx.borrow().windows.keys().copied().collect()
|
||||
}
|
||||
|
||||
pub fn remove_all_windows(&mut self) {
|
||||
self.update(|cx| cx.windows.clear());
|
||||
}
|
||||
|
||||
pub fn read<T, F: FnOnce(&AppContext) -> T>(&self, callback: F) -> T {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
elements::AnyRootElement,
|
||||
geometry::rect::RectF,
|
||||
json::{self, ToJson},
|
||||
json::ToJson,
|
||||
keymap_matcher::{Binding, Keystroke, MatchResult},
|
||||
platform::{
|
||||
self, Appearance, CursorStyle, Event, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent,
|
||||
@ -975,17 +975,6 @@ impl<'a> WindowContext<'a> {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn debug_elements(&self) -> Option<json::Value> {
|
||||
let view = self.window.root_view();
|
||||
Some(json!({
|
||||
"root_view": view.debug_json(self),
|
||||
"root_element": self.window.rendered_views.get(&view.id())
|
||||
.and_then(|root_element| {
|
||||
root_element.debug(self).log_err()
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn set_window_title(&mut self, title: &str) {
|
||||
self.window.platform_window.set_title(title);
|
||||
}
|
||||
@ -1454,13 +1443,7 @@ impl<V: View> Element<V> for ChildView {
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "ChildView",
|
||||
"view_id": self.view_id,
|
||||
"bounds": bounds.to_json(),
|
||||
"view": if let Some(view) = cx.views.get(&(cx.window_id, self.view_id)) {
|
||||
view.debug_json(cx)
|
||||
} else {
|
||||
json!(null)
|
||||
},
|
||||
"child": if let Some(element) = cx.window.rendered_views.get(&self.view_id) {
|
||||
element.debug(&cx.window_context).log_err().unwrap_or_else(|| json!(null))
|
||||
} else {
|
||||
|
@ -45,7 +45,6 @@ use std::{
|
||||
mem,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
pub trait Element<V: View>: 'static {
|
||||
type LayoutState;
|
||||
@ -709,7 +708,12 @@ impl<V: View> AnyRootElement for RootElement<V> {
|
||||
.ok_or_else(|| anyhow!("debug called on a root element for a dropped view"))?;
|
||||
let view = view.read(cx);
|
||||
let view_context = ViewContext::immutable(cx, self.view.id());
|
||||
Ok(self.element.debug(view, &view_context))
|
||||
Ok(serde_json::json!({
|
||||
"view_id": self.view.id(),
|
||||
"view_name": V::ui_name(),
|
||||
"view": view.debug_json(cx),
|
||||
"element": self.element.debug(view, &view_context)
|
||||
}))
|
||||
}
|
||||
|
||||
fn name(&self) -> Option<&str> {
|
||||
@ -717,63 +721,6 @@ impl<V: View> AnyRootElement for RootElement<V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: View, R: View> Element<V> for RootElement<R> {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: SizeConstraint,
|
||||
_view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> (Vector2F, ()) {
|
||||
let size = AnyRootElement::layout(self, constraint, cx)
|
||||
.log_err()
|
||||
.unwrap_or_else(|| Vector2F::zero());
|
||||
(size, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
_view: &mut V,
|
||||
cx: &mut ViewContext<V>,
|
||||
) {
|
||||
AnyRootElement::paint(self, scene, bounds.origin(), visible_bounds, cx).log_err();
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
range_utf16: Range<usize>,
|
||||
_bounds: RectF,
|
||||
_visible_bounds: RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
_view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
AnyRootElement::rect_for_text_range(self, range_utf16, cx)
|
||||
.log_err()
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
_bounds: RectF,
|
||||
_layout: &Self::LayoutState,
|
||||
_paint: &Self::PaintState,
|
||||
_view: &V,
|
||||
cx: &ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
AnyRootElement::debug(self, cx)
|
||||
.log_err()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ParentElement<'a, V: View>: Extend<AnyElement<V>> + Sized {
|
||||
fn add_children<E: Element<V>>(&mut self, children: impl IntoIterator<Item = E>) {
|
||||
self.extend(children.into_iter().map(|child| child.into_any()));
|
||||
|
@ -699,6 +699,31 @@ impl platform::Window for Window {
|
||||
msg: &str,
|
||||
answers: &[&str],
|
||||
) -> oneshot::Receiver<usize> {
|
||||
// macOs applies overrides to modal window buttons after they are added.
|
||||
// Two most important for this logic are:
|
||||
// * Buttons with "Cancel" title will be displayed as the last buttons in the modal
|
||||
// * Last button added to the modal via `addButtonWithTitle` stays focused
|
||||
// * Focused buttons react on "space"/" " keypresses
|
||||
// * Usage of `keyEquivalent`, `makeFirstResponder` or `setInitialFirstResponder` does not change the focus
|
||||
//
|
||||
// See also https://developer.apple.com/documentation/appkit/nsalert/1524532-addbuttonwithtitle#discussion
|
||||
// ```
|
||||
// By default, the first button has a key equivalent of Return,
|
||||
// any button with a title of “Cancel” has a key equivalent of Escape,
|
||||
// and any button with the title “Don’t Save” has a key equivalent of Command-D (but only if it’s not the first button).
|
||||
// ```
|
||||
//
|
||||
// To avoid situations when the last element added is "Cancel" and it gets the focus
|
||||
// (hence stealing both ESC and Space shortcuts), we find and add one non-Cancel button
|
||||
// last, so it gets focus and a Space shortcut.
|
||||
// This way, "Save this file? Yes/No/Cancel"-ish modals will get all three buttons mapped with a key.
|
||||
let latest_non_cancel_label = answers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find(|(_, &label)| label != "Cancel")
|
||||
.filter(|&(label_index, _)| label_index > 0);
|
||||
|
||||
unsafe {
|
||||
let alert: id = msg_send![class!(NSAlert), alloc];
|
||||
let alert: id = msg_send![alert, init];
|
||||
@ -709,10 +734,20 @@ impl platform::Window for Window {
|
||||
};
|
||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
||||
for (ix, answer) in answers.iter().enumerate() {
|
||||
|
||||
for (ix, answer) in answers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(ix, _)| Some(ix) != latest_non_cancel_label.map(|(ix, _)| ix))
|
||||
{
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
}
|
||||
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
||||
}
|
||||
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let done_tx = Cell::new(Some(done_tx));
|
||||
let block = ConcreteBlock::new(move |answer: NSInteger| {
|
||||
@ -720,7 +755,7 @@ impl platform::Window for Window {
|
||||
let _ = postage::sink::Sink::try_send(&mut done_tx, answer.try_into().unwrap());
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
|
||||
let native_window = self.0.borrow().native_window;
|
||||
self.0
|
||||
.borrow()
|
||||
|
@ -100,7 +100,7 @@ pub fn run_test(
|
||||
test_fn(cx, foreground_platform.clone(), deterministic.clone(), seed);
|
||||
});
|
||||
|
||||
cx.update(|cx| cx.remove_all_windows());
|
||||
cx.remove_all_windows();
|
||||
deterministic.run_until_parked();
|
||||
cx.update(|cx| cx.clear_globals());
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, EventContext,
|
||||
View, ViewContext, WeakViewHandle,
|
||||
actions, elements::*, impl_actions, platform::MouseButton, AppContext, Entity, View,
|
||||
ViewContext, WeakViewHandle,
|
||||
};
|
||||
|
||||
pub struct Select {
|
||||
@ -116,10 +116,9 @@ impl View for Select {
|
||||
.contained()
|
||||
.with_style(style.header)
|
||||
})
|
||||
.on_click(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx: &mut EventContext<Self>| cx.dispatch_action(ToggleSelect),
|
||||
),
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle(&Default::default(), cx);
|
||||
}),
|
||||
);
|
||||
if self.is_open {
|
||||
result.add_child(Overlay::new(
|
||||
@ -143,12 +142,9 @@ impl View for Select {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx: &mut EventContext<Self>| {
|
||||
cx.dispatch_action(SelectItem(ix))
|
||||
},
|
||||
)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.select_item(&SelectItem(ix), cx);
|
||||
})
|
||||
.into_any()
|
||||
}))
|
||||
},
|
||||
|
@ -137,7 +137,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
);
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
#cx_varname.update(|cx| cx.remove_all_windows());
|
||||
#cx_varname.remove_all_windows();
|
||||
deterministic.run_until_parked();
|
||||
#cx_varname.update(|cx| cx.clear_globals());
|
||||
));
|
||||
@ -212,7 +212,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||
);
|
||||
));
|
||||
cx_teardowns.extend(quote!(
|
||||
#cx_varname.update(|cx| cx.remove_all_windows());
|
||||
#cx_varname.remove_all_windows();
|
||||
deterministic.run_until_parked();
|
||||
#cx_varname.update(|cx| cx.clear_globals());
|
||||
));
|
||||
|
@ -2,27 +2,23 @@ use editor::Editor;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Entity, Subscription, View, ViewContext, ViewHandle,
|
||||
Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
pub struct ActiveBufferLanguage {
|
||||
active_language: Option<Option<Arc<str>>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Default for ActiveBufferLanguage {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveBufferLanguage {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
active_language: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
}
|
||||
@ -66,8 +62,12 @@ impl View for ActiveBufferLanguage {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(crate::Toggle)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
crate::toggle(workspace, &Default::default(), cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
} else {
|
||||
|
@ -11,21 +11,18 @@ use project::Project;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use workspace::{AppState, Workspace};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(language_selector, [Toggle]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<LanguageSelectorDelegate>::init(cx);
|
||||
cx.add_action({
|
||||
let language_registry = app_state.languages.clone();
|
||||
move |workspace, _: &Toggle, cx| toggle(workspace, language_registry.clone(), cx)
|
||||
});
|
||||
cx.add_action(toggle);
|
||||
}
|
||||
|
||||
fn toggle(
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
registry: Arc<LanguageRegistry>,
|
||||
_: &Toggle,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<()> {
|
||||
let (_, buffer, _) = workspace
|
||||
@ -34,6 +31,7 @@ fn toggle(
|
||||
.read(cx)
|
||||
.active_excerpt(cx)?;
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let registry = workspace.app_state().languages.clone();
|
||||
cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),
|
||||
|
@ -5,7 +5,7 @@ use futures::{future::Shared, FutureExt};
|
||||
use gpui::{executor::Background, Task};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use smol::{fs, io::BufReader};
|
||||
use smol::{fs, io::BufReader, process::Command};
|
||||
use std::{
|
||||
env::consts,
|
||||
path::{Path, PathBuf},
|
||||
@ -48,12 +48,41 @@ impl NodeRuntime {
|
||||
Ok(installation_path.join("bin/node"))
|
||||
}
|
||||
|
||||
pub async fn run_npm_subcommand(
|
||||
&self,
|
||||
directory: &Path,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<()> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.arg(subcommand)
|
||||
.args(args)
|
||||
.current_dir(directory)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
let output = smol::process::Command::new(node_binary)
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.args(["-fetch-retry-mintimeout", "2000"])
|
||||
.args(["-fetch-retry-maxtimeout", "5000"])
|
||||
@ -64,11 +93,11 @@ impl NodeRuntime {
|
||||
.context("failed to run npm info")?;
|
||||
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
));
|
||||
}
|
||||
|
||||
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
|
||||
@ -80,14 +109,14 @@ impl NodeRuntime {
|
||||
|
||||
pub async fn npm_install_packages(
|
||||
&self,
|
||||
packages: impl IntoIterator<Item = (&str, &str)>,
|
||||
directory: &Path,
|
||||
packages: impl IntoIterator<Item = (&str, &str)>,
|
||||
) -> Result<()> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
let output = smol::process::Command::new(node_binary)
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.args(["-fetch-retry-mintimeout", "2000"])
|
||||
.args(["-fetch-retry-maxtimeout", "5000"])
|
||||
@ -103,12 +132,13 @@ impl NodeRuntime {
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm install")?;
|
||||
|
||||
if !output.status.success() {
|
||||
Err(anyhow!(
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
))?;
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
OutlineView::init(cx);
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.downcast::<Editor>())
|
||||
|
@ -13,7 +13,7 @@ use gpui::{
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
@ -44,6 +44,7 @@ pub struct ProjectPanel {
|
||||
clipboard_entry: Option<ClipboardEntry>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
dragged_entry_destination: Option<Arc<Path>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@ -137,7 +138,8 @@ pub enum Event {
|
||||
}
|
||||
|
||||
impl ProjectPanel {
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
let project = workspace.project().clone();
|
||||
let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
cx.observe(&project, |this, _, cx| {
|
||||
this.update_visible_entries(None, cx);
|
||||
@ -206,6 +208,7 @@ impl ProjectPanel {
|
||||
clipboard_entry: None,
|
||||
context_menu: cx.add_view(ContextMenu::new),
|
||||
dragged_entry_destination: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this
|
||||
@ -1296,8 +1299,14 @@ impl View for ProjectPanel {
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(workspace::Open)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(task) = workspace.open(&Default::default(), cx) {
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
@ -1400,7 +1409,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
@ -1492,7 +1501,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
select_path(&panel, "root1", cx);
|
||||
assert_eq!(
|
||||
@ -1785,7 +1794,7 @@ mod tests {
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
|
||||
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.select_next(&Default::default(), cx);
|
||||
|
@ -11,24 +11,24 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::sync::Arc;
|
||||
use workspace::{
|
||||
notifications::simple_message_notification::MessageNotification, AppState, Workspace,
|
||||
WorkspaceLocation, WORKSPACE_DB,
|
||||
notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
|
||||
WORKSPACE_DB,
|
||||
};
|
||||
|
||||
actions!(projects, [OpenRecent]);
|
||||
|
||||
pub fn init(cx: &mut AppContext, app_state: Weak<AppState>) {
|
||||
cx.add_async_action(
|
||||
move |_: &mut Workspace, _: &OpenRecent, cx: &mut ViewContext<Workspace>| {
|
||||
toggle(app_state.clone(), cx)
|
||||
},
|
||||
);
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_async_action(toggle);
|
||||
RecentProjects::init(cx);
|
||||
}
|
||||
|
||||
fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
|
||||
fn toggle(
|
||||
_: &mut Workspace,
|
||||
_: &OpenRecent,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
let workspace_locations: Vec<_> = cx
|
||||
.background()
|
||||
@ -49,11 +49,7 @@ fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<
|
||||
let workspace = cx.weak_handle();
|
||||
cx.add_view(|cx| {
|
||||
RecentProjects::new(
|
||||
RecentProjectsDelegate::new(
|
||||
workspace,
|
||||
workspace_locations,
|
||||
app_state.clone(),
|
||||
),
|
||||
RecentProjectsDelegate::new(workspace, workspace_locations),
|
||||
cx,
|
||||
)
|
||||
.with_max_size(800., 1200.)
|
||||
@ -61,7 +57,7 @@ fn toggle(app_state: Weak<AppState>, cx: &mut ViewContext<Workspace>) -> Option<
|
||||
});
|
||||
} else {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.add_view(|_| MessageNotification::new_message("No recent projects to open."))
|
||||
cx.add_view(|_| MessageNotification::new("No recent projects to open."))
|
||||
})
|
||||
}
|
||||
})?;
|
||||
@ -74,7 +70,6 @@ type RecentProjects = Picker<RecentProjectsDelegate>;
|
||||
struct RecentProjectsDelegate {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
workspace_locations: Vec<WorkspaceLocation>,
|
||||
app_state: Weak<AppState>,
|
||||
selected_match_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
@ -83,12 +78,10 @@ impl RecentProjectsDelegate {
|
||||
fn new(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
workspace_locations: Vec<WorkspaceLocation>,
|
||||
app_state: Weak<AppState>,
|
||||
) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
workspace_locations,
|
||||
app_state,
|
||||
selected_match_index: 0,
|
||||
matches: Default::default(),
|
||||
}
|
||||
@ -155,20 +148,16 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut ViewContext<RecentProjects>) {
|
||||
if let Some(((selected_match, workspace), app_state)) = self
|
||||
if let Some((selected_match, workspace)) = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.zip(self.workspace.upgrade(cx))
|
||||
.zip(self.app_state.upgrade())
|
||||
{
|
||||
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(
|
||||
workspace_location.paths().as_ref().clone(),
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
workspace
|
||||
.open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
|
@ -338,8 +338,8 @@ impl BufferSearchBar {
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_search_option(option, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self>(
|
||||
@ -386,8 +386,10 @@ impl BufferSearchBar {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.boxed_clone();
|
||||
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
move |_, this, cx| match direction {
|
||||
Direction::Prev => this.select_prev_match(&Default::default(), cx),
|
||||
Direction::Next => this.select_next_match(&Default::default(), cx),
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<NavButton>(
|
||||
@ -405,7 +407,6 @@ impl BufferSearchBar {
|
||||
theme: &theme::Search,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let action = Box::new(Dismiss);
|
||||
let tooltip = "Dismiss Buffer Search";
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
@ -422,12 +423,17 @@ impl BufferSearchBar {
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.boxed_clone();
|
||||
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<CloseButton>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
|
||||
.with_tooltip::<CloseButton>(
|
||||
0,
|
||||
tooltip.to_string(),
|
||||
Some(Box::new(Dismiss)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
|
@ -788,9 +788,10 @@ impl ProjectSearchBar {
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.boxed_clone();
|
||||
move |_, _, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(search) = this.active_project_search.as_ref() {
|
||||
search.update(cx, |search, cx| search.select_match(direction, cx));
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<NavButton>(
|
||||
@ -822,8 +823,8 @@ impl ProjectSearchBar {
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_any_action(option.to_toggle_action())
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_search_option(option, cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<Self>(
|
||||
|
@ -7,7 +7,11 @@ use gpui::{
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use workspace::{dock::FocusDock, item::ItemHandle, NewTerminal, StatusItemView, Workspace};
|
||||
use workspace::{
|
||||
dock::{Dock, FocusDock},
|
||||
item::ItemHandle,
|
||||
NewTerminal, StatusItemView, Workspace,
|
||||
};
|
||||
|
||||
pub struct TerminalButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
@ -80,7 +84,11 @@ impl View for TerminalButton {
|
||||
this.deploy_terminal_menu(cx);
|
||||
} else {
|
||||
if !active {
|
||||
cx.dispatch_action(FocusDock);
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
|
@ -659,6 +659,7 @@ pub struct DiagnosticPathHeader {
|
||||
pub struct DiagnosticHeader {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub source: ContainedLabel,
|
||||
pub message: ContainedLabel,
|
||||
pub code: ContainedText,
|
||||
pub text_scale_factor: f32,
|
||||
|
@ -156,24 +156,7 @@ pub fn keystroke_label<V: View>(
|
||||
|
||||
pub type ButtonStyle = Interactive<ContainedText>;
|
||||
|
||||
pub fn cta_button<L, A, V>(
|
||||
label: L,
|
||||
action: A,
|
||||
max_width: f32,
|
||||
style: &ButtonStyle,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> MouseEventHandler<A, V>
|
||||
where
|
||||
L: Into<Cow<'static, str>>,
|
||||
A: 'static + Action + Clone,
|
||||
V: View,
|
||||
{
|
||||
cta_button_with_click::<A, _, _, _>(label, max_width, style, cx, move |_, _, cx| {
|
||||
cx.dispatch_action(action.clone())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cta_button_with_click<Tag, L, V, F>(
|
||||
pub fn cta_button<Tag, L, V, F>(
|
||||
label: L,
|
||||
max_width: f32,
|
||||
style: &ButtonStyle,
|
||||
|
@ -6,20 +6,18 @@ use staff_mode::StaffMode;
|
||||
use std::sync::Arc;
|
||||
use theme::{Theme, ThemeMeta, ThemeRegistry};
|
||||
use util::ResultExt;
|
||||
use workspace::{AppState, Workspace};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(theme_selector, [Toggle, Reload]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
cx.add_action({
|
||||
let theme_registry = app_state.themes.clone();
|
||||
move |workspace, _: &Toggle, cx| toggle(workspace, theme_registry.clone(), cx)
|
||||
});
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(toggle);
|
||||
ThemeSelector::init(cx);
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, themes: Arc<ThemeRegistry>, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let themes = workspace.app_state().themes.clone();
|
||||
cx.add_view(|cx| ThemeSelector::new(ThemeSelectorDelegate::new(themes, cx), cx))
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::http::HttpClient;
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::AsyncReadExt;
|
||||
use serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
@ -12,7 +12,10 @@ pub struct GitHubLspBinaryVersion {
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct GithubRelease {
|
||||
pub name: String,
|
||||
#[serde(rename = "prerelease")]
|
||||
pub pre_release: bool,
|
||||
pub assets: Vec<GithubReleaseAsset>,
|
||||
pub tarball_url: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@ -23,16 +26,18 @@ pub struct GithubReleaseAsset {
|
||||
|
||||
pub async fn latest_github_release(
|
||||
repo_name_with_owner: &str,
|
||||
pre_release: bool,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<GithubRelease, anyhow::Error> {
|
||||
let mut response = http
|
||||
.get(
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases/latest"),
|
||||
&format!("https://api.github.com/repos/{repo_name_with_owner}/releases"),
|
||||
Default::default(),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.context("error fetching latest release")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
@ -40,13 +45,20 @@ pub async fn latest_github_release(
|
||||
.await
|
||||
.context("error reading latest release")?;
|
||||
|
||||
let release = serde_json::from_slice::<GithubRelease>(body.as_slice());
|
||||
if release.is_err() {
|
||||
log::error!(
|
||||
"Github API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
}
|
||||
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
|
||||
Ok(releases) => releases,
|
||||
|
||||
release.context("error deserializing latest release")
|
||||
Err(_) => {
|
||||
log::error!(
|
||||
"Error deserializing Github API response text: {:?}",
|
||||
String::from_utf8_lossy(body.as_slice())
|
||||
);
|
||||
return Err(anyhow!("error deserializing latest release"));
|
||||
}
|
||||
};
|
||||
|
||||
releases
|
||||
.into_iter()
|
||||
.find(|release| release.pre_release == pre_release)
|
||||
.ok_or(anyhow!("Failed to find a release"))
|
||||
}
|
||||
|
@ -40,8 +40,14 @@ pub trait HttpClient: Send + Sync {
|
||||
&'a self,
|
||||
uri: &str,
|
||||
body: AsyncBody,
|
||||
follow_redirects: bool,
|
||||
) -> BoxFuture<'a, Result<Response<AsyncBody>, Error>> {
|
||||
let request = isahc::Request::builder()
|
||||
.redirect_policy(if follow_redirects {
|
||||
RedirectPolicy::Follow
|
||||
} else {
|
||||
RedirectPolicy::None
|
||||
})
|
||||
.method(Method::POST)
|
||||
.uri(uri)
|
||||
.header("Content-Type", "application/json")
|
||||
|
@ -18,7 +18,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
BaseKeymapSelector::init(cx);
|
||||
}
|
||||
|
||||
fn toggle(
|
||||
pub fn toggle(
|
||||
workspace: &mut Workspace,
|
||||
_: &ToggleBaseKeymapSelector,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
|
@ -5,7 +5,7 @@ use std::{borrow::Cow, sync::Arc};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
elements::{Flex, Label, ParentElement},
|
||||
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext,
|
||||
AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use settings::{settings_file::SettingsFile, Settings};
|
||||
|
||||
@ -20,7 +20,7 @@ pub const FIRST_OPEN: &str = "first_open";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
|
||||
let welcome_page = cx.add_view(WelcomePage::new);
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
||||
workspace.add_item(Box::new(welcome_page), cx)
|
||||
});
|
||||
|
||||
@ -30,7 +30,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
workspace.toggle_sidebar(SidebarSide::Left, cx);
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(cx));
|
||||
let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
|
||||
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||
cx.focus(&welcome_page);
|
||||
cx.notify();
|
||||
@ -43,6 +43,7 @@ pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub struct WelcomePage {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
@ -97,26 +98,46 @@ impl View for WelcomePage {
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(theme::ui::cta_button(
|
||||
.with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
|
||||
"Choose a theme",
|
||||
theme_selector::Toggle,
|
||||
width,
|
||||
&theme.welcome.button,
|
||||
cx,
|
||||
|_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
theme_selector::toggle(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.with_child(theme::ui::cta_button(
|
||||
.with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
|
||||
"Choose a keymap",
|
||||
ToggleBaseKeymapSelector,
|
||||
width,
|
||||
&theme.welcome.button,
|
||||
cx,
|
||||
|_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
base_keymap_picker::toggle(
|
||||
workspace,
|
||||
&Default::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
))
|
||||
.with_child(theme::ui::cta_button(
|
||||
.with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
|
||||
"Install the CLI",
|
||||
install_cli::Install,
|
||||
width,
|
||||
&theme.welcome.button,
|
||||
cx,
|
||||
|_, _, cx| {
|
||||
cx.app_context()
|
||||
.spawn(|cx| async move { install_cli::install_cli(&cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
},
|
||||
))
|
||||
.contained()
|
||||
.with_style(theme.welcome.button_group)
|
||||
@ -190,8 +211,9 @@ impl View for WelcomePage {
|
||||
}
|
||||
|
||||
impl WelcomePage {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
WelcomePage {
|
||||
workspace: workspace.weak_handle(),
|
||||
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
|
||||
}
|
||||
}
|
||||
@ -220,11 +242,15 @@ impl Item for WelcomePage {
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self> {
|
||||
Some(WelcomePage::new(cx))
|
||||
Some(WelcomePage {
|
||||
workspace: self.workspace.clone(),
|
||||
_settings_subscription: cx.observe_global::<Settings, _>(move |_, cx| cx.notify()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -271,11 +271,11 @@ impl Dock {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
||||
pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx);
|
||||
}
|
||||
|
||||
fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
||||
pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext<Workspace>) {
|
||||
Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx);
|
||||
}
|
||||
|
||||
@ -374,8 +374,8 @@ impl Dock {
|
||||
.with_background_color(style.wash_color)
|
||||
})
|
||||
.capture_all()
|
||||
.on_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(HideDock);
|
||||
.on_down(MouseButton::Left, |_, workspace, cx| {
|
||||
Dock::hide_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow),
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock};
|
||||
use crate::{handle_dropped_item, StatusItemView, Workspace};
|
||||
use gpui::{
|
||||
elements::{Empty, MouseEventHandler, Svg},
|
||||
platform::CursorStyle,
|
||||
@ -6,10 +8,6 @@ use gpui::{
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::{handle_dropped_item, StatusItemView, Workspace};
|
||||
|
||||
use super::{icon_for_dock_anchor, FocusDock, HideDock};
|
||||
|
||||
pub struct ToggleDockButton {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
@ -82,8 +80,12 @@ impl View for ToggleDockButton {
|
||||
|
||||
if dock_position.is_visible() {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(HideDock);
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::hide_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
@ -94,8 +96,12 @@ impl View for ToggleDockButton {
|
||||
)
|
||||
} else {
|
||||
button
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.dispatch_action(FocusDock);
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::focus_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
|
@ -114,17 +114,14 @@ impl Workspace {
|
||||
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
|
||||
self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
|
||||
self.show_notification(toast.id, cx, |cx| {
|
||||
cx.add_view(|_cx| match &toast.click {
|
||||
Some((click_msg, action)) => {
|
||||
simple_message_notification::MessageNotification::new_boxed_action(
|
||||
toast.msg.clone(),
|
||||
action.boxed_clone(),
|
||||
click_msg.clone(),
|
||||
)
|
||||
}
|
||||
None => {
|
||||
simple_message_notification::MessageNotification::new_message(toast.msg.clone())
|
||||
cx.add_view(|_cx| match toast.on_click.as_ref() {
|
||||
Some((click_msg, on_click)) => {
|
||||
let on_click = on_click.clone();
|
||||
simple_message_notification::MessageNotification::new(toast.msg.clone())
|
||||
.with_click_message(click_msg.clone())
|
||||
.on_click(move |cx| on_click(cx))
|
||||
}
|
||||
None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -152,19 +149,17 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub mod simple_message_notification {
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
|
||||
impl_actions,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AppContext, Element, Entity, View, ViewContext,
|
||||
AppContext, Element, Entity, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use crate::Workspace;
|
||||
|
||||
@ -194,7 +189,7 @@ pub mod simple_message_notification {
|
||||
|
||||
pub struct MessageNotification {
|
||||
message: Cow<'static, str>,
|
||||
click_action: Option<Box<dyn Action>>,
|
||||
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
|
||||
click_message: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
@ -207,36 +202,31 @@ pub mod simple_message_notification {
|
||||
}
|
||||
|
||||
impl MessageNotification {
|
||||
pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
|
||||
pub fn new<S>(message: S) -> MessageNotification
|
||||
where
|
||||
S: Into<Cow<'static, str>>,
|
||||
{
|
||||
Self {
|
||||
message: message.into(),
|
||||
click_action: None,
|
||||
on_click: None,
|
||||
click_message: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
|
||||
message: S1,
|
||||
click_action: Box<dyn Action>,
|
||||
click_message: S2,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
click_action: Some(click_action),
|
||||
click_message: Some(click_message.into()),
|
||||
}
|
||||
pub fn with_click_message<S>(mut self, message: S) -> Self
|
||||
where
|
||||
S: Into<Cow<'static, str>>,
|
||||
{
|
||||
self.click_message = Some(message.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
|
||||
message: S1,
|
||||
click_action: A,
|
||||
click_message: S2,
|
||||
) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
click_action: Some(Box::new(click_action) as Box<dyn Action>),
|
||||
click_message: Some(click_message.into()),
|
||||
}
|
||||
pub fn on_click<F>(mut self, on_click: F) -> Self
|
||||
where
|
||||
F: 'static + Fn(&mut ViewContext<Self>),
|
||||
{
|
||||
self.on_click = Some(Arc::new(on_click));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
|
||||
@ -255,14 +245,10 @@ pub mod simple_message_notification {
|
||||
|
||||
enum MessageNotificationTag {}
|
||||
|
||||
let click_action = self
|
||||
.click_action
|
||||
.as_ref()
|
||||
.map(|action| action.boxed_clone());
|
||||
let click_message = self.click_message.as_ref().map(|message| message.clone());
|
||||
let click_message = self.click_message.clone();
|
||||
let message = self.message.clone();
|
||||
|
||||
let has_click_action = click_action.is_some();
|
||||
let on_click = self.on_click.clone();
|
||||
let has_click_action = on_click.is_some();
|
||||
|
||||
MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
@ -292,8 +278,8 @@ pub mod simple_message_notification {
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(CancelMessageNotification)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.aligned()
|
||||
@ -326,10 +312,10 @@ pub mod simple_message_notification {
|
||||
// Since we're not using a proper overlay, we have to capture these extra events
|
||||
.on_down(MouseButton::Left, |_, _, _| {})
|
||||
.on_up(MouseButton::Left, |_, _, _| {})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
if let Some(click_action) = click_action.as_ref() {
|
||||
cx.dispatch_any_action(click_action.boxed_clone());
|
||||
cx.dispatch_action(CancelMessageNotification)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(on_click) = on_click.as_ref() {
|
||||
on_click(cx);
|
||||
this.dismiss(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(if has_click_action {
|
||||
@ -372,7 +358,7 @@ where
|
||||
Err(err) => {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.add_view(|_cx| {
|
||||
simple_message_notification::MessageNotification::new_message(format!(
|
||||
simple_message_notification::MessageNotification::new(format!(
|
||||
"Error: {:?}",
|
||||
err,
|
||||
))
|
||||
|
@ -2,7 +2,7 @@ mod dragged_item_receiver;
|
||||
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{
|
||||
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, ExpandDock, HideDock},
|
||||
dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock},
|
||||
item::WeakItemHandle,
|
||||
toolbar::Toolbar,
|
||||
Item, NewFile, NewSearch, NewTerminal, Workspace,
|
||||
@ -259,6 +259,10 @@ impl Pane {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn workspace(&self) -> &WeakViewHandle<Workspace> {
|
||||
&self.workspace
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.is_active
|
||||
}
|
||||
@ -1340,8 +1344,8 @@ impl Pane {
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(ActivateItem(ix));
|
||||
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||
this.activate_item(ix, true, true, cx);
|
||||
})
|
||||
.on_click(MouseButton::Middle, {
|
||||
let item_id = item.id();
|
||||
@ -1639,7 +1643,15 @@ impl Pane {
|
||||
3,
|
||||
"icons/x_mark_8.svg",
|
||||
cx,
|
||||
|_, cx| cx.dispatch_action(HideDock),
|
||||
|this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Dock::hide_dock(workspace, &Default::default(), cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
None,
|
||||
)
|
||||
}))
|
||||
@ -1693,8 +1705,8 @@ impl View for Pane {
|
||||
})
|
||||
.on_down(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
cx.dispatch_action(ActivateItem(active_item_index));
|
||||
move |_, this, cx| {
|
||||
this.activate_item(active_item_index, true, true, cx);
|
||||
},
|
||||
),
|
||||
);
|
||||
@ -1759,15 +1771,27 @@ impl View for Pane {
|
||||
})
|
||||
.on_down(
|
||||
MouseButton::Navigate(NavigationDirection::Back),
|
||||
move |_, _, cx| {
|
||||
let pane = cx.weak_handle();
|
||||
cx.dispatch_action(GoBack { pane: Some(pane) });
|
||||
move |_, pane, cx| {
|
||||
if let Some(workspace) = pane.workspace.upgrade(cx) {
|
||||
let pane = cx.weak_handle();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane), cx).detach_and_log_err(cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
.on_down(MouseButton::Navigate(NavigationDirection::Forward), {
|
||||
move |_, _, cx| {
|
||||
let pane = cx.weak_handle();
|
||||
cx.dispatch_action(GoForward { pane: Some(pane) })
|
||||
move |_, pane, cx| {
|
||||
if let Some(workspace) = pane.workspace.upgrade(cx) {
|
||||
let pane = cx.weak_handle();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_forward(workspace, Some(pane), cx).detach_and_log_err(cx)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_any_named("pane")
|
||||
|
@ -279,9 +279,9 @@ impl View for SidebarButtons {
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let action = action.clone();
|
||||
move |_, _, cx| cx.dispatch_action(action.clone())
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.sidebar
|
||||
.update(cx, |sidebar, cx| sidebar.toggle_item(ix, cx));
|
||||
})
|
||||
.with_tooltip::<Self>(
|
||||
ix,
|
||||
|
@ -130,8 +130,23 @@ impl View for Toolbar {
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
super::GoBack {
|
||||
pane: Some(pane.clone()),
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoBack { pane: None },
|
||||
"Go Back",
|
||||
@ -143,7 +158,24 @@ impl View for Toolbar {
|
||||
tooltip_style,
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
super::GoForward { pane: Some(pane) },
|
||||
{
|
||||
let pane = pane.clone();
|
||||
move |toolbar, cx| {
|
||||
if let Some(workspace) = toolbar
|
||||
.pane
|
||||
.upgrade(cx)
|
||||
.and_then(|pane| pane.read(cx).workspace().upgrade(cx))
|
||||
{
|
||||
let pane = pane.clone();
|
||||
cx.window_context().defer(move |cx| {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Pane::go_forward(workspace, Some(pane.clone()), cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
super::GoForward { pane: None },
|
||||
"Go Forward",
|
||||
cx,
|
||||
@ -161,13 +193,13 @@ impl View for Toolbar {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn nav_button<A: Action + Clone>(
|
||||
fn nav_button<A: Action, F: 'static + Fn(&mut Toolbar, &mut ViewContext<Toolbar>)>(
|
||||
svg_path: &'static str,
|
||||
style: theme::Interactive<theme::IconButton>,
|
||||
tooltip_style: TooltipStyle,
|
||||
enabled: bool,
|
||||
spacing: f32,
|
||||
action: A,
|
||||
on_click: F,
|
||||
tooltip_action: A,
|
||||
action_name: &str,
|
||||
cx: &mut ViewContext<Toolbar>,
|
||||
@ -195,8 +227,8 @@ fn nav_button<A: Action + Clone>(
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
cx.dispatch_action(action.clone())
|
||||
.on_click(MouseButton::Left, move |_, toolbar, cx| {
|
||||
on_click(toolbar, cx)
|
||||
})
|
||||
.with_tooltip::<A>(
|
||||
0,
|
||||
|
@ -42,8 +42,9 @@ use gpui::{
|
||||
CursorStyle, MouseButton, PathPromptOptions, Platform, PromptLevel, WindowBounds,
|
||||
WindowOptions,
|
||||
},
|
||||
Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext,
|
||||
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
};
|
||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
|
||||
use language::{LanguageRegistry, Rope};
|
||||
@ -59,7 +60,7 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
notifications::simple_message_notification::{MessageNotification, OsOpen},
|
||||
notifications::simple_message_notification::MessageNotification,
|
||||
persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace},
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
@ -139,7 +140,7 @@ pub struct ActivatePane(pub usize);
|
||||
pub struct Toast {
|
||||
id: usize,
|
||||
msg: Cow<'static, str>,
|
||||
click: Option<(Cow<'static, str>, Box<dyn Action>)>,
|
||||
on_click: Option<(Cow<'static, str>, Arc<dyn Fn(&mut WindowContext)>)>,
|
||||
}
|
||||
|
||||
impl Toast {
|
||||
@ -147,21 +148,17 @@ impl Toast {
|
||||
Toast {
|
||||
id,
|
||||
msg: msg.into(),
|
||||
click: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
|
||||
id: usize,
|
||||
msg: I1,
|
||||
click_msg: I2,
|
||||
action: impl Action,
|
||||
) -> Self {
|
||||
Toast {
|
||||
id,
|
||||
msg: msg.into(),
|
||||
click: Some((click_msg.into(), Box::new(action))),
|
||||
}
|
||||
pub fn on_click<F, M>(mut self, message: M, on_click: F) -> Self
|
||||
where
|
||||
M: Into<Cow<'static, str>>,
|
||||
F: Fn(&mut WindowContext) + 'static,
|
||||
{
|
||||
self.on_click = Some((message.into(), Arc::new(on_click)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +166,7 @@ impl PartialEq for Toast {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
&& self.msg == other.msg
|
||||
&& self.click.is_some() == other.click.is_some()
|
||||
&& self.on_click.is_some() == other.on_click.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,10 +175,7 @@ impl Clone for Toast {
|
||||
Toast {
|
||||
id: self.id,
|
||||
msg: self.msg.to_owned(),
|
||||
click: self
|
||||
.click
|
||||
.as_ref()
|
||||
.map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
|
||||
on_click: self.on_click.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -216,52 +210,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_, _: &Open, cx: &mut ViewContext<Workspace>| {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
if let Some(task) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.open_workspace_for_paths(paths, app_state, cx)
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
task.await.log_err();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewWindow, cx: &mut AppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewFile, cx: &mut AppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)).detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_async_action(Workspace::open);
|
||||
|
||||
cx.add_async_action(Workspace::follow_next_collaborator);
|
||||
cx.add_async_action(Workspace::close);
|
||||
cx.add_global_action(Workspace::close_global);
|
||||
cx.add_global_action(restart);
|
||||
cx.add_async_action(Workspace::save_all);
|
||||
cx.add_action(Workspace::add_folder_to_project);
|
||||
cx.add_action(
|
||||
@ -305,9 +259,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
} else {
|
||||
workspace.show_notification(1, cx, |cx| {
|
||||
cx.add_view(|_| {
|
||||
MessageNotification::new_message(
|
||||
"Successfully installed the `zed` binary",
|
||||
)
|
||||
MessageNotification::new("Successfully installed the `zed` binary")
|
||||
})
|
||||
});
|
||||
}
|
||||
@ -316,17 +268,16 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
.detach();
|
||||
});
|
||||
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
cx.add_action(
|
||||
move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
|
||||
create_and_open_local_file(&paths::SETTINGS, app_state.clone(), cx, || {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
Settings::initial_user_settings_content(&Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let client = &app_state.client;
|
||||
client.add_view_request_handler(Workspace::handle_follow);
|
||||
@ -934,7 +885,6 @@ impl Workspace {
|
||||
/// to the callback. Otherwise, a new empty window will be created.
|
||||
pub fn with_local_workspace<T, F>(
|
||||
&mut self,
|
||||
app_state: &Arc<AppState>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
callback: F,
|
||||
) -> Task<Result<T>>
|
||||
@ -945,7 +895,7 @@ impl Workspace {
|
||||
if self.project.read(cx).is_local() {
|
||||
Task::Ready(Some(Ok(callback(self, cx))))
|
||||
} else {
|
||||
let task = Self::new_local(Vec::new(), app_state.clone(), None, cx);
|
||||
let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
|
||||
cx.spawn(|_vh, mut cx| async move {
|
||||
let (workspace, _) = task.await;
|
||||
workspace.update(&mut cx, callback)
|
||||
@ -981,12 +931,18 @@ impl Workspace {
|
||||
}
|
||||
|
||||
pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
|
||||
let id = cx.window_ids().find(|&id| cx.window_is_active(id));
|
||||
if let Some(id) = id {
|
||||
//This can only get called when the window's project connection has been lost
|
||||
//so we don't need to prompt the user for anything and instead just close the window
|
||||
cx.remove_window(id);
|
||||
}
|
||||
cx.spawn(|mut cx| async move {
|
||||
let id = cx
|
||||
.window_ids()
|
||||
.into_iter()
|
||||
.find(|&id| cx.window_is_active(id));
|
||||
if let Some(id) = id {
|
||||
//This can only get called when the window's project connection has been lost
|
||||
//so we don't need to prompt the user for anything and instead just close the window
|
||||
cx.remove_window(id);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn close(
|
||||
@ -1011,19 +967,14 @@ impl Workspace {
|
||||
) -> Task<Result<bool>> {
|
||||
let active_call = self.active_call().cloned();
|
||||
let window_id = cx.window_id();
|
||||
let workspace_count = cx
|
||||
.window_ids()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.filter_map(|window_id| {
|
||||
cx.app_context()
|
||||
.root_view(window_id)?
|
||||
.clone()
|
||||
.downcast::<Workspace>()
|
||||
})
|
||||
.count();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let workspace_count = cx
|
||||
.window_ids()
|
||||
.into_iter()
|
||||
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
|
||||
.count();
|
||||
|
||||
if let Some(active_call) = active_call {
|
||||
if !quitting
|
||||
&& workspace_count == 1
|
||||
@ -1114,10 +1065,29 @@ impl Workspace {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
if let Some(task) = this
|
||||
.update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
|
||||
.log_err()
|
||||
{
|
||||
task.await?
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn open_workspace_for_paths(
|
||||
&mut self,
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let window_id = cx.window_id();
|
||||
@ -1129,6 +1099,7 @@ impl Workspace {
|
||||
} else {
|
||||
Some(self.prepare_to_close(false, cx))
|
||||
};
|
||||
let app_state = self.app_state.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let window_id_to_replace = if let Some(close_task) = close_task {
|
||||
@ -2682,36 +2653,37 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
|
||||
workspace.show_notification_once(0, cx, |cx| {
|
||||
cx.add_view(|_| {
|
||||
MessageNotification::new(
|
||||
"Failed to load any database file.",
|
||||
OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
|
||||
"Click to let us know about this error"
|
||||
)
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let backup_path = (*db::BACKUP_DB_PATH).read();
|
||||
if let Some(backup_path) = &*backup_path {
|
||||
const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) {
|
||||
workspace.show_notification_once(0, cx, |cx| {
|
||||
cx.add_view(|_| {
|
||||
let backup_path = backup_path.to_string_lossy();
|
||||
MessageNotification::new(
|
||||
format!(
|
||||
"Database file was corrupted. Old database backed up to {}",
|
||||
backup_path
|
||||
),
|
||||
OsOpen::new(backup_path.to_string()),
|
||||
"Click to show old database in finder",
|
||||
)
|
||||
MessageNotification::new("Failed to load any database file.")
|
||||
.with_click_message("Click to let us know about this error")
|
||||
.on_click(|cx| cx.platform().open_url(REPORT_ISSUE_URL))
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let backup_path = (*db::BACKUP_DB_PATH).read();
|
||||
if let Some(backup_path) = backup_path.clone() {
|
||||
workspace.show_notification_once(0, cx, move |cx| {
|
||||
cx.add_view(move |_| {
|
||||
MessageNotification::new(format!(
|
||||
"Database file was corrupted. Old database backed up to {}",
|
||||
backup_path.display()
|
||||
))
|
||||
.with_click_message("Click to show old database in finder")
|
||||
.on_click(move |cx| {
|
||||
cx.platform().open_url(&backup_path.to_string_lossy())
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}).log_err();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
impl Entity for Workspace {
|
||||
@ -2891,10 +2863,10 @@ impl std::fmt::Debug for OpenPaths {
|
||||
pub struct WorkspaceCreated(WeakViewHandle<Workspace>);
|
||||
|
||||
pub fn activate_workspace_for_project(
|
||||
cx: &mut AppContext,
|
||||
cx: &mut AsyncAppContext,
|
||||
predicate: impl Fn(&mut Project, &mut ModelContext<Project>) -> bool,
|
||||
) -> Option<WeakViewHandle<Workspace>> {
|
||||
for window_id in cx.window_ids().collect::<Vec<_>>() {
|
||||
for window_id in cx.window_ids() {
|
||||
let handle = cx
|
||||
.update_window(window_id, |cx| {
|
||||
if let Some(workspace_handle) = cx.root_view().clone().downcast::<Workspace>() {
|
||||
@ -2933,13 +2905,14 @@ pub fn open_paths(
|
||||
> {
|
||||
log::info!("open paths {:?}", abs_paths);
|
||||
|
||||
// Open paths in existing workspace if possible
|
||||
let existing =
|
||||
activate_workspace_for_project(cx, |project, cx| project.contains_paths(abs_paths, cx));
|
||||
|
||||
let app_state = app_state.clone();
|
||||
let abs_paths = abs_paths.to_vec();
|
||||
cx.spawn(|mut cx| async move {
|
||||
// Open paths in existing workspace if possible
|
||||
let existing = activate_workspace_for_project(&mut cx, |project, cx| {
|
||||
project.contains_paths(&abs_paths, cx)
|
||||
});
|
||||
|
||||
if let Some(existing) = existing {
|
||||
Ok((
|
||||
existing.clone(),
|
||||
@ -2997,12 +2970,11 @@ pub fn open_new(
|
||||
|
||||
pub fn create_and_open_local_file(
|
||||
path: &'static Path,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
default_content: impl 'static + Send + FnOnce() -> Rope,
|
||||
) -> Task<Result<Box<dyn ItemHandle>>> {
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let fs = &app_state.fs;
|
||||
let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
|
||||
if !fs.is_file(path).await {
|
||||
fs.create_file(path, Default::default()).await?;
|
||||
fs.save(path, &default_content(), Default::default())
|
||||
@ -3011,7 +2983,7 @@ pub fn create_and_open_local_file(
|
||||
|
||||
let mut items = workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(cx, |workspace, cx| {
|
||||
workspace.open_paths(vec![path.to_path_buf()], false, cx)
|
||||
})
|
||||
})?
|
||||
@ -3030,13 +3002,16 @@ pub fn join_remote_project(
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let existing_workspace = cx.update(|cx| {
|
||||
cx.window_ids()
|
||||
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
|
||||
.find(|workspace| {
|
||||
let existing_workspace = cx
|
||||
.window_ids()
|
||||
.into_iter()
|
||||
.filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::<Workspace>())
|
||||
.find(|workspace| {
|
||||
cx.read_window(workspace.window_id(), |cx| {
|
||||
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||
})
|
||||
});
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||
existing_workspace.downgrade()
|
||||
@ -3104,6 +3079,59 @@ pub fn join_remote_project(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn restart(_: &Restart, cx: &mut AppContext) {
|
||||
let should_confirm = cx.global::<Settings>().confirm_quit;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut workspaces = cx
|
||||
.window_ids()
|
||||
.into_iter()
|
||||
.filter_map(|window_id| {
|
||||
Some(
|
||||
cx.root_view(window_id)?
|
||||
.clone()
|
||||
.downcast::<Workspace>()?
|
||||
.downgrade(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If multiple windows have unsaved changes, and need a save prompt,
|
||||
// prompt in the active window before switching to a different window.
|
||||
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
|
||||
|
||||
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
|
||||
let answer = cx.prompt(
|
||||
workspace.window_id(),
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to restart?",
|
||||
&["Restart", "Cancel"],
|
||||
);
|
||||
|
||||
if let Some(mut answer) = answer {
|
||||
let answer = answer.next().await;
|
||||
if answer != Some(0) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user cancels any save prompt, then keep the app open.
|
||||
for workspace in workspaces {
|
||||
if !workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.prepare_to_close(true, cx)
|
||||
})?
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
cx.platform().restart();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
|
||||
let mut parts = value.split(',');
|
||||
let width: usize = parts.next()?.parse().ok()?;
|
||||
|
@ -23,7 +23,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("clangd/clangd", http).await?;
|
||||
let release = latest_github_release("clangd/clangd", false, http).await?;
|
||||
let asset_name = format!("clangd-mac-{}.zip", release.name);
|
||||
let asset = release
|
||||
.assets
|
||||
|
@ -24,7 +24,7 @@ impl LspAdapter for ElixirLspAdapter {
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("elixir-lsp/elixir-ls", http).await?;
|
||||
let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?;
|
||||
let asset_name = "elixir-ls.zip";
|
||||
let asset = release
|
||||
.assets
|
||||
|
@ -33,7 +33,7 @@ impl super::LspAdapter for GoLspAdapter {
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("golang/tools", http).await?;
|
||||
let release = latest_github_release("golang/tools", false, http).await?;
|
||||
let version: Option<String> = release.name.strip_prefix("gopls/v").map(str::to_string);
|
||||
if version.is_none() {
|
||||
log::warn!(
|
||||
|
@ -57,8 +57,8 @@ impl LspAdapter for HtmlLspAdapter {
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
[("vscode-langservers-extracted", version.as_str())],
|
||||
&container_dir,
|
||||
[("vscode-langservers-extracted", version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
@ -76,8 +76,8 @@ impl LspAdapter for JsonLspAdapter {
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
[("vscode-json-languageserver", version.as_str())],
|
||||
&container_dir,
|
||||
[("vscode-json-languageserver", version.as_str())],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ impl super::LspAdapter for LuaLspAdapter {
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("LuaLS/lua-language-server", http).await?;
|
||||
let release = latest_github_release("LuaLS/lua-language-server", false, http).await?;
|
||||
let version = release.name.clone();
|
||||
let platform = match consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
|
@ -53,7 +53,7 @@ impl LspAdapter for PythonLspAdapter {
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages([("pyright", version.as_str())], &container_dir)
|
||||
.npm_install_packages(&container_dir, [("pyright", version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
@ -24,18 +24,17 @@ impl LspAdapter for RustLspAdapter {
|
||||
&self,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
let release = latest_github_release("rust-analyzer/rust-analyzer", http).await?;
|
||||
let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?;
|
||||
let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH);
|
||||
let asset = release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?;
|
||||
let version = GitHubLspBinaryVersion {
|
||||
Ok(Box::new(GitHubLspBinaryVersion {
|
||||
name: release.name,
|
||||
url: asset.browser_download_url.clone(),
|
||||
};
|
||||
Ok(Box::new(version) as Box<_>)
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
@ -77,6 +76,7 @@ impl LspAdapter for RustLspAdapter {
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
|
||||
arguments: Default::default(),
|
||||
|
@ -1,4 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use gpui::AppContext;
|
||||
@ -6,7 +8,7 @@ use language::{LanguageServerBinary, LanguageServerName, LspAdapter};
|
||||
use lsp::CodeActionKind;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
use smol::fs;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
@ -14,8 +16,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::http::HttpClient;
|
||||
use util::ResultExt;
|
||||
use util::{fs::remove_matching, github::latest_github_release, http::HttpClient};
|
||||
use util::{github::GitHubLspBinaryVersion, ResultExt};
|
||||
|
||||
fn typescript_server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![
|
||||
@ -69,24 +71,24 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
versions: Box<dyn 'static + Send + Any>,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let versions = versions.downcast::<TypeScriptVersions>().unwrap();
|
||||
let version = version.downcast::<TypeScriptVersions>().unwrap();
|
||||
let server_path = container_dir.join(Self::NEW_SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
&container_dir,
|
||||
[
|
||||
("typescript", versions.typescript_version.as_str()),
|
||||
("typescript", version.typescript_version.as_str()),
|
||||
(
|
||||
"typescript-language-server",
|
||||
versions.server_version.as_str(),
|
||||
version.server_version.as_str(),
|
||||
),
|
||||
],
|
||||
&container_dir,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@ -172,8 +174,7 @@ pub struct EsLintLspAdapter {
|
||||
}
|
||||
|
||||
impl EsLintLspAdapter {
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/vscode-langservers-extracted/lib/eslint-language-server/eslintServer.js";
|
||||
const SERVER_PATH: &'static str = "vscode-eslint/server/out/eslintServer.js";
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||
@ -187,35 +188,10 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
Some(
|
||||
future::ready(json!({
|
||||
"": {
|
||||
"validate": "on",
|
||||
"packageManager": "npm",
|
||||
"useESLintClass": false,
|
||||
"experimental": {
|
||||
"useFlatConfig": false
|
||||
},
|
||||
"codeActionOnSave": {
|
||||
"mode": "all"
|
||||
},
|
||||
"format": false,
|
||||
"quiet": false,
|
||||
"onIgnoredFiles": "off",
|
||||
"options": {},
|
||||
"rulesCustomizations": [],
|
||||
"run": "onType",
|
||||
"problems": {
|
||||
"shortenToSingleLine": false
|
||||
},
|
||||
"nodePath": null,
|
||||
"codeAction": {
|
||||
"disableRuleComment": {
|
||||
"enable": true,
|
||||
"location": "separateLine",
|
||||
"commentStyle": "line"
|
||||
},
|
||||
"showDocumentation": {
|
||||
"enable": true
|
||||
}
|
||||
}
|
||||
"validate": "on",
|
||||
"rulesCustomizations": [],
|
||||
"run": "onType",
|
||||
"nodePath": null,
|
||||
}
|
||||
}))
|
||||
.boxed(),
|
||||
@ -228,30 +204,50 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Send + Any>> {
|
||||
Ok(Box::new(
|
||||
self.node
|
||||
.npm_package_latest_version("vscode-langservers-extracted")
|
||||
.await?,
|
||||
))
|
||||
// At the time of writing the latest vscode-eslint release was released in 2020 and requires
|
||||
// special custom LSP protocol extensions be handled to fully initalize. Download the latest
|
||||
// prerelease instead to sidestep this issue
|
||||
let release = latest_github_release("microsoft/vscode-eslint", true, http).await?;
|
||||
Ok(Box::new(GitHubLspBinaryVersion {
|
||||
name: release.name,
|
||||
url: release.tarball_url,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
versions: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
version: Box<dyn 'static + Send + Any>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
container_dir: PathBuf,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = versions.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name));
|
||||
let server_path = destination_path.join(Self::SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
remove_matching(&container_dir, |entry| entry != destination_path).await;
|
||||
|
||||
let mut response = http
|
||||
.get(&version.url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error downloading release: {}", err))?;
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&destination_path).await?;
|
||||
|
||||
let mut dir = fs::read_dir(&destination_path).await?;
|
||||
let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
|
||||
let repo_root = destination_path.join("vscode-eslint");
|
||||
fs::rename(first.path(), &repo_root).await?;
|
||||
|
||||
self.node
|
||||
.npm_install_packages(
|
||||
[("vscode-langservers-extracted", version.as_str())],
|
||||
&container_dir,
|
||||
)
|
||||
.run_npm_subcommand(&repo_root, "install", &[])
|
||||
.await?;
|
||||
|
||||
self.node
|
||||
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@ -263,18 +259,17 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
|
||||
async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: eslint_server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
container_dir
|
||||
))
|
||||
// This is unfortunate but we don't know what the version is to build a path directly
|
||||
let mut dir = fs::read_dir(&container_dir).await?;
|
||||
let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
|
||||
if !first.file_type().await?.is_dir() {
|
||||
return Err(anyhow!("First entry is not a directory"));
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: first.path().join(Self::SERVER_PATH),
|
||||
arguments: Default::default(),
|
||||
})
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
|
@ -61,7 +61,7 @@ impl LspAdapter for YamlLspAdapter {
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
.npm_install_packages([("yaml-language-server", version.as_str())], &container_dir)
|
||||
.npm_install_packages(&container_dir, [("yaml-language-server", version.as_str())])
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ use cli::{
|
||||
};
|
||||
use client::{self, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
FutureExt, SinkExt, StreamExt,
|
||||
@ -29,8 +30,16 @@ use settings::{
|
||||
use simplelog::ConfigBuilder;
|
||||
use smol::process::Command;
|
||||
use std::{
|
||||
env, ffi::OsStr, fs::OpenOptions, io::Write as _, os::unix::prelude::OsStrExt, panic,
|
||||
path::PathBuf, sync::Arc, thread, time::Duration,
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::OpenOptions,
|
||||
io::Write as _,
|
||||
os::unix::prelude::OsStrExt,
|
||||
panic,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Weak},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use terminal_view::{get_working_directory, TerminalView};
|
||||
use util::http::{self, HttpClient};
|
||||
@ -43,8 +52,8 @@ use staff_mode::StaffMode;
|
||||
use theme::ThemeRegistry;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
|
||||
OpenSettings, Workspace,
|
||||
dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings,
|
||||
Workspace,
|
||||
};
|
||||
use zed::{self, build_window_options, initialize_workspace, languages, menus};
|
||||
|
||||
@ -104,7 +113,16 @@ fn main() {
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.on_reopen(move |cx| cx.dispatch_global_action(NewFile));
|
||||
.on_reopen(move |cx| {
|
||||
if cx.has_global::<Weak<AppState>>() {
|
||||
if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.run(move |cx| {
|
||||
cx.set_global(*RELEASE_CHANNEL);
|
||||
@ -172,8 +190,8 @@ fn main() {
|
||||
})
|
||||
.detach();
|
||||
|
||||
client.start_telemetry();
|
||||
client.report_event(
|
||||
client.telemetry().start();
|
||||
client.telemetry().report_mixpanel_event(
|
||||
"start app",
|
||||
Default::default(),
|
||||
cx.global::<Settings>().telemetry(),
|
||||
@ -190,17 +208,18 @@ fn main() {
|
||||
dock_default_item_factory,
|
||||
background_actions,
|
||||
});
|
||||
cx.set_global(Arc::downgrade(&app_state));
|
||||
auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
|
||||
|
||||
workspace::init(app_state.clone(), cx);
|
||||
recent_projects::init(cx, Arc::downgrade(&app_state));
|
||||
recent_projects::init(cx);
|
||||
|
||||
journal::init(app_state.clone(), cx);
|
||||
language_selector::init(app_state.clone(), cx);
|
||||
theme_selector::init(app_state.clone(), cx);
|
||||
language_selector::init(cx);
|
||||
theme_selector::init(cx);
|
||||
zed::init(&app_state, cx);
|
||||
collab_ui::init(&app_state, cx);
|
||||
feedback::init(app_state.clone(), cx);
|
||||
feedback::init(cx);
|
||||
welcome::init(cx);
|
||||
|
||||
cx.set_menus(menus::menus());
|
||||
@ -274,7 +293,10 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
|
||||
cx.update(|cx| show_welcome_experience(app_state, cx));
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(NewFile);
|
||||
workspace::open_new(app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use collections::VecDeque;
|
||||
pub use editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
|
||||
use anyhow::anyhow;
|
||||
use feedback::{
|
||||
feedback_info_text::FeedbackInfoText, submit_feedback_button::SubmitFeedbackButton,
|
||||
};
|
||||
@ -20,7 +21,7 @@ use gpui::{
|
||||
geometry::vector::vec2f,
|
||||
impl_actions,
|
||||
platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
|
||||
ViewContext,
|
||||
AppContext, ViewContext,
|
||||
};
|
||||
pub use lsp;
|
||||
pub use project;
|
||||
@ -34,7 +35,10 @@ use terminal_view::terminal_button::TerminalButton;
|
||||
use util::{channel::ReleaseChannel, paths, ResultExt};
|
||||
use uuid::Uuid;
|
||||
pub use workspace;
|
||||
use workspace::{create_and_open_local_file, sidebar::SidebarSide, AppState, Restart, Workspace};
|
||||
use workspace::{
|
||||
create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
#[derive(Deserialize, Clone, PartialEq)]
|
||||
pub struct OpenBrowser {
|
||||
@ -111,7 +115,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
},
|
||||
);
|
||||
cx.add_global_action(quit);
|
||||
cx.add_global_action(restart);
|
||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||
cx.update_global::<Settings, _, _>(|settings, cx| {
|
||||
@ -146,72 +149,71 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext<Workspace>| {
|
||||
open_log_file(workspace, app_state.clone(), cx);
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
move |_: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
|
||||
open_log_file(workspace, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
|
||||
open_bundled_file(
|
||||
app_state.clone(),
|
||||
workspace,
|
||||
"licenses.md",
|
||||
"Open Source License Attribution",
|
||||
"Markdown",
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext<Workspace>| {
|
||||
open_telemetry_log_file(workspace, app_state.clone(), cx);
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
open_telemetry_log_file(workspace, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
|
||||
create_and_open_local_file(&paths::KEYMAP, app_state.clone(), cx, Default::default)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
move |_: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
|
||||
create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
|
||||
open_bundled_file(
|
||||
app_state.clone(),
|
||||
workspace,
|
||||
"keymaps/default.json",
|
||||
"Default Key Bindings",
|
||||
"JSON",
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
move |_: &mut Workspace, _: &OpenDefaultSettings, cx: &mut ViewContext<Workspace>| {
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
move |workspace: &mut Workspace,
|
||||
_: &OpenDefaultSettings,
|
||||
cx: &mut ViewContext<Workspace>| {
|
||||
open_bundled_file(
|
||||
app_state.clone(),
|
||||
workspace,
|
||||
DEFAULT_SETTINGS_ASSET_PATH,
|
||||
"Default Settings",
|
||||
"JSON",
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
cx.add_action({
|
||||
let app_state = app_state.clone();
|
||||
move |_: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
|
||||
let app_state = app_state.clone();
|
||||
move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
let markdown = app_state.languages.language_for_name("JSON");
|
||||
let content = to_string_pretty(&cx.debug_elements()).unwrap();
|
||||
let window_id = cx.window_id();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let markdown = markdown.await.log_err();
|
||||
let content = to_string_pretty(
|
||||
&cx.debug_elements(window_id)
|
||||
.ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?,
|
||||
)
|
||||
.unwrap();
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(&app_state, cx, move |workspace, cx| {
|
||||
workspace.with_local_workspace(cx, move |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let buffer = project
|
||||
@ -243,6 +245,28 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||
workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx);
|
||||
},
|
||||
);
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewWindow, cx: &mut AppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewFile, cx: &mut AppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
activity_indicator::init(cx);
|
||||
lsp_log::init(cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
@ -260,7 +284,7 @@ pub fn initialize_workspace(
|
||||
if let workspace::Event::PaneAdded(pane) = event {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().update(cx, |toolbar, cx| {
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
|
||||
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
|
||||
toolbar.add_item(breadcrumbs, cx);
|
||||
let buffer_search_bar = cx.add_view(BufferSearchBar::new);
|
||||
toolbar.add_item(buffer_search_bar, cx);
|
||||
@ -284,12 +308,11 @@ pub fn initialize_workspace(
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
|
||||
cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone()));
|
||||
|
||||
let collab_titlebar_item = cx.add_view(|cx| {
|
||||
CollabTitlebarItem::new(&workspace_handle, app_state.user_store.clone(), cx)
|
||||
});
|
||||
let collab_titlebar_item =
|
||||
cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
|
||||
workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
|
||||
|
||||
let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
|
||||
let project_panel = ProjectPanel::new(workspace, cx);
|
||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item(
|
||||
"icons/folder_tree_16.svg",
|
||||
@ -300,14 +323,15 @@ pub fn initialize_workspace(
|
||||
});
|
||||
|
||||
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
|
||||
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.clone(), cx));
|
||||
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
|
||||
let diagnostic_summary =
|
||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
|
||||
let activity_indicator =
|
||||
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
|
||||
let active_buffer_language = cx.add_view(|_| language_selector::ActiveBufferLanguage::new());
|
||||
let active_buffer_language =
|
||||
cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
|
||||
let feedback_button =
|
||||
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new());
|
||||
cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
|
||||
let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
status_bar.add_left_item(diagnostic_summary, cx);
|
||||
@ -354,77 +378,26 @@ pub fn build_window_options(
|
||||
}
|
||||
}
|
||||
|
||||
fn restart(_: &Restart, cx: &mut gpui::AppContext) {
|
||||
let mut workspaces = cx
|
||||
.window_ids()
|
||||
.filter_map(|window_id| {
|
||||
Some(
|
||||
cx.root_view(window_id)?
|
||||
.clone()
|
||||
.downcast::<Workspace>()?
|
||||
.downgrade(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If multiple windows have unsaved changes, and need a save prompt,
|
||||
// prompt in the active window before switching to a different window.
|
||||
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
|
||||
|
||||
let should_confirm = cx.global::<Settings>().confirm_quit;
|
||||
cx.spawn(|mut cx| async move {
|
||||
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
|
||||
let answer = cx.prompt(
|
||||
workspace.window_id(),
|
||||
PromptLevel::Info,
|
||||
"Are you sure you want to restart?",
|
||||
&["Restart", "Cancel"],
|
||||
);
|
||||
|
||||
if let Some(mut answer) = answer {
|
||||
let answer = answer.next().await;
|
||||
if answer != Some(0) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the user cancels any save prompt, then keep the app open.
|
||||
for workspace in workspaces {
|
||||
if !workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.prepare_to_close(true, cx)
|
||||
})?
|
||||
.await?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
cx.platform().restart();
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut gpui::AppContext) {
|
||||
let mut workspaces = cx
|
||||
.window_ids()
|
||||
.filter_map(|window_id| {
|
||||
Some(
|
||||
cx.root_view(window_id)?
|
||||
.clone()
|
||||
.downcast::<Workspace>()?
|
||||
.downgrade(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If multiple windows have unsaved changes, and need a save prompt,
|
||||
// prompt in the active window before switching to a different window.
|
||||
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
|
||||
|
||||
let should_confirm = cx.global::<Settings>().confirm_quit;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut workspaces = cx
|
||||
.window_ids()
|
||||
.into_iter()
|
||||
.filter_map(|window_id| {
|
||||
Some(
|
||||
cx.root_view(window_id)?
|
||||
.clone()
|
||||
.downcast::<Workspace>()?
|
||||
.downgrade(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// If multiple windows have unsaved changes, and need a save prompt,
|
||||
// prompt in the active window before switching to a different window.
|
||||
workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id()));
|
||||
|
||||
if let (true, Some(workspace)) = (should_confirm, workspaces.first()) {
|
||||
let answer = cx.prompt(
|
||||
workspace.window_id(),
|
||||
@ -464,20 +437,15 @@ fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
|
||||
cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
|
||||
}
|
||||
|
||||
fn open_log_file(
|
||||
workspace: &mut Workspace,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
const MAX_LINES: usize = 1000;
|
||||
|
||||
workspace
|
||||
.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
|
||||
.with_local_workspace(cx, move |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let (old_log, new_log) = futures::join!(
|
||||
app_state.fs.load(&paths::OLD_LOG),
|
||||
app_state.fs.load(&paths::LOG)
|
||||
);
|
||||
let (old_log, new_log) =
|
||||
futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
|
||||
|
||||
let mut lines = VecDeque::with_capacity(MAX_LINES);
|
||||
for line in old_log
|
||||
@ -522,15 +490,12 @@ fn open_log_file(
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_telemetry_log_file(
|
||||
workspace: &mut Workspace,
|
||||
app_state: Arc<AppState>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
workspace.with_local_workspace(&app_state.clone(), cx, move |_, cx| {
|
||||
fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.with_local_workspace(cx, move |workspace, cx| {
|
||||
let app_state = workspace.app_state().clone();
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
|
||||
let path = app_state.client.telemetry_log_file_path()?;
|
||||
let path = app_state.client.telemetry().log_file_path()?;
|
||||
app_state.fs.load(&path).await.log_err()
|
||||
}
|
||||
|
||||
@ -583,18 +548,18 @@ fn open_telemetry_log_file(
|
||||
}
|
||||
|
||||
fn open_bundled_file(
|
||||
app_state: Arc<AppState>,
|
||||
workspace: &mut Workspace,
|
||||
asset_path: &'static str,
|
||||
title: &'static str,
|
||||
language: &'static str,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let language = app_state.languages.language_for_name(language);
|
||||
let language = workspace.app_state().languages.language_for_name(language);
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
let language = language.await.log_err();
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||
workspace.with_local_workspace(cx, |workspace, cx| {
|
||||
let project = workspace.project();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let text = Assets::get(asset_path)
|
||||
@ -825,8 +790,12 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
cx.update(|cx| open_new(&app_state, cx, |_, cx| cx.dispatch_action(NewFile)))
|
||||
.await;
|
||||
cx.update(|cx| {
|
||||
open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
let window_id = *cx.window_ids().first().unwrap();
|
||||
let workspace = cx
|
||||
|
@ -176,6 +176,9 @@ export default function editor(colorScheme: ColorScheme) {
|
||||
left: 10,
|
||||
},
|
||||
},
|
||||
source: {
|
||||
text: text(colorScheme.middle, "sans", { size: "sm", weight: "bold", }),
|
||||
},
|
||||
message: {
|
||||
highlightText: text(colorScheme.middle, "sans", {
|
||||
size: "sm",
|
||||
|
@ -40,7 +40,7 @@ export default function HoverPopover(colorScheme: ColorScheme) {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
prose: text(layer, "sans", { size: "sm" }),
|
||||
diagnosticSourceHighlight: { underline: true, color: foreground(layer, "accent") },
|
||||
diagnosticSourceHighlight: { color: foreground(layer, "accent") },
|
||||
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user