Compare commits
1 Commits
telemetry-
...
assistant-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96cfc0e224 |
30
Cargo.lock
generated
30
Cargo.lock
generated
@@ -434,7 +434,7 @@ dependencies = [
|
||||
"smallvec",
|
||||
"smol",
|
||||
"strum 0.25.0",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"text",
|
||||
@@ -2472,7 +2472,7 @@ dependencies = [
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 1.0.69",
|
||||
"time",
|
||||
@@ -2653,7 +2653,7 @@ dependencies = [
|
||||
"strum 0.25.0",
|
||||
"subtle",
|
||||
"supermaven_api",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"theme",
|
||||
"thiserror 1.0.69",
|
||||
@@ -3366,9 +3366,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.8"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
@@ -4233,7 +4233,6 @@ dependencies = [
|
||||
"settings",
|
||||
"smallvec",
|
||||
"snippet_provider",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -6558,7 +6557,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol",
|
||||
"strum 0.25.0",
|
||||
"telemetry",
|
||||
"ui",
|
||||
"util",
|
||||
]
|
||||
@@ -6591,7 +6589,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"strum 0.25.0",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"theme",
|
||||
"thiserror 1.0.69",
|
||||
"tiktoken-rs",
|
||||
@@ -6722,9 +6720,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.162"
|
||||
version = "0.2.164"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
|
||||
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
||||
|
||||
[[package]]
|
||||
name = "libdbus-sys"
|
||||
@@ -9884,7 +9882,7 @@ dependencies = [
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"toml 0.8.19",
|
||||
"util",
|
||||
"worktree",
|
||||
@@ -10888,9 +10886,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.132"
|
||||
version = "1.0.133"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03"
|
||||
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
|
||||
dependencies = [
|
||||
"indexmap 2.6.0",
|
||||
"itoa",
|
||||
@@ -12202,13 +12200,11 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "telemetry"
|
||||
name = "telemetry_events"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures 0.3.31",
|
||||
"semantic_version",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -15558,7 +15554,7 @@ dependencies = [
|
||||
"tab_switcher",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
|
||||
@@ -111,7 +111,7 @@ members = [
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
"crates/telemetry",
|
||||
"crates/telemetry_events",
|
||||
"crates/terminal",
|
||||
"crates/terminal_view",
|
||||
"crates/text",
|
||||
@@ -287,7 +287,7 @@ supermaven_api = { path = "crates/supermaven_api" }
|
||||
tab_switcher = { path = "crates/tab_switcher" }
|
||||
task = { path = "crates/task" }
|
||||
tasks_ui = { path = "crates/tasks_ui" }
|
||||
telemetry = { path = "crates/telemetry" }
|
||||
telemetry_events = { path = "crates/telemetry_events" }
|
||||
terminal = { path = "crates/terminal" }
|
||||
terminal_view = { path = "crates/terminal_view" }
|
||||
text = { path = "crates/text" }
|
||||
@@ -354,7 +354,6 @@ derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
erased-serde = "0.4.5"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.14.0"
|
||||
@@ -604,7 +603,7 @@ snippets_ui = { codegen-units = 1 }
|
||||
sqlez_macros = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
supermaven_api = { codegen-units = 1 }
|
||||
telemetry = { codegen-units = 1 }
|
||||
telemetry_events = { codegen-units = 1 }
|
||||
theme_selector = { codegen-units = 1 }
|
||||
time_format = { codegen-units = 1 }
|
||||
ui_input = { codegen-units = 1 }
|
||||
|
||||
@@ -78,7 +78,7 @@ similar.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
|
||||
@@ -49,7 +49,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use text::{BufferSnapshot, ToPoint};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -50,7 +50,7 @@ use std::{
|
||||
task::{self, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::{OffsetRangeExt, ToPoint as _};
|
||||
use theme::ThemeSettings;
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use terminal::Terminal;
|
||||
use terminal_view::TerminalView;
|
||||
use theme::ThemeSettings;
|
||||
|
||||
@@ -42,7 +42,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -49,8 +49,8 @@ use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use ::telemetry::EventBody;
|
||||
pub use rpc::*;
|
||||
pub use telemetry_events::Event;
|
||||
pub use user::*;
|
||||
|
||||
static ZED_SERVER_URL: LazyLock<Option<String>> =
|
||||
|
||||
@@ -4,8 +4,7 @@ use crate::{ChannelId, TelemetrySettings};
|
||||
use anyhow::Result;
|
||||
use clock::SystemClock;
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::channel::mpsc;
|
||||
use futures::{Future, StreamExt};
|
||||
use futures::Future;
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
|
||||
use once_cell::sync::Lazy;
|
||||
@@ -17,8 +16,8 @@ use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
use std::{env, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use telemetry::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, EventBody,
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, ReplEvent, SettingEvent,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
@@ -288,29 +287,12 @@ impl Telemetry {
|
||||
session_id: String,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let (tx, mut rx) = mpsc::unbounded();
|
||||
|
||||
let mut state = self.state.lock();
|
||||
state.system_id = system_id.map(|id| id.into());
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id);
|
||||
state.app_version = release_channel::AppVersion::global(cx).to_string();
|
||||
state.os_name = os_name();
|
||||
drop(state);
|
||||
|
||||
let this = Arc::downgrade(&self);
|
||||
|
||||
::telemetry::init(tx);
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
while let Some(event) = rx.next().await {
|
||||
let Some(this) = this.upgrade() else {
|
||||
break;
|
||||
};
|
||||
this.report_event(EventBody::Event(event));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn metrics_enabled(self: &Arc<Self>) -> bool {
|
||||
@@ -346,7 +328,7 @@ impl Telemetry {
|
||||
copilot_enabled_for_language: bool,
|
||||
is_via_ssh: bool,
|
||||
) {
|
||||
let event = EventBody::Editor(EditorEvent {
|
||||
let event = Event::Editor(EditorEvent {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation: operation.into(),
|
||||
@@ -364,7 +346,7 @@ impl Telemetry {
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
) {
|
||||
let event = EventBody::InlineCompletion(InlineCompletionEvent {
|
||||
let event = Event::InlineCompletion(InlineCompletionEvent {
|
||||
provider,
|
||||
suggestion_accepted,
|
||||
file_extension,
|
||||
@@ -374,7 +356,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(self: &Arc<Self>, event: AssistantEvent) {
|
||||
self.report_event(EventBody::Assistant(event));
|
||||
self.report_event(Event::Assistant(event));
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
@@ -383,7 +365,7 @@ impl Telemetry {
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<ChannelId>,
|
||||
) {
|
||||
let event = EventBody::Call(CallEvent {
|
||||
let event = Event::Call(CallEvent {
|
||||
operation: operation.to_string(),
|
||||
room_id,
|
||||
channel_id: channel_id.map(|cid| cid.0),
|
||||
@@ -392,8 +374,8 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> EventBody {
|
||||
let event = EventBody::App(AppEvent { operation });
|
||||
pub fn report_app_event(self: &Arc<Self>, operation: String) -> Event {
|
||||
let event = Event::App(AppEvent { operation });
|
||||
|
||||
self.report_event(event.clone());
|
||||
|
||||
@@ -401,7 +383,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_setting_event(self: &Arc<Self>, setting: &'static str, value: String) {
|
||||
let event = EventBody::Setting(SettingEvent {
|
||||
let event = Event::Setting(SettingEvent {
|
||||
setting: setting.to_string(),
|
||||
value,
|
||||
});
|
||||
@@ -410,7 +392,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_extension_event(self: &Arc<Self>, extension_id: Arc<str>, version: Arc<str>) {
|
||||
self.report_event(EventBody::Extension(ExtensionEvent {
|
||||
self.report_event(Event::Extension(ExtensionEvent {
|
||||
extension_id,
|
||||
version,
|
||||
}))
|
||||
@@ -422,7 +404,7 @@ impl Telemetry {
|
||||
drop(state);
|
||||
|
||||
if let Some((start, end, environment)) = period_data {
|
||||
let event = EventBody::Edit(EditEvent {
|
||||
let event = Event::Edit(EditEvent {
|
||||
duration: end
|
||||
.saturating_duration_since(start)
|
||||
.min(Duration::from_secs(60 * 60 * 24))
|
||||
@@ -436,7 +418,7 @@ impl Telemetry {
|
||||
}
|
||||
|
||||
pub fn report_action_event(self: &Arc<Self>, source: &'static str, action: String) {
|
||||
let event = EventBody::Action(ActionEvent {
|
||||
let event = Event::Action(ActionEvent {
|
||||
source: source.to_string(),
|
||||
action,
|
||||
});
|
||||
@@ -496,7 +478,7 @@ impl Telemetry {
|
||||
kernel_status: String,
|
||||
repl_session_id: String,
|
||||
) {
|
||||
let event = EventBody::Repl(ReplEvent {
|
||||
let event = Event::Repl(ReplEvent {
|
||||
kernel_language,
|
||||
kernel_status,
|
||||
repl_session_id,
|
||||
@@ -505,7 +487,7 @@ impl Telemetry {
|
||||
self.report_event(event)
|
||||
}
|
||||
|
||||
fn report_event(self: &Arc<Self>, event: EventBody) {
|
||||
fn report_event(self: &Arc<Self>, event: Event) {
|
||||
let mut state = self.state.lock();
|
||||
|
||||
if !state.settings.metrics {
|
||||
@@ -687,7 +669,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -703,7 +685,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -719,7 +701,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -736,7 +718,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
@@ -770,7 +752,7 @@ mod tests {
|
||||
let event = telemetry.report_app_event(operation.clone());
|
||||
assert_eq!(
|
||||
event,
|
||||
EventBody::App(AppEvent {
|
||||
Event::App(AppEvent {
|
||||
operation: operation.clone(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -64,7 +64,7 @@ sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "json"
|
||||
strum.workspace = true
|
||||
subtle.workspace = true
|
||||
supermaven_api.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -18,8 +18,8 @@ use serde::{Deserialize, Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use telemetry::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, EventBody,
|
||||
use telemetry_events::{
|
||||
ActionEvent, AppEvent, AssistantEvent, CallEvent, CpuEvent, EditEvent, EditorEvent, Event,
|
||||
EventRequestBody, EventWrapper, ExtensionEvent, InlineCompletionEvent, MemoryEvent, Panic,
|
||||
ReplEvent, SettingEvent,
|
||||
};
|
||||
@@ -240,7 +240,7 @@ pub async fn post_hang(
|
||||
.ok();
|
||||
}
|
||||
|
||||
let report: telemetry::HangReport = serde_json::from_slice(&body).map_err(|err| {
|
||||
let report: telemetry_events::HangReport = serde_json::from_slice(&body).map_err(|err| {
|
||||
log::error!("can't parse report json: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
})?;
|
||||
@@ -283,7 +283,7 @@ pub async fn post_panic(
|
||||
))?;
|
||||
}
|
||||
|
||||
let report: telemetry::PanicRequest = serde_json::from_slice(&body)
|
||||
let report: telemetry_events::PanicRequest = serde_json::from_slice(&body)
|
||||
.map_err(|_| Error::http(StatusCode::BAD_REQUEST, "invalid json".into()))?;
|
||||
let panic = report.panic;
|
||||
|
||||
@@ -400,7 +400,7 @@ pub async fn post_events(
|
||||
|
||||
let checksum_matched = checksum == expected;
|
||||
|
||||
let request_body: telemetry::EventRequestBody =
|
||||
let request_body: telemetry_events::EventRequestBody =
|
||||
serde_json::from_slice(&body).map_err(|err| {
|
||||
log::error!("can't parse event json: {err}");
|
||||
Error::Internal(anyhow!(err))
|
||||
@@ -445,8 +445,7 @@ pub async fn post_events(
|
||||
|
||||
for wrapper in &request_body.events {
|
||||
match &wrapper.event {
|
||||
EventBody::Event(_) => continue,
|
||||
EventBody::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
|
||||
Event::Editor(event) => to_upload.editor_events.push(EditorEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
@@ -454,7 +453,7 @@ pub async fn post_events(
|
||||
country_code.clone(),
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::InlineCompletion(event) => {
|
||||
Event::InlineCompletion(event) => {
|
||||
to_upload
|
||||
.inline_completion_events
|
||||
.push(InlineCompletionEventRow::from_event(
|
||||
@@ -466,14 +465,14 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
Event::Call(event) => to_upload.call_events.push(CallEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Assistant(event) => {
|
||||
Event::Assistant(event) => {
|
||||
to_upload
|
||||
.assistant_events
|
||||
.push(AssistantEventRow::from_event(
|
||||
@@ -484,38 +483,36 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Cpu(_) | EventBody::Memory(_) => continue,
|
||||
EventBody::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
Event::Cpu(_) | Event::Memory(_) => continue,
|
||||
Event::App(event) => to_upload.app_events.push(AppEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Setting(event) => {
|
||||
to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
Event::Setting(event) => to_upload.setting_events.push(SettingEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
Event::Edit(event) => to_upload.edit_events.push(EditEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
EventBody::Extension(event) => {
|
||||
Event::Action(event) => to_upload.action_events.push(ActionEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
first_event_at,
|
||||
checksum_matched,
|
||||
)),
|
||||
Event::Extension(event) => {
|
||||
let metadata = app
|
||||
.db
|
||||
.get_extension_version(&event.extension_id, &event.version)
|
||||
@@ -531,7 +528,7 @@ pub async fn post_events(
|
||||
checksum_matched,
|
||||
))
|
||||
}
|
||||
EventBody::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
|
||||
Event::Repl(event) => to_upload.repl_events.push(ReplEventRow::from_event(
|
||||
event.clone(),
|
||||
wrapper,
|
||||
&request_body,
|
||||
@@ -1390,7 +1387,7 @@ fn for_snowflake(
|
||||
let timestamp =
|
||||
first_event_at + Duration::milliseconds(event.milliseconds_since_first_event);
|
||||
let (event_type, mut event_properties) = match &event.event {
|
||||
EventBody::Editor(e) => (
|
||||
Event::Editor(e) => (
|
||||
match e.operation.as_str() {
|
||||
"open" => "Editor Opened".to_string(),
|
||||
"save" => "Editor Saved".to_string(),
|
||||
@@ -1398,8 +1395,7 @@ fn for_snowflake(
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Event(e) => (e.name.clone(), serde_json::to_value(&e.properties).unwrap()),
|
||||
EventBody::InlineCompletion(e) => (
|
||||
Event::InlineCompletion(e) => (
|
||||
format!(
|
||||
"Inline Completion {}",
|
||||
if e.suggestion_accepted {
|
||||
@@ -1410,7 +1406,7 @@ fn for_snowflake(
|
||||
),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Call(e) => {
|
||||
Event::Call(e) => {
|
||||
let event_type = match e.operation.trim() {
|
||||
"unshare project" => "Project Unshared".to_string(),
|
||||
"open channel notes" => "Channel Notes Opened".to_string(),
|
||||
@@ -1431,21 +1427,21 @@ fn for_snowflake(
|
||||
|
||||
(event_type, serde_json::to_value(e).unwrap())
|
||||
}
|
||||
EventBody::Assistant(e) => (
|
||||
Event::Assistant(e) => (
|
||||
match e.phase {
|
||||
telemetry::AssistantPhase::Response => "Assistant Responded".to_string(),
|
||||
telemetry::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
|
||||
telemetry::AssistantPhase::Accepted => {
|
||||
telemetry_events::AssistantPhase::Response => "Assistant Responded".to_string(),
|
||||
telemetry_events::AssistantPhase::Invoked => "Assistant Invoked".to_string(),
|
||||
telemetry_events::AssistantPhase::Accepted => {
|
||||
"Assistant Response Accepted".to_string()
|
||||
}
|
||||
telemetry::AssistantPhase::Rejected => {
|
||||
telemetry_events::AssistantPhase::Rejected => {
|
||||
"Assistant Response Rejected".to_string()
|
||||
}
|
||||
},
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Cpu(_) | EventBody::Memory(_) => return None,
|
||||
EventBody::App(e) => {
|
||||
Event::Cpu(_) | Event::Memory(_) => return None,
|
||||
Event::App(e) => {
|
||||
let mut properties = json!({});
|
||||
let event_type = match e.operation.trim() {
|
||||
"extensions: install extension" => "Extension Installed".to_string(),
|
||||
@@ -1526,23 +1522,23 @@ fn for_snowflake(
|
||||
};
|
||||
(event_type, properties)
|
||||
}
|
||||
EventBody::Setting(e) => (
|
||||
Event::Setting(e) => (
|
||||
"Settings Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Extension(e) => (
|
||||
Event::Extension(e) => (
|
||||
"Extension Loaded".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Edit(e) => (
|
||||
Event::Edit(e) => (
|
||||
"Editor Edited".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Action(e) => (
|
||||
Event::Action(e) => (
|
||||
"Action Invoked".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
EventBody::Repl(e) => (
|
||||
Event::Repl(e) => (
|
||||
"Kernel Status Changed".to_string(),
|
||||
serde_json::to_value(e).unwrap(),
|
||||
),
|
||||
@@ -1582,7 +1578,7 @@ fn for_snowflake(
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SnowflakeRow {
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
pub user_id: Option<String>,
|
||||
|
||||
@@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.map_ok(|_| ())
|
||||
.boxed(),
|
||||
LspRequestKind::CodeAction => project
|
||||
.code_actions(&buffer, offset..offset, None, cx)
|
||||
.code_actions(&buffer, offset..offset, cx)
|
||||
.map(|_| Ok(()))
|
||||
.boxed(),
|
||||
LspRequestKind::Definition => project
|
||||
|
||||
@@ -13811,9 +13811,7 @@ impl CodeActionProvider for Model<Project> {
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
self.update(cx, |project, cx| {
|
||||
project.code_actions(buffer, range, None, cx)
|
||||
})
|
||||
self.update(cx, |project, cx| project.code_actions(buffer, range, cx))
|
||||
}
|
||||
|
||||
fn apply_code_action(
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod wasm_host;
|
||||
#[cfg(test)]
|
||||
mod extension_store_test;
|
||||
|
||||
use crate::extension_lsp_adapter::ExtensionLspAdapter;
|
||||
use anyhow::{anyhow, bail, Context as _, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
@@ -121,13 +122,7 @@ pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
|
||||
) {
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
_extension: Arc<dyn Extension>,
|
||||
_language_server_id: LanguageServerName,
|
||||
_language: LanguageName,
|
||||
) {
|
||||
}
|
||||
fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
|
||||
|
||||
fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {}
|
||||
|
||||
@@ -1260,9 +1255,12 @@ impl ExtensionStore {
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
ExtensionLspAdapter {
|
||||
extension: extension.clone(),
|
||||
language_server_id: language_server_id.clone(),
|
||||
language_name: language.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,23 +45,9 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
|
||||
}
|
||||
|
||||
pub struct ExtensionLspAdapter {
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
}
|
||||
|
||||
impl ExtensionLspAdapter {
|
||||
pub fn new(
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language_name: LanguageName,
|
||||
) -> Self {
|
||||
Self {
|
||||
extension,
|
||||
language_server_id,
|
||||
language_name,
|
||||
}
|
||||
}
|
||||
pub(crate) extension: Arc<dyn Extension>,
|
||||
pub(crate) language_server_id: LanguageServerName,
|
||||
pub(crate) language_name: LanguageName,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
|
||||
@@ -7,14 +7,11 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use async_compression::futures::bufread::GzipEncoder;
|
||||
use collections::BTreeMap;
|
||||
use extension::Extension;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{
|
||||
LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage,
|
||||
};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -83,18 +80,11 @@ impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn update_lsp_status(
|
||||
|
||||
@@ -177,17 +177,20 @@ impl HeadlessExtensionStore {
|
||||
let wasm_extension: Arc<dyn Extension> =
|
||||
Arc::new(WasmExtension::load(extension_dir, &manifest, wasm_host.clone(), &cx).await?);
|
||||
|
||||
for (language_server_id, language_server_config) in &manifest.language_servers {
|
||||
for (language_server_name, language_server_config) in &manifest.language_servers {
|
||||
for language in language_server_config.languages() {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.loaded_language_servers
|
||||
.entry(manifest.id.clone())
|
||||
.or_default()
|
||||
.push((language_server_id.clone(), language.clone()));
|
||||
.push((language_server_name.clone(), language.clone()));
|
||||
this.registration_hooks.register_lsp_adapter(
|
||||
wasm_extension.clone(),
|
||||
language_server_id.clone(),
|
||||
language.clone(),
|
||||
ExtensionLspAdapter {
|
||||
extension: wasm_extension.clone(),
|
||||
language_server_id: language_server_name.clone(),
|
||||
language_name: language,
|
||||
},
|
||||
);
|
||||
})?;
|
||||
}
|
||||
@@ -341,22 +344,10 @@ impl ExtensionRegistrationHooks for HeadlessRegistrationHooks {
|
||||
self.language_registry
|
||||
.register_language(language, None, matcher, load)
|
||||
}
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
) {
|
||||
fn register_lsp_adapter(&self, language: LanguageName, adapter: ExtensionLspAdapter) {
|
||||
log::info!("registering lsp adapter {:?}", language);
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language, Arc::new(adapter) as _);
|
||||
}
|
||||
|
||||
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
|
||||
|
||||
@@ -37,7 +37,6 @@ serde.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use client::telemetry::Telemetry;
|
||||
use gpui::{AnyElement, Div, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
use ui::{prelude::*, ButtonLike};
|
||||
@@ -5,15 +8,17 @@ use ui::{prelude::*, ButtonLike};
|
||||
#[derive(IntoElement)]
|
||||
pub struct FeatureUpsell {
|
||||
base: Div,
|
||||
telemetry: Arc<Telemetry>,
|
||||
text: SharedString,
|
||||
docs_url: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl FeatureUpsell {
|
||||
pub fn new(text: impl Into<SharedString>) -> Self {
|
||||
pub fn new(telemetry: Arc<Telemetry>, text: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: h_flex(),
|
||||
telemetry,
|
||||
text: text.into(),
|
||||
docs_url: None,
|
||||
children: SmallVec::new(),
|
||||
@@ -62,13 +67,12 @@ impl RenderOnce for FeatureUpsell {
|
||||
.child(Icon::new(IconName::ArrowUpRight)),
|
||||
)
|
||||
.on_click({
|
||||
let telemetry = self.telemetry.clone();
|
||||
let docs_url = docs_url.clone();
|
||||
move |_event, cx| {
|
||||
telemetry::event!(
|
||||
"Documentation Viewed",
|
||||
source = "Feature Upsell",
|
||||
url = docs_url
|
||||
);
|
||||
telemetry.report_app_event(format!(
|
||||
"feature upsell: viewed docs ({docs_url})"
|
||||
));
|
||||
cx.open_url(&docs_url)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -11,8 +11,7 @@ use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, BackgroundExecutor, Model, Task};
|
||||
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
|
||||
use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use snippet_provider::SnippetRegistry;
|
||||
use theme::{ThemeRegistry, ThemeSettings};
|
||||
use ui::SharedString;
|
||||
@@ -160,18 +159,11 @@ impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistratio
|
||||
|
||||
fn register_lsp_adapter(
|
||||
&self,
|
||||
extension: Arc<dyn Extension>,
|
||||
language_server_id: LanguageServerName,
|
||||
language: LanguageName,
|
||||
language_name: language::LanguageName,
|
||||
adapter: ExtensionLspAdapter,
|
||||
) {
|
||||
self.language_registry.register_lsp_adapter(
|
||||
language.clone(),
|
||||
Arc::new(ExtensionLspAdapter::new(
|
||||
extension,
|
||||
language_server_id,
|
||||
language,
|
||||
)),
|
||||
);
|
||||
self.language_registry
|
||||
.register_lsp_adapter(language_name, Arc::new(adapter));
|
||||
}
|
||||
|
||||
fn remove_lsp_adapter(
|
||||
|
||||
@@ -975,16 +975,19 @@ impl ExtensionsPage {
|
||||
let upsells_count = self.upsells.len();
|
||||
|
||||
v_flex().children(self.upsells.iter().enumerate().map(|(ix, feature)| {
|
||||
let telemetry = self.telemetry.clone();
|
||||
let upsell = match feature {
|
||||
Feature::Git => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed comes with basic Git support. More Git features are coming in the future.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git"),
|
||||
Feature::OpenIn => FeatureUpsell::new(
|
||||
telemetry,
|
||||
"Zed supports linking to a source line on GitHub and others.",
|
||||
)
|
||||
.docs_url("https://zed.dev/docs/git#git-integrations"),
|
||||
Feature::Vim => FeatureUpsell::new("Vim support is built-in to Zed!")
|
||||
Feature::Vim => FeatureUpsell::new(telemetry, "Vim support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/vim")
|
||||
.child(CheckboxWithLabel::new(
|
||||
"enable-vim",
|
||||
@@ -1004,22 +1007,36 @@ impl ExtensionsPage {
|
||||
);
|
||||
}),
|
||||
)),
|
||||
Feature::LanguageBash => FeatureUpsell::new("Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash"),
|
||||
Feature::LanguageC => FeatureUpsell::new("C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c"),
|
||||
Feature::LanguageCpp => FeatureUpsell::new("C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp"),
|
||||
Feature::LanguageGo => FeatureUpsell::new("Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go"),
|
||||
Feature::LanguagePython => FeatureUpsell::new("Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python"),
|
||||
Feature::LanguageReact => FeatureUpsell::new("React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript"),
|
||||
Feature::LanguageRust => FeatureUpsell::new("Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust"),
|
||||
Feature::LanguageBash => {
|
||||
FeatureUpsell::new(telemetry, "Shell support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/bash")
|
||||
}
|
||||
Feature::LanguageC => {
|
||||
FeatureUpsell::new(telemetry, "C support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/c")
|
||||
}
|
||||
Feature::LanguageCpp => {
|
||||
FeatureUpsell::new(telemetry, "C++ support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/cpp")
|
||||
}
|
||||
Feature::LanguageGo => {
|
||||
FeatureUpsell::new(telemetry, "Go support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/go")
|
||||
}
|
||||
Feature::LanguagePython => {
|
||||
FeatureUpsell::new(telemetry, "Python support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/python")
|
||||
}
|
||||
Feature::LanguageReact => {
|
||||
FeatureUpsell::new(telemetry, "React support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
Feature::LanguageRust => {
|
||||
FeatureUpsell::new(telemetry, "Rust support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/rust")
|
||||
}
|
||||
Feature::LanguageTypescript => {
|
||||
FeatureUpsell::new("Typescript support is built-in to Zed!")
|
||||
FeatureUpsell::new(telemetry, "Typescript support is built-in to Zed!")
|
||||
.docs_url("https://zed.dev/docs/languages/typescript")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3070,7 +3070,7 @@ impl<'a> WindowContext<'a> {
|
||||
&self,
|
||||
action: &dyn Action,
|
||||
focus_handle: &FocusHandle,
|
||||
) -> String {
|
||||
) -> Option<String> {
|
||||
self.bindings_for_action_in(action, focus_handle)
|
||||
.into_iter()
|
||||
.next()
|
||||
@@ -3082,7 +3082,6 @@ impl<'a> WindowContext<'a> {
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.unwrap_or_else(|| action.name().to_string())
|
||||
}
|
||||
|
||||
/// Dispatch a mouse or keyboard event on the window.
|
||||
|
||||
@@ -35,7 +35,6 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
theme.workspace = true
|
||||
thiserror.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::BackgroundExecutor;
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use telemetry::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::provider::anthropic::PROVIDER_ID as ANTHROPIC_PROVIDER_ID;
|
||||
|
||||
@@ -81,10 +81,9 @@ fn get_max_tokens(name: &str) -> usize {
|
||||
"llama2" | "yi" | "vicuna" | "stablelm2" => 4096,
|
||||
"llama3" | "gemma2" | "gemma" | "codegemma" | "starcoder" | "aya" => 8192,
|
||||
"codellama" | "starcoder2" => 16384,
|
||||
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
|
||||
| "dolphin-mixtral" => 32768,
|
||||
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "dolphin-mixtral" => 32768,
|
||||
"llama3.1" | "phi3" | "phi3.5" | "command-r" | "deepseek-coder-v2" | "yi-coder"
|
||||
| "llama3.2" => 128000,
|
||||
| "llama3.2" | "qwen2.5-coder" => 128000,
|
||||
_ => DEFAULT_TOKENS,
|
||||
}
|
||||
.clamp(1, MAXIMUM_TOKENS)
|
||||
|
||||
@@ -2090,33 +2090,19 @@ impl LspCommand for GetCodeActions {
|
||||
server_id: LanguageServerId,
|
||||
_: AsyncAppContext,
|
||||
) -> Result<Vec<CodeAction>> {
|
||||
let requested_kinds_set = if let Some(kinds) = self.kinds {
|
||||
Some(kinds.into_iter().collect::<HashSet<_>>())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(actions
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|entry| {
|
||||
let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some((requested_kinds, kind)) =
|
||||
requested_kinds_set.as_ref().zip(lsp_action.kind.as_ref())
|
||||
{
|
||||
if !requested_kinds.contains(kind) {
|
||||
return None;
|
||||
}
|
||||
if let lsp::CodeActionOrCommand::CodeAction(lsp_action) = entry {
|
||||
Some(CodeAction {
|
||||
server_id,
|
||||
range: self.range.clone(),
|
||||
lsp_action,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
Some(CodeAction {
|
||||
server_id,
|
||||
range: self.range.clone(),
|
||||
lsp_action,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
@@ -2015,7 +2015,6 @@ impl LspStore {
|
||||
&mut self,
|
||||
buffer_handle: &Model<Buffer>,
|
||||
range: Range<Anchor>,
|
||||
kinds: Option<Vec<CodeActionKind>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
if let Some((upstream_client, project_id)) = self.upstream_client() {
|
||||
@@ -2029,7 +2028,7 @@ impl LspStore {
|
||||
request: Some(proto::multi_lsp_query::Request::GetCodeActions(
|
||||
GetCodeActions {
|
||||
range: range.clone(),
|
||||
kinds: kinds.clone(),
|
||||
kinds: None,
|
||||
}
|
||||
.to_proto(project_id, buffer_handle.read(cx)),
|
||||
)),
|
||||
@@ -2055,7 +2054,7 @@ impl LspStore {
|
||||
.map(|code_actions_response| {
|
||||
GetCodeActions {
|
||||
range: range.clone(),
|
||||
kinds: kinds.clone(),
|
||||
kinds: None,
|
||||
}
|
||||
.response_from_proto(
|
||||
code_actions_response,
|
||||
@@ -2080,7 +2079,7 @@ impl LspStore {
|
||||
Some(range.start),
|
||||
GetCodeActions {
|
||||
range: range.clone(),
|
||||
kinds: kinds.clone(),
|
||||
kinds: None,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -52,8 +52,8 @@ use language::{
|
||||
Transaction, Unclipped,
|
||||
};
|
||||
use lsp::{
|
||||
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer,
|
||||
LanguageServerId, LanguageServerName, MessageActionItem,
|
||||
CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
||||
LanguageServerName, MessageActionItem,
|
||||
};
|
||||
use lsp_command::*;
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -2843,13 +2843,12 @@ impl Project {
|
||||
&mut self,
|
||||
buffer_handle: &Model<Buffer>,
|
||||
range: Range<T>,
|
||||
kinds: Option<Vec<CodeActionKind>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<CodeAction>>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end);
|
||||
self.lsp_store.update(cx, |lsp_store, cx| {
|
||||
lsp_store.code_actions(buffer_handle, range, kinds, cx)
|
||||
lsp_store.code_actions(buffer_handle, range, cx)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2792,9 +2792,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
|
||||
let fake_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// Language server returns code actions that contain commands, and not edits.
|
||||
let actions = project.update(cx, |project, cx| {
|
||||
project.code_actions(&buffer, 0..0, None, cx)
|
||||
});
|
||||
let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
|
||||
fake_server
|
||||
.handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
|
||||
Ok(Some(vec![
|
||||
@@ -4963,84 +4961,6 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
"a.ts": "a",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/dir".as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(typescript_lang());
|
||||
let mut fake_language_servers = language_registry.register_fake_lsp(
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let fake_server = fake_language_servers
|
||||
.next()
|
||||
.await
|
||||
.expect("failed to get the language server");
|
||||
|
||||
let mut request_handled = fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
|
||||
move |_, _| async move {
|
||||
Ok(Some(vec![
|
||||
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
|
||||
title: "organize imports".to_string(),
|
||||
kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
|
||||
..lsp::CodeAction::default()
|
||||
}),
|
||||
lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
|
||||
title: "fix code".to_string(),
|
||||
kind: Some(CodeActionKind::SOURCE_FIX_ALL),
|
||||
..lsp::CodeAction::default()
|
||||
}),
|
||||
]))
|
||||
},
|
||||
);
|
||||
|
||||
let code_actions_task = project.update(cx, |project, cx| {
|
||||
project.code_actions(
|
||||
&buffer,
|
||||
0..buffer.read(cx).len(),
|
||||
Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let () = request_handled
|
||||
.next()
|
||||
.await
|
||||
.expect("The code action request should have been triggered");
|
||||
|
||||
let code_actions = code_actions_task.await.unwrap();
|
||||
assert_eq!(code_actions.len(), 1);
|
||||
assert_eq!(
|
||||
code_actions[0].lsp_action.kind,
|
||||
Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
@@ -5172,7 +5092,7 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
|
||||
let code_actions_task = project.update(cx, |project, cx| {
|
||||
project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx)
|
||||
project.code_actions(&buffer, 0..buffer.read(cx).len(), cx)
|
||||
});
|
||||
|
||||
// cx.run_until_parked();
|
||||
|
||||
@@ -40,7 +40,6 @@ use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cell::OnceCell,
|
||||
cmp,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
@@ -54,7 +53,7 @@ use ui::{
|
||||
IndentGuideColors, IndentGuideLayout, KeyBinding, Label, ListItem, Scrollbar, ScrollbarState,
|
||||
Tooltip,
|
||||
};
|
||||
use util::{maybe, paths::compare_paths, ResultExt, TryFutureExt};
|
||||
use util::{maybe, ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotifyTaskExt},
|
||||
@@ -551,7 +550,7 @@ impl ProjectPanel {
|
||||
.entry((project_path.worktree_id, path_buffer.clone()))
|
||||
.and_modify(|strongest_diagnostic_severity| {
|
||||
*strongest_diagnostic_severity =
|
||||
cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
|
||||
std::cmp::min(*strongest_diagnostic_severity, diagnostic_severity);
|
||||
})
|
||||
.or_insert(diagnostic_severity);
|
||||
}
|
||||
@@ -1185,15 +1184,15 @@ impl ProjectPanel {
|
||||
|
||||
fn remove(&mut self, trash: bool, skip_prompt: bool, cx: &mut ViewContext<'_, ProjectPanel>) {
|
||||
maybe!({
|
||||
let items_to_delete = self.disjoint_entries_for_removal(cx);
|
||||
if items_to_delete.is_empty() {
|
||||
if self.marked_entries.is_empty() && self.selection.is_none() {
|
||||
return None;
|
||||
}
|
||||
let project = self.project.read(cx);
|
||||
let items_to_delete = self.marked_entries();
|
||||
|
||||
let mut dirty_buffers = 0;
|
||||
let file_paths = items_to_delete
|
||||
.iter()
|
||||
.into_iter()
|
||||
.filter_map(|selection| {
|
||||
let project_path = project.path_for_entry(selection.entry_id, cx)?;
|
||||
dirty_buffers +=
|
||||
@@ -1262,120 +1261,28 @@ impl ProjectPanel {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let next_selection = self.find_next_selection_after_deletion(items_to_delete, cx);
|
||||
cx.spawn(|panel, mut cx| async move {
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(answer) = answer {
|
||||
if answer.await != Ok(0) {
|
||||
return anyhow::Ok(());
|
||||
return Result::<(), anyhow::Error>::Ok(());
|
||||
}
|
||||
}
|
||||
for (entry_id, _) in file_paths {
|
||||
panel
|
||||
.update(&mut cx, |panel, cx| {
|
||||
panel
|
||||
.project
|
||||
.update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
|
||||
.context("no such entry")
|
||||
})??
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.project
|
||||
.update(cx, |project, cx| project.delete_entry(entry_id, trash, cx))
|
||||
.ok_or_else(|| anyhow!("no such entry"))
|
||||
})??
|
||||
.await?;
|
||||
}
|
||||
panel.update(&mut cx, |panel, cx| {
|
||||
if let Some(next_selection) = next_selection {
|
||||
panel.selection = Some(next_selection);
|
||||
panel.autoscroll(cx);
|
||||
} else {
|
||||
panel.select_last(&SelectLast {}, cx);
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
Result::<(), anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn find_next_selection_after_deletion(
|
||||
&self,
|
||||
sanitized_entries: BTreeSet<SelectedEntry>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<SelectedEntry> {
|
||||
if sanitized_entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let (worktree_id, worktree) = sanitized_entries
|
||||
.iter()
|
||||
.map(|entry| entry.worktree_id)
|
||||
.filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
|
||||
.max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
|
||||
|
||||
let marked_entries_in_worktree = sanitized_entries
|
||||
.iter()
|
||||
.filter(|e| e.worktree_id == worktree_id)
|
||||
.collect::<HashSet<_>>();
|
||||
let latest_entry = marked_entries_in_worktree
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
match (
|
||||
worktree.entry_for_id(a.entry_id),
|
||||
worktree.entry_for_id(b.entry_id),
|
||||
) {
|
||||
(Some(a), Some(b)) => {
|
||||
compare_paths((&a.path, a.is_file()), (&b.path, b.is_file()))
|
||||
}
|
||||
_ => cmp::Ordering::Equal,
|
||||
}
|
||||
})
|
||||
.and_then(|e| worktree.entry_for_id(e.entry_id))?;
|
||||
|
||||
let parent_path = latest_entry.path.parent()?;
|
||||
let parent_entry = worktree.entry_for_path(parent_path)?;
|
||||
|
||||
// Remove all siblings that are being deleted except the last marked entry
|
||||
let mut siblings: Vec<Entry> = worktree
|
||||
.snapshot()
|
||||
.child_entries(parent_path)
|
||||
.filter(|sibling| {
|
||||
sibling.id == latest_entry.id
|
||||
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: sibling.id,
|
||||
})
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
project::sort_worktree_entries(&mut siblings);
|
||||
let sibling_entry_index = siblings
|
||||
.iter()
|
||||
.position(|sibling| sibling.id == latest_entry.id)?;
|
||||
|
||||
if let Some(next_sibling) = sibling_entry_index
|
||||
.checked_add(1)
|
||||
.and_then(|i| siblings.get(i))
|
||||
{
|
||||
return Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: next_sibling.id,
|
||||
});
|
||||
}
|
||||
if let Some(prev_sibling) = sibling_entry_index
|
||||
.checked_sub(1)
|
||||
.and_then(|i| siblings.get(i))
|
||||
{
|
||||
return Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: prev_sibling.id,
|
||||
});
|
||||
}
|
||||
// No neighbour sibling found, fall back to parent
|
||||
Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: parent_entry.id,
|
||||
})
|
||||
}
|
||||
|
||||
fn unfold_directory(&mut self, _: &UnfoldDirectory, cx: &mut ViewContext<Self>) {
|
||||
if let Some((worktree, entry)) = self.selected_entry(cx) {
|
||||
self.unfolded_dir_ids.insert(entry.id);
|
||||
@@ -1928,54 +1835,6 @@ impl ProjectPanel {
|
||||
None
|
||||
}
|
||||
|
||||
fn disjoint_entries_for_removal(&self, cx: &AppContext) -> BTreeSet<SelectedEntry> {
|
||||
let marked_entries = self.marked_entries();
|
||||
let mut sanitized_entries = BTreeSet::new();
|
||||
if marked_entries.is_empty() {
|
||||
return sanitized_entries;
|
||||
}
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let marked_entries_by_worktree: HashMap<WorktreeId, Vec<SelectedEntry>> = marked_entries
|
||||
.into_iter()
|
||||
.filter(|entry| !project.entry_is_worktree_root(entry.entry_id, cx))
|
||||
.fold(HashMap::default(), |mut map, entry| {
|
||||
map.entry(entry.worktree_id).or_default().push(entry);
|
||||
map
|
||||
});
|
||||
|
||||
for (worktree_id, marked_entries) in marked_entries_by_worktree {
|
||||
if let Some(worktree) = project.worktree_for_id(worktree_id, cx) {
|
||||
let worktree = worktree.read(cx);
|
||||
let marked_dir_paths = marked_entries
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
worktree.entry_for_id(entry.entry_id).and_then(|entry| {
|
||||
if entry.is_dir() {
|
||||
Some(entry.path.as_ref())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
sanitized_entries.extend(marked_entries.into_iter().filter(|entry| {
|
||||
let Some(entry_info) = worktree.entry_for_id(entry.entry_id) else {
|
||||
return false;
|
||||
};
|
||||
let entry_path = entry_info.path.as_ref();
|
||||
let inside_marked_dir = marked_dir_paths.iter().any(|&marked_dir_path| {
|
||||
entry_path != marked_dir_path && entry_path.starts_with(marked_dir_path)
|
||||
});
|
||||
!inside_marked_dir
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
sanitized_entries
|
||||
}
|
||||
|
||||
// Returns list of entries that should be affected by an operation.
|
||||
// When currently selected entry is not marked, it's treated as the only marked entry.
|
||||
fn marked_entries(&self) -> BTreeSet<SelectedEntry> {
|
||||
@@ -5221,13 +5080,14 @@ mod tests {
|
||||
&[
|
||||
"v src",
|
||||
" v test",
|
||||
" second.rs <== selected",
|
||||
" second.rs",
|
||||
" third.rs"
|
||||
],
|
||||
"Project panel should have no deleted file, no other file is selected in it"
|
||||
);
|
||||
ensure_no_open_items_and_panes(&workspace, cx);
|
||||
|
||||
select_path(&panel, "src/test/second.rs", cx);
|
||||
panel.update(cx, |panel, cx| panel.open(&Open, cx));
|
||||
cx.executor().run_until_parked();
|
||||
assert_eq!(
|
||||
@@ -5261,7 +5121,7 @@ mod tests {
|
||||
submit_deletion_skipping_prompt(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v src", " v test", " third.rs <== selected"],
|
||||
&["v src", " v test", " third.rs"],
|
||||
"Project panel should have no deleted file, with one last file remaining"
|
||||
);
|
||||
ensure_no_open_items_and_panes(&workspace, cx);
|
||||
@@ -5770,11 +5630,7 @@ mod tests {
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v project_root",
|
||||
" v dir_1",
|
||||
" v nested_dir <== selected",
|
||||
]
|
||||
&["v project_root", " v dir_1", " v nested_dir",]
|
||||
);
|
||||
}
|
||||
#[gpui::test]
|
||||
@@ -6471,598 +6327,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"subdir1": {},
|
||||
"file1.txt": "",
|
||||
"file2.txt": "",
|
||||
},
|
||||
"dir2": {
|
||||
"subdir2": {},
|
||||
"file3.txt": "",
|
||||
"file4.txt": "",
|
||||
},
|
||||
"file5.txt": "",
|
||||
"file6.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2", cx);
|
||||
|
||||
// Test Case 1: Delete middle file in directory
|
||||
select_path(&panel, "root/dir1/file1.txt", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" > subdir1",
|
||||
" file1.txt <== selected",
|
||||
" file2.txt",
|
||||
" v dir2",
|
||||
" > subdir2",
|
||||
" file3.txt",
|
||||
" file4.txt",
|
||||
" file5.txt",
|
||||
" file6.txt",
|
||||
],
|
||||
"Initial state before deleting middle file"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" > subdir1",
|
||||
" file2.txt <== selected",
|
||||
" v dir2",
|
||||
" > subdir2",
|
||||
" file3.txt",
|
||||
" file4.txt",
|
||||
" file5.txt",
|
||||
" file6.txt",
|
||||
],
|
||||
"Should select next file after deleting middle file"
|
||||
);
|
||||
|
||||
// Test Case 2: Delete last file in directory
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" > subdir1 <== selected",
|
||||
" v dir2",
|
||||
" > subdir2",
|
||||
" file3.txt",
|
||||
" file4.txt",
|
||||
" file5.txt",
|
||||
" file6.txt",
|
||||
],
|
||||
"Should select next directory when last file is deleted"
|
||||
);
|
||||
|
||||
// Test Case 3: Delete root level file
|
||||
select_path(&panel, "root/file6.txt", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" > subdir1",
|
||||
" v dir2",
|
||||
" > subdir2",
|
||||
" file3.txt",
|
||||
" file4.txt",
|
||||
" file5.txt",
|
||||
" file6.txt <== selected",
|
||||
],
|
||||
"Initial state before deleting root level file"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" > subdir1",
|
||||
" v dir2",
|
||||
" > subdir2",
|
||||
" file3.txt",
|
||||
" file4.txt",
|
||||
" file5.txt <== selected",
|
||||
],
|
||||
"Should select prev entry at root level"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"subdir1": {
|
||||
"a.txt": "",
|
||||
"b.txt": ""
|
||||
},
|
||||
"file1.txt": "",
|
||||
},
|
||||
"dir2": {
|
||||
"subdir2": {
|
||||
"c.txt": "",
|
||||
"d.txt": ""
|
||||
},
|
||||
"file2.txt": "",
|
||||
},
|
||||
"file3.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
|
||||
|
||||
// Test Case 1: Select and delete nested directory with parent
|
||||
cx.simulate_modifiers_change(gpui::Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
});
|
||||
select_path_with_mark(&panel, "root/dir1/subdir1", cx);
|
||||
select_path_with_mark(&panel, "root/dir1", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1 <== selected <== marked",
|
||||
" v subdir1 <== marked",
|
||||
" a.txt",
|
||||
" b.txt",
|
||||
" file1.txt",
|
||||
" v dir2",
|
||||
" v subdir2",
|
||||
" c.txt",
|
||||
" d.txt",
|
||||
" file2.txt",
|
||||
" file3.txt",
|
||||
],
|
||||
"Initial state before deleting nested directory with parent"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir2 <== selected",
|
||||
" v subdir2",
|
||||
" c.txt",
|
||||
" d.txt",
|
||||
" file2.txt",
|
||||
" file3.txt",
|
||||
],
|
||||
"Should select next directory after deleting directory with parent"
|
||||
);
|
||||
|
||||
// Test Case 2: Select mixed files and directories across levels
|
||||
select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx);
|
||||
select_path_with_mark(&panel, "root/dir2/file2.txt", cx);
|
||||
select_path_with_mark(&panel, "root/file3.txt", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir2",
|
||||
" v subdir2",
|
||||
" c.txt <== marked",
|
||||
" d.txt",
|
||||
" file2.txt <== marked",
|
||||
" file3.txt <== selected <== marked",
|
||||
],
|
||||
"Initial state before deleting"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..15, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir2 <== selected",
|
||||
" v subdir2",
|
||||
" d.txt",
|
||||
],
|
||||
"Should select sibling directory"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"subdir1": {
|
||||
"a.txt": "",
|
||||
"b.txt": ""
|
||||
},
|
||||
"file1.txt": "",
|
||||
},
|
||||
"dir2": {
|
||||
"subdir2": {
|
||||
"c.txt": "",
|
||||
"d.txt": ""
|
||||
},
|
||||
"file2.txt": "",
|
||||
},
|
||||
"file3.txt": "",
|
||||
"file4.txt": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2/subdir2", cx);
|
||||
|
||||
// Test Case 1: Select all root files and directories
|
||||
cx.simulate_modifiers_change(gpui::Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
});
|
||||
select_path_with_mark(&panel, "root/dir1", cx);
|
||||
select_path_with_mark(&panel, "root/dir2", cx);
|
||||
select_path_with_mark(&panel, "root/file3.txt", cx);
|
||||
select_path_with_mark(&panel, "root/file4.txt", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1 <== marked",
|
||||
" v subdir1",
|
||||
" a.txt",
|
||||
" b.txt",
|
||||
" file1.txt",
|
||||
" v dir2 <== marked",
|
||||
" v subdir2",
|
||||
" c.txt",
|
||||
" d.txt",
|
||||
" file2.txt",
|
||||
" file3.txt <== marked",
|
||||
" file4.txt <== selected <== marked",
|
||||
],
|
||||
"State before deleting all contents"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&["v root <== selected"],
|
||||
"Only empty root directory should remain after deleting all contents"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root",
|
||||
json!({
|
||||
"dir1": {
|
||||
"subdir1": {
|
||||
"file_a.txt": "content a",
|
||||
"file_b.txt": "content b",
|
||||
},
|
||||
"subdir2": {
|
||||
"file_c.txt": "content c",
|
||||
},
|
||||
"file1.txt": "content 1",
|
||||
},
|
||||
"dir2": {
|
||||
"file2.txt": "content 2",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir1/subdir1", cx);
|
||||
toggle_expand_dir(&panel, "root/dir2", cx);
|
||||
cx.simulate_modifiers_change(gpui::Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory
|
||||
select_path_with_mark(&panel, "root/dir1", cx);
|
||||
select_path_with_mark(&panel, "root/dir1/subdir1", cx);
|
||||
select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1 <== marked",
|
||||
" v subdir1 <== marked",
|
||||
" file_a.txt <== selected <== marked",
|
||||
" file_b.txt",
|
||||
" > subdir2",
|
||||
" file1.txt",
|
||||
" v dir2",
|
||||
" file2.txt",
|
||||
],
|
||||
"State with parent dir, subdir, and file selected"
|
||||
);
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&["v root", " v dir2 <== selected", " file2.txt",],
|
||||
"Only dir2 should remain after deletion"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
// First worktree
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
"dir1": {
|
||||
"file1.txt": "content 1",
|
||||
"file2.txt": "content 2",
|
||||
},
|
||||
"dir2": {
|
||||
"file3.txt": "content 3",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Second worktree
|
||||
fs.insert_tree(
|
||||
"/root2",
|
||||
json!({
|
||||
"dir3": {
|
||||
"file4.txt": "content 4",
|
||||
"file5.txt": "content 5",
|
||||
},
|
||||
"file6.txt": "content 6",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
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 cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
// Expand all directories for testing
|
||||
toggle_expand_dir(&panel, "root1/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root1/dir2", cx);
|
||||
toggle_expand_dir(&panel, "root2/dir3", cx);
|
||||
|
||||
// Test Case 1: Delete files across different worktrees
|
||||
cx.simulate_modifiers_change(gpui::Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
});
|
||||
select_path_with_mark(&panel, "root1/dir1/file1.txt", cx);
|
||||
select_path_with_mark(&panel, "root2/dir3/file4.txt", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir1",
|
||||
" file1.txt <== marked",
|
||||
" file2.txt",
|
||||
" v dir2",
|
||||
" file3.txt",
|
||||
"v root2",
|
||||
" v dir3",
|
||||
" file4.txt <== selected <== marked",
|
||||
" file5.txt",
|
||||
" file6.txt",
|
||||
],
|
||||
"Initial state with files selected from different worktrees"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir1",
|
||||
" file2.txt",
|
||||
" v dir2",
|
||||
" file3.txt",
|
||||
"v root2",
|
||||
" v dir3",
|
||||
" file5.txt <== selected",
|
||||
" file6.txt",
|
||||
],
|
||||
"Should select next file in the last worktree after deletion"
|
||||
);
|
||||
|
||||
// Test Case 2: Delete directories from different worktrees
|
||||
select_path_with_mark(&panel, "root1/dir1", cx);
|
||||
select_path_with_mark(&panel, "root2/dir3", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir1 <== marked",
|
||||
" file2.txt",
|
||||
" v dir2",
|
||||
" file3.txt",
|
||||
"v root2",
|
||||
" v dir3 <== selected <== marked",
|
||||
" file5.txt",
|
||||
" file6.txt",
|
||||
],
|
||||
"State with directories marked from different worktrees"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir2",
|
||||
" file3.txt",
|
||||
"v root2",
|
||||
" file6.txt <== selected",
|
||||
],
|
||||
"Should select remaining file in last worktree after directory deletion"
|
||||
);
|
||||
|
||||
// Test Case 4: Delete all remaining files except roots
|
||||
select_path_with_mark(&panel, "root1/dir2/file3.txt", cx);
|
||||
select_path_with_mark(&panel, "root2/file6.txt", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root1",
|
||||
" v dir2",
|
||||
" file3.txt <== marked",
|
||||
"v root2",
|
||||
" file6.txt <== selected <== marked",
|
||||
],
|
||||
"State with all remaining files marked"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&["v root1", " v dir2", "v root2 <== selected"],
|
||||
"Second parent root should be selected after deleting"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
"/root_b",
|
||||
json!({
|
||||
"dir1": {
|
||||
"file1.txt": "content 1",
|
||||
"file2.txt": "content 2",
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.insert_tree(
|
||||
"/root_c",
|
||||
json!({
|
||||
"dir2": {},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
toggle_expand_dir(&panel, "root_b/dir1", cx);
|
||||
toggle_expand_dir(&panel, "root_c/dir2", cx);
|
||||
|
||||
cx.simulate_modifiers_change(gpui::Modifiers {
|
||||
control: true,
|
||||
..Default::default()
|
||||
});
|
||||
select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx);
|
||||
select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root_b",
|
||||
" v dir1",
|
||||
" file1.txt <== marked",
|
||||
" file2.txt <== selected <== marked",
|
||||
"v root_c",
|
||||
" v dir2",
|
||||
],
|
||||
"Initial state with files marked in root_b"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&[
|
||||
"v root_b",
|
||||
" v dir1 <== selected",
|
||||
"v root_c",
|
||||
" v dir2",
|
||||
],
|
||||
"After deletion in root_b as it's last deletion, selection should be in root_b"
|
||||
);
|
||||
|
||||
select_path_with_mark(&panel, "root_c/dir2", cx);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..20, cx),
|
||||
&["v root_b", " v dir1", "v root_c <== selected",],
|
||||
"After deleting from root_c, it should remain in root_c"
|
||||
);
|
||||
}
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &View<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
@@ -7100,32 +6364,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn select_path_with_mark(
|
||||
panel: &View<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
let entry = crate::SelectedEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id,
|
||||
};
|
||||
if !panel.marked_entries.contains(&entry) {
|
||||
panel.marked_entries.insert(entry);
|
||||
}
|
||||
panel.selection = Some(entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {:?}", path);
|
||||
});
|
||||
}
|
||||
|
||||
fn find_project_entry(
|
||||
panel: &View<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
|
||||
@@ -55,7 +55,7 @@ settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ use settings::{watch_config_file, Settings, SettingsStore};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use ::telemetry::LocationData;
|
||||
use smol::Async;
|
||||
use smol::{net::unix::UnixListener, stream::StreamExt as _};
|
||||
use std::ffi::OsStr;
|
||||
@@ -41,6 +40,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use telemetry_events::LocationData;
|
||||
use util::ResultExt;
|
||||
|
||||
fn init_logging_proxy() {
|
||||
@@ -147,7 +147,7 @@ fn init_panic_hook() {
|
||||
(&backtrace).join("\n")
|
||||
);
|
||||
|
||||
let panic_data = ::telemetry::Panic {
|
||||
let panic_data = telemetry_events::Panic {
|
||||
thread: thread_name.into(),
|
||||
payload: payload.clone(),
|
||||
location_data: info.location().map(|location| LocationData {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "telemetry"
|
||||
name = "telemetry_events"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
@@ -9,10 +9,8 @@ license = "GPL-3.0-or-later"
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/telemetry.rs"
|
||||
path = "src/telemetry_events.rs"
|
||||
|
||||
[dependencies]
|
||||
semantic_version.workspace = true
|
||||
serde.workspace = true
|
||||
futures.workspace = true
|
||||
serde_json = { workspace = true, features = ["raw_value"] }
|
||||
@@ -1,15 +1,8 @@
|
||||
//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information.
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use semantic_version::SemanticVersion;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use serde_json;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Display,
|
||||
sync::{Arc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{fmt::Display, sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EventRequestBody {
|
||||
@@ -39,14 +32,14 @@ impl EventRequestBody {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct EventWrapper {
|
||||
pub signed_in: bool,
|
||||
/// Duration between this event's timestamp and the timestamp of the first event in the current batch
|
||||
pub milliseconds_since_first_event: i64,
|
||||
/// The event itself
|
||||
#[serde(flatten)]
|
||||
pub event: EventBody,
|
||||
pub event: Event,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -97,8 +90,7 @@ impl Display for AssistantPhase {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventBody {
|
||||
Event(Event),
|
||||
pub enum Event {
|
||||
Editor(EditorEvent),
|
||||
InlineCompletion(InlineCompletionEvent),
|
||||
Call(CallEvent),
|
||||
@@ -273,60 +265,3 @@ pub struct Panic {
|
||||
pub struct PanicRequest {
|
||||
pub panic: Panic,
|
||||
}
|
||||
/// Macro to create telemetry events and send them to the telemetry queue.
|
||||
///
|
||||
/// By convention, the name should be "Noun Verbed", e.g. "Keymap Changed"
|
||||
/// or "Project Diagnostics Opened".
|
||||
///
|
||||
/// The properties can be any value that implements serde::Serialize.
|
||||
///
|
||||
/// ```
|
||||
/// telemetry::event!("Keymap Changed", version = "1.0.0");
|
||||
/// telemetry::event!("Documentation Viewed", url, source = "Extension Upsell");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! event {
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {{
|
||||
let event = $crate::Event {
|
||||
name: $name.to_string(),
|
||||
properties: std::collections::HashMap::from([
|
||||
$(
|
||||
(stringify!($key).to_string(),
|
||||
$crate::serde_json::value::to_value(&$crate::serialize_property!($key $(= $value)?))
|
||||
.unwrap_or_else(|_| $crate::serde_json::to_value(&()).unwrap())
|
||||
),
|
||||
)+
|
||||
]),
|
||||
};
|
||||
$crate::send_event(event);
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! serialize_property {
|
||||
($key:ident) => {
|
||||
$key
|
||||
};
|
||||
($key:ident = $value:expr) => {
|
||||
$value
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct Event {
|
||||
pub name: String,
|
||||
pub properties: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn send_event(event: Event) {
|
||||
if let Some(queue) = TELEMETRY_QUEUE.get() {
|
||||
queue.unbounded_send(event).ok();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(tx: mpsc::UnboundedSender<Event>) {
|
||||
TELEMETRY_QUEUE.set(tx).ok();
|
||||
}
|
||||
|
||||
static TELEMETRY_QUEUE: OnceLock<mpsc::UnboundedSender<Event>> = OnceLock::new();
|
||||
@@ -378,15 +378,7 @@ pub fn compare_paths(
|
||||
.as_deref()
|
||||
.map(NumericPrefixWithSuffix::from_numeric_prefixed_str);
|
||||
|
||||
num_and_remainder_a.cmp(&num_and_remainder_b).then_with(|| {
|
||||
if a_is_file && b_is_file {
|
||||
let ext_a = path_a.extension().unwrap_or_default();
|
||||
let ext_b = path_b.extension().unwrap_or_default();
|
||||
ext_a.cmp(ext_b)
|
||||
} else {
|
||||
cmp::Ordering::Equal
|
||||
}
|
||||
})
|
||||
num_and_remainder_a.cmp(&num_and_remainder_b)
|
||||
});
|
||||
if !ordering.is_eq() {
|
||||
return ordering;
|
||||
@@ -441,28 +433,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_paths_with_same_name_different_extensions() {
|
||||
let mut paths = vec![
|
||||
(Path::new("test_dirs/file.rs"), true),
|
||||
(Path::new("test_dirs/file.txt"), true),
|
||||
(Path::new("test_dirs/file.md"), true),
|
||||
(Path::new("test_dirs/file"), true),
|
||||
(Path::new("test_dirs/file.a"), true),
|
||||
];
|
||||
paths.sort_by(|&a, &b| compare_paths(a, b));
|
||||
assert_eq!(
|
||||
paths,
|
||||
vec![
|
||||
(Path::new("test_dirs/file"), true),
|
||||
(Path::new("test_dirs/file.a"), true),
|
||||
(Path::new("test_dirs/file.md"), true),
|
||||
(Path::new("test_dirs/file.rs"), true),
|
||||
(Path::new("test_dirs/file.txt"), true),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_paths_case_semi_sensitive() {
|
||||
let mut paths = vec![
|
||||
|
||||
@@ -48,7 +48,7 @@ use ui::{v_flex, ContextMenu};
|
||||
use util::{debug_panic, maybe, truncate_and_remove_front, ResultExt};
|
||||
|
||||
/// A selected entry in e.g. project panel.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct SelectedEntry {
|
||||
pub worktree_id: WorktreeId,
|
||||
pub entry_id: ProjectEntryId,
|
||||
|
||||
@@ -1096,17 +1096,10 @@ impl Workspace {
|
||||
);
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut paths_to_open = Vec::with_capacity(abs_paths.len());
|
||||
for path in abs_paths.into_iter() {
|
||||
if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
|
||||
paths_to_open.push(canonical)
|
||||
} else {
|
||||
paths_to_open.push(path)
|
||||
}
|
||||
}
|
||||
|
||||
let serialized_workspace: Option<SerializedWorkspace> =
|
||||
persistence::DB.workspace_for_roots(paths_to_open.as_slice());
|
||||
persistence::DB.workspace_for_roots(abs_paths.as_slice());
|
||||
|
||||
let mut paths_to_open = abs_paths;
|
||||
|
||||
let workspace_location = serialized_workspace
|
||||
.as_ref()
|
||||
|
||||
@@ -106,7 +106,7 @@ sysinfo.workspace = true
|
||||
tab_switcher.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
theme_selector.workspace = true
|
||||
|
||||
@@ -6,9 +6,6 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{AppContext, SemanticVersion};
|
||||
use http_client::{HttpRequestExt, Method};
|
||||
|
||||
use ::telemetry::LocationData;
|
||||
use ::telemetry::Panic;
|
||||
use ::telemetry::PanicRequest;
|
||||
use http_client::{self, HttpClient, HttpClientWithUrl};
|
||||
use paths::{crashes_dir, crashes_retired_dir};
|
||||
use project::Project;
|
||||
@@ -22,6 +19,9 @@ use std::{
|
||||
sync::{atomic::Ordering, Arc},
|
||||
};
|
||||
use std::{io::Write, panic, sync::atomic::AtomicU32, thread};
|
||||
use telemetry_events::LocationData;
|
||||
use telemetry_events::Panic;
|
||||
use telemetry_events::PanicRequest;
|
||||
use url::Url;
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -90,7 +90,7 @@ pub fn init_panic_hook(
|
||||
backtrace.drain(0..=ix);
|
||||
}
|
||||
|
||||
let panic_data = ::telemetry::Panic {
|
||||
let panic_data = telemetry_events::Panic {
|
||||
thread: thread_name.into(),
|
||||
payload,
|
||||
location_data: info.location().map(|location| LocationData {
|
||||
@@ -225,13 +225,13 @@ pub fn monitor_main_thread_hangs(
|
||||
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use ::telemetry::{BacktraceFrame, HangReport};
|
||||
use http_client::Method;
|
||||
use std::{
|
||||
ffi::c_int,
|
||||
sync::{mpsc, OnceLock},
|
||||
time::Duration,
|
||||
};
|
||||
use telemetry_events::{BacktraceFrame, HangReport};
|
||||
|
||||
use nix::sys::pthread;
|
||||
|
||||
@@ -490,7 +490,7 @@ async fn upload_previous_panics(
|
||||
async fn upload_panic(
|
||||
http: &Arc<HttpClientWithUrl>,
|
||||
panic_report_url: &Url,
|
||||
panic: ::telemetry::Panic,
|
||||
panic: telemetry_events::Panic,
|
||||
most_recent_panic: &mut Option<(i64, String)>,
|
||||
) -> Result<bool> {
|
||||
*most_recent_panic = Some((panic.panicked_on, panic.payload.clone()));
|
||||
|
||||
@@ -81,9 +81,9 @@ impl ActiveLineTrailerProvider for AssistantHintsProvider {
|
||||
}
|
||||
|
||||
let chat_keybinding =
|
||||
cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle);
|
||||
cx.keystroke_text_for_action_in(&assistant::ToggleFocus, focus_handle)?;
|
||||
let generate_keybinding =
|
||||
cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle);
|
||||
cx.keystroke_text_for_action_in(&zed_actions::InlineAssist::default(), focus_handle)?;
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
|
||||
@@ -31,7 +31,7 @@ Telemetry is sent from the application to our servers. Data is proxied through o
|
||||
|
||||
Diagnostic events include debug information (stack traces) from crash reports. Reports are sent on the first application launch after the crash occurred. We've built dashboards that allow us to visualize the frequency and severity of issues experienced by users. Having these reports sent automatically allows us to begin implementing fixes without the user needing to file a report in our issue tracker. The plots in the dashboards also give us an informal measurement of the stability of Zed.
|
||||
|
||||
You can see what data is sent when a panic occurs by inspecting the `Panic` struct in [crates/telemetry/src/telemetry.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry/src/telemetry.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation.
|
||||
You can see what data is sent when a panic occurs by inspecting the `Panic` struct in [crates/telemetry_events/src/telemetry_events.rs](https://github.com/zed-industries/zed/blob/main/crates/telemetry_events/src/telemetry_events.rs) in the Zed repo. You can find additional information in the [Debugging Crashes](./development/debugging-crashes.md) documentation.
|
||||
|
||||
### Usage Data (Metrics) {#metrics}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user