Compare commits
15 Commits
tasks-debu
...
v0.179.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6f061838d | ||
|
|
7fe42e860f | ||
|
|
3386e34f1a | ||
|
|
087f58206a | ||
|
|
bdd39bb012 | ||
|
|
f24086ecf8 | ||
|
|
c86019ec68 | ||
|
|
64892107e1 | ||
|
|
3c538f40d3 | ||
|
|
494cccf3fc | ||
|
|
e840961f1c | ||
|
|
bfbbe881c3 | ||
|
|
e2fc17d486 | ||
|
|
9b75c88498 | ||
|
|
f2c27bdcc3 |
13
.github/workflows/community_release_actions.yml
vendored
13
.github/workflows/community_release_actions.yml
vendored
@@ -58,10 +58,11 @@ jobs:
|
||||
- name: Send release notes email
|
||||
if: steps.check-promotion-from-preview.outputs.was_promoted_from_preview == 'true'
|
||||
run: |
|
||||
TAG="${{ github.event.release.tag_name }}"
|
||||
echo \"${{ toJSON(github.event.release.body) }}\" > release_body.txt
|
||||
jq -n --arg tag "$TAG" --rawfile body release_body.txt '{version: $tag, markdown_body: $body}' \
|
||||
> release_data.json
|
||||
curl -X POST "https://zed.dev/api/send_release_notes_email" \
|
||||
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"version": "${{ github.event.release.tag_name }}",
|
||||
"markdown_body": ${{ toJSON(github.event.release.body) }}
|
||||
}'
|
||||
-H "Authorization: Bearer ${{ secrets.RELEASE_NOTES_API_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @release_data.json
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -4926,25 +4926,13 @@ dependencies = [
|
||||
name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.8.0",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"human_bytes",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"ui",
|
||||
"urlencoding",
|
||||
@@ -17332,7 +17320,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.179.0"
|
||||
version = "0.179.2"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -188,7 +188,7 @@ impl AskPassSession {
|
||||
}
|
||||
|
||||
pub async fn run(&mut self) -> AskPassResult {
|
||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))).await;
|
||||
futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))).await;
|
||||
AskPassResult::Timedout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1673,13 +1673,13 @@ pub fn entry_diagnostic_aware_icon_decoration_and_color(
|
||||
pub fn entry_git_aware_label_color(git_status: GitSummary, ignored: bool, selected: bool) -> Color {
|
||||
let tracked = git_status.index + git_status.worktree;
|
||||
if ignored {
|
||||
Color::VersionControlIgnored
|
||||
Color::Ignored
|
||||
} else if git_status.conflict > 0 {
|
||||
Color::VersionControlConflict
|
||||
Color::Conflict
|
||||
} else if tracked.modified > 0 {
|
||||
Color::VersionControlModified
|
||||
Color::Modified
|
||||
} else if tracked.added > 0 || git_status.untracked > 0 {
|
||||
Color::VersionControlAdded
|
||||
Color::Created
|
||||
} else {
|
||||
entry_label_color(selected)
|
||||
}
|
||||
|
||||
@@ -15,25 +15,12 @@ path = "src/feedback.rs"
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
bitflags.workspace = true
|
||||
client.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
human_bytes = "0.4.1"
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
ui.workspace = true
|
||||
urlencoding.workspace = true
|
||||
|
||||
@@ -11,19 +11,16 @@ actions!(
|
||||
zed,
|
||||
[
|
||||
CopySystemSpecsIntoClipboard,
|
||||
EmailZed,
|
||||
FileBugReport,
|
||||
OpenZedRepo,
|
||||
RequestFeature,
|
||||
OpenZedRepo
|
||||
]
|
||||
);
|
||||
|
||||
const fn zed_repo_url() -> &'static str {
|
||||
"https://github.com/zed-industries/zed"
|
||||
}
|
||||
const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed";
|
||||
|
||||
fn request_feature_url() -> String {
|
||||
"https://github.com/zed-industries/zed/discussions/new/choose".to_string()
|
||||
}
|
||||
const REQUEST_FEATURE_URL: &str = "https://github.com/zed-industries/zed/discussions/new/choose";
|
||||
|
||||
fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
format!(
|
||||
@@ -38,6 +35,18 @@ fn file_bug_report_url(specs: &SystemSpecs) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn email_zed_url(specs: &SystemSpecs) -> String {
|
||||
format!(
|
||||
concat!("mailto:hi@zed.dev", "?", "body={}"),
|
||||
email_body(specs)
|
||||
)
|
||||
}
|
||||
|
||||
fn email_body(specs: &SystemSpecs) -> String {
|
||||
let body = format!("\n\nSystem Information:\n\n{}", specs);
|
||||
urlencoding::encode(&body).to_string()
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
@@ -66,14 +75,8 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.register_action(|_, _: &RequestFeature, window, cx| {
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
cx.update(|_, cx| {
|
||||
cx.open_url(&request_feature_url());
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
.register_action(|_, _: &RequestFeature, _, cx| {
|
||||
cx.open_url(REQUEST_FEATURE_URL);
|
||||
})
|
||||
.register_action(move |_, _: &FileBugReport, window, cx| {
|
||||
let specs = SystemSpecs::new(window, cx);
|
||||
@@ -86,8 +89,19 @@ pub fn init(cx: &mut App) {
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.register_action(move |_, _: &EmailZed, window, cx| {
|
||||
let specs = SystemSpecs::new(window, cx);
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
let specs = specs.await;
|
||||
cx.update(|_, cx| {
|
||||
cx.open_url(&email_zed_url(&specs));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.register_action(move |_, _: &OpenZedRepo, _, cx| {
|
||||
cx.open_url(zed_repo_url());
|
||||
cx.open_url(ZED_REPO_URL);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -1,421 +1,37 @@
|
||||
use std::{
|
||||
ops::RangeInclusive,
|
||||
sync::{Arc, LazyLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use bitflags::bitflags;
|
||||
use client::Client;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
div, rems, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
PromptLevel, Render, Task, Window,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
use language::Buffer;
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use serde_derive::Serialize;
|
||||
use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
|
||||
use util::ResultExt;
|
||||
use workspace::{DismissDecision, ModalView, Workspace};
|
||||
use gpui::{App, Context, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, Window};
|
||||
use ui::{prelude::*, IconPosition};
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::feedback::GiveFeedback;
|
||||
|
||||
use crate::{system_specs::SystemSpecs, OpenZedRepo};
|
||||
|
||||
// For UI testing purposes
|
||||
const SEND_SUCCESS_IN_DEV_MODE: bool = true;
|
||||
const SEND_TIME_IN_DEV_MODE: Duration = Duration::from_secs(2);
|
||||
|
||||
// Temporary, until tests are in place
|
||||
#[cfg(debug_assertions)]
|
||||
const DEV_MODE: bool = true;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEV_MODE: bool = false;
|
||||
|
||||
const DATABASE_KEY_NAME: &str = "email_address";
|
||||
static EMAIL_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap());
|
||||
const FEEDBACK_CHAR_LIMIT: RangeInclusive<i32> = 10..=5000;
|
||||
const FEEDBACK_SUBMISSION_ERROR_TEXT: &str =
|
||||
"Feedback failed to submit, see error log for details.";
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct FeedbackRequestBody<'a> {
|
||||
feedback_text: &'a str,
|
||||
email: Option<String>,
|
||||
installation_id: Option<Arc<str>>,
|
||||
metrics_id: Option<Arc<str>>,
|
||||
system_specs: SystemSpecs,
|
||||
is_staff: bool,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
struct InvalidStateFlags: u8 {
|
||||
const EmailAddress = 0b00000001;
|
||||
const CharacterCount = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum CannotSubmitReason {
|
||||
InvalidState { flags: InvalidStateFlags },
|
||||
AwaitingSubmission,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
enum SubmissionState {
|
||||
CanSubmit,
|
||||
CannotSubmit { reason: CannotSubmitReason },
|
||||
}
|
||||
use crate::{EmailZed, FileBugReport, OpenZedRepo, RequestFeature};
|
||||
|
||||
pub struct FeedbackModal {
|
||||
system_specs: SystemSpecs,
|
||||
feedback_editor: Entity<Editor>,
|
||||
email_address_editor: Entity<Editor>,
|
||||
submission_state: Option<SubmissionState>,
|
||||
dismiss_modal: bool,
|
||||
character_count: i32,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Focusable for FeedbackModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.feedback_editor.focus_handle(cx)
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for FeedbackModal {}
|
||||
|
||||
impl ModalView for FeedbackModal {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> DismissDecision {
|
||||
self.update_email_in_store(window, cx);
|
||||
|
||||
if self.dismiss_modal {
|
||||
return DismissDecision::Dismiss(true);
|
||||
}
|
||||
|
||||
let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
|
||||
if !has_feedback {
|
||||
return DismissDecision::Dismiss(true);
|
||||
}
|
||||
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Discard feedback?",
|
||||
None,
|
||||
&["Yes", "No"],
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if answer.await.ok() == Some(0) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.dismiss_modal = true;
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
DismissDecision::Pending
|
||||
}
|
||||
}
|
||||
impl ModalView for FeedbackModal {}
|
||||
|
||||
impl FeedbackModal {
|
||||
pub fn register(workspace: &mut Workspace, _: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let _handle = cx.entity().downgrade();
|
||||
workspace.register_action(move |workspace, _: &GiveFeedback, window, cx| {
|
||||
workspace
|
||||
.with_local_workspace(window, cx, |workspace, window, cx| {
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let system_specs = SystemSpecs::new(window, cx);
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let markdown = markdown.await.log_err();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer("", markdown, cx)
|
||||
})?;
|
||||
let system_specs = system_specs.await;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, move |window, cx| {
|
||||
FeedbackModal::new(system_specs, project, buffer, window, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
workspace.toggle_modal(window, cx, move |_, cx| FeedbackModal::new(cx));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
system_specs: SystemSpecs,
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let email_address_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Email address (optional)", cx);
|
||||
|
||||
if let Ok(Some(email_address)) = KEY_VALUE_STORE.read_kvp(DATABASE_KEY_NAME) {
|
||||
editor.set_text(email_address, window, cx)
|
||||
}
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let feedback_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), window, cx);
|
||||
editor.set_placeholder_text(
|
||||
"You can use markdown to organize your feedback with code and links.",
|
||||
cx,
|
||||
);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_soft_wrap();
|
||||
editor
|
||||
});
|
||||
|
||||
cx.subscribe(&feedback_editor, |this, editor, event: &EditorEvent, cx| {
|
||||
if matches!(event, EditorEvent::Edited { .. }) {
|
||||
this.character_count = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("Feedback editor is never a multi-buffer")
|
||||
.read(cx)
|
||||
.len() as i32;
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
system_specs: system_specs.clone(),
|
||||
feedback_editor,
|
||||
email_address_editor,
|
||||
submission_state: None,
|
||||
dismiss_modal: false,
|
||||
character_count: 0,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let feedback_text = self.feedback_editor.read(cx).text(cx).trim().to_string();
|
||||
let email = self.email_address_editor.read(cx).text_option(cx);
|
||||
|
||||
let answer = window.prompt(
|
||||
PromptLevel::Info,
|
||||
"Ready to submit your feedback?",
|
||||
None,
|
||||
&["Yes, Submit!", "No"],
|
||||
cx,
|
||||
);
|
||||
let client = Client::global(cx).clone();
|
||||
let specs = self.system_specs.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let answer = answer.await.ok();
|
||||
if answer == Some(0) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
|
||||
let res =
|
||||
FeedbackModal::submit_feedback(&feedback_text, email, client, specs).await;
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.dismiss_modal = true;
|
||||
cx.notify();
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("{}", error);
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let prompt = window.prompt(
|
||||
PromptLevel::Critical,
|
||||
FEEDBACK_SUBMISSION_ERROR_TEXT,
|
||||
None,
|
||||
&["OK"],
|
||||
cx,
|
||||
);
|
||||
cx.spawn_in(window, async move |_, _cx| {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
this.submission_state = Some(SubmissionState::CanSubmit);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
async fn submit_feedback(
|
||||
feedback_text: &str,
|
||||
email: Option<String>,
|
||||
zed_client: Arc<Client>,
|
||||
system_specs: SystemSpecs,
|
||||
) -> anyhow::Result<()> {
|
||||
if DEV_MODE {
|
||||
smol::Timer::after(SEND_TIME_IN_DEV_MODE).await;
|
||||
|
||||
if SEND_SUCCESS_IN_DEV_MODE {
|
||||
return Ok(());
|
||||
} else {
|
||||
return Err(anyhow!("Error submitting feedback"));
|
||||
}
|
||||
}
|
||||
|
||||
let telemetry = zed_client.telemetry();
|
||||
let installation_id = telemetry.installation_id();
|
||||
let metrics_id = telemetry.metrics_id();
|
||||
let is_staff = telemetry.is_staff();
|
||||
let http_client = zed_client.http_client();
|
||||
let feedback_endpoint = http_client.build_url("/api/feedback");
|
||||
let request = FeedbackRequestBody {
|
||||
feedback_text,
|
||||
email,
|
||||
installation_id,
|
||||
metrics_id,
|
||||
system_specs,
|
||||
is_staff: is_staff.unwrap_or(false),
|
||||
};
|
||||
let json_bytes = serde_json::to_vec(&request)?;
|
||||
let request = http_client::http::Request::post(feedback_endpoint)
|
||||
.header("content-type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
let mut response = http_client.send(request).await?;
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
let response_status = response.status();
|
||||
if !response_status.is_success() {
|
||||
bail!("Feedback API failed with error: {}", response_status)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_submission_state(&mut self, cx: &mut Context<Self>) {
|
||||
if self.awaiting_submission() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut invalid_state_flags = InvalidStateFlags::empty();
|
||||
|
||||
let valid_email_address = match self.email_address_editor.read(cx).text_option(cx) {
|
||||
Some(email_address) => EMAIL_REGEX.is_match(&email_address),
|
||||
None => true,
|
||||
};
|
||||
|
||||
if !valid_email_address {
|
||||
invalid_state_flags |= InvalidStateFlags::EmailAddress;
|
||||
}
|
||||
|
||||
if !FEEDBACK_CHAR_LIMIT.contains(&self.character_count) {
|
||||
invalid_state_flags |= InvalidStateFlags::CharacterCount;
|
||||
}
|
||||
|
||||
if invalid_state_flags.is_empty() {
|
||||
self.submission_state = Some(SubmissionState::CanSubmit);
|
||||
} else {
|
||||
self.submission_state = Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState {
|
||||
flags: invalid_state_flags,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn update_email_in_store(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let email = self.email_address_editor.read(cx).text_option(cx);
|
||||
|
||||
cx.spawn_in(window, async move |_, _| match email {
|
||||
Some(email) => {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(DATABASE_KEY_NAME.to_string(), email)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
None => {
|
||||
KEY_VALUE_STORE
|
||||
.delete_kvp(DATABASE_KEY_NAME.to_string())
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn valid_email_address(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::EmailAddress)
|
||||
}
|
||||
|
||||
fn valid_character_count(&self) -> bool {
|
||||
!self.in_invalid_state(InvalidStateFlags::CharacterCount)
|
||||
}
|
||||
|
||||
fn in_invalid_state(&self, flag: InvalidStateFlags) -> bool {
|
||||
match self.submission_state {
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::InvalidState { ref flags },
|
||||
}) => flags.contains(flag),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn awaiting_submission(&self) -> bool {
|
||||
matches!(
|
||||
self.submission_state,
|
||||
Some(SubmissionState::CannotSubmit {
|
||||
reason: CannotSubmitReason::AwaitingSubmission
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
fn can_submit(&self) -> bool {
|
||||
matches!(self.submission_state, Some(SubmissionState::CanSubmit))
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
@@ -423,118 +39,75 @@ impl FeedbackModal {
|
||||
|
||||
impl Render for FeedbackModal {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.update_submission_state(cx);
|
||||
|
||||
let submit_button_text = if self.awaiting_submission() {
|
||||
"Submitting..."
|
||||
} else {
|
||||
"Submit"
|
||||
};
|
||||
|
||||
let open_zed_repo =
|
||||
cx.listener(|_, _, window, cx| window.dispatch_action(Box::new(OpenZedRepo), cx));
|
||||
|
||||
v_flex()
|
||||
.elevation_3(cx)
|
||||
.key_context("GiveFeedback")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.min_w(rems(40.))
|
||||
.max_w(rems(96.))
|
||||
.h(rems(32.))
|
||||
.elevation_3(cx)
|
||||
.w_96()
|
||||
.h_auto()
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.child(Headline::new("Give Feedback"))
|
||||
.child(
|
||||
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
|
||||
format!(
|
||||
"Feedback must be at least {} characters.",
|
||||
FEEDBACK_CHAR_LIMIT.start()
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Characters: {}",
|
||||
*FEEDBACK_CHAR_LIMIT.end() - self.character_count
|
||||
)
|
||||
})
|
||||
.color(if self.valid_character_count() {
|
||||
Color::Success
|
||||
} else {
|
||||
Color::Error
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(self.feedback_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.p_2()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.border_color(if self.valid_email_address() {
|
||||
cx.theme().colors().border
|
||||
} else {
|
||||
cx.theme().status().error_border
|
||||
})
|
||||
.child(self.email_address_editor.clone()),
|
||||
)
|
||||
.child(
|
||||
Label::new("Provide an email address if you want us to be able to reply.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(Headline::new("Give Feedback"))
|
||||
.child(
|
||||
Button::new("zed_repository", "Zed Repository")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.icon(IconName::ExternalLink)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(open_zed_repo),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel_feedback", "Cancel")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
})
|
||||
.detach();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("submit_feedback", submit_button_text)
|
||||
.color(Color::Accent)
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.submit(window, cx).detach();
|
||||
}))
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::simple("Submit feedback to the Zed team.", cx)
|
||||
})
|
||||
.when(!self.can_submit(), |this| this.disabled(true)),
|
||||
),
|
||||
IconButton::new("close-btn", IconName::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
})
|
||||
.detach();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(Label::new("Thanks for using Zed! To share your experience with us, reach for the channel that's the most appropriate:"))
|
||||
.child(
|
||||
Button::new("file-a-bug-report", "File a Bug Report")
|
||||
.full_width()
|
||||
.icon(IconName::Debug)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(FileBugReport), cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("request-a-feature", "Request a Feature")
|
||||
.full_width()
|
||||
.icon(IconName::Sparkle)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(RequestFeature), cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("send-us_an-email", "Send an Email")
|
||||
.full_width()
|
||||
.icon(IconName::Envelope)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(Box::new(EmailZed), cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("zed_repository", "GitHub Repository")
|
||||
.full_width()
|
||||
.icon(IconName::Github)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(open_zed_repo),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Testing of various button states, dismissal prompts, etc. :)
|
||||
|
||||
@@ -202,7 +202,7 @@ fn paint_line(
|
||||
|
||||
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
|
||||
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
|
||||
if glyph_ix == 0 {
|
||||
if glyph_ix == 0 && run_ix == 0 {
|
||||
first_glyph_x = glyph_origin.x;
|
||||
}
|
||||
|
||||
|
||||
@@ -1645,7 +1645,11 @@ impl MultiBuffer {
|
||||
};
|
||||
let locator = snapshot.excerpt_locator_for_id(*existing);
|
||||
excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &());
|
||||
let existing_excerpt = excerpts_cursor.item().unwrap();
|
||||
let Some(existing_excerpt) = excerpts_cursor.item() else {
|
||||
to_remove.push(existing_iter.next().unwrap());
|
||||
to_insert.push(new_iter.next().unwrap());
|
||||
continue;
|
||||
};
|
||||
if existing_excerpt.buffer_id != buffer_snapshot.remote_id() {
|
||||
to_remove.push(existing_iter.next().unwrap());
|
||||
to_insert.push(new_iter.next().unwrap());
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -704,7 +704,7 @@ pub(super) async fn format_with_prettier(
|
||||
prettier_store: &WeakEntity<PrettierStore>,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Result<crate::lsp_store::FormatOperation>> {
|
||||
) -> Option<Result<language::Diff>> {
|
||||
let prettier_instance = prettier_store
|
||||
.update(cx, |prettier_store, cx| {
|
||||
prettier_store.prettier_instance_for_buffer(buffer, cx)
|
||||
@@ -738,7 +738,6 @@ pub(super) async fn format_with_prettier(
|
||||
let format_result = prettier
|
||||
.format(buffer, buffer_path, ignore_dir, cx)
|
||||
.await
|
||||
.map(crate::lsp_store::FormatOperation::Prettier)
|
||||
.with_context(|| format!("{} failed to format buffer", prettier_description));
|
||||
|
||||
Some(format_result)
|
||||
|
||||
@@ -4699,19 +4699,6 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_code_actions(code_actions: &HashMap<String, bool>) -> Vec<lsp::CodeActionKind> {
|
||||
code_actions
|
||||
.iter()
|
||||
.flat_map(|(kind, enabled)| {
|
||||
if *enabled {
|
||||
Some(kind.clone().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub struct PathMatchCandidateSet {
|
||||
pub snapshot: Snapshot,
|
||||
pub include_ignored: bool,
|
||||
|
||||
@@ -24,7 +24,7 @@ use util::ResultExt;
|
||||
use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
|
||||
|
||||
use crate::{
|
||||
task_store::TaskStore,
|
||||
task_store::{TaskSettingsLocation, TaskStore},
|
||||
worktree_store::{WorktreeStore, WorktreeStoreEvent},
|
||||
};
|
||||
|
||||
@@ -642,7 +642,7 @@ impl SettingsObserver {
|
||||
LocalSettingsKind::Tasks(task_kind) => task_store.update(cx, |task_store, cx| {
|
||||
task_store
|
||||
.update_user_tasks(
|
||||
Some(SettingsLocation {
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id,
|
||||
path: directory.as_ref(),
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::format_collect)]
|
||||
|
||||
use crate::{task_inventory::TaskContexts, Event, *};
|
||||
use crate::{task_inventory::TaskContexts, task_store::TaskSettingsLocation, Event, *};
|
||||
use buffer_diff::{
|
||||
assert_hunks, BufferDiffEvent, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind,
|
||||
};
|
||||
@@ -19,6 +19,7 @@ use lsp::{
|
||||
NumberOrString, TextDocumentEdit, WillRenameFiles,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use paths::tasks_file;
|
||||
use pretty_assertions::{assert_eq, assert_matches};
|
||||
use serde_json::json;
|
||||
#[cfg(not(windows))]
|
||||
@@ -327,7 +328,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
|
||||
inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task);
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
None,
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(
|
||||
&json!([{
|
||||
"label": "cargo check unstable",
|
||||
|
||||
@@ -13,7 +13,7 @@ use collections::{HashMap, HashSet, VecDeque};
|
||||
use gpui::{App, AppContext as _, Entity, SharedString, Task};
|
||||
use itertools::Itertools;
|
||||
use language::{ContextProvider, File, Language, LanguageToolchainStore, Location};
|
||||
use settings::{parse_json_with_comments, SettingsLocation, TaskKind};
|
||||
use settings::{parse_json_with_comments, TaskKind};
|
||||
use task::{
|
||||
DebugTaskDefinition, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
|
||||
TaskVariables, VariableName,
|
||||
@@ -22,7 +22,7 @@ use text::{Point, ToPoint};
|
||||
use util::{paths::PathExt as _, post_inc, NumericPrefixWithSuffix, ResultExt as _};
|
||||
use worktree::WorktreeId;
|
||||
|
||||
use crate::worktree_store::WorktreeStore;
|
||||
use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
|
||||
|
||||
/// Inventory tracks available tasks for a given project.
|
||||
#[derive(Debug, Default)]
|
||||
@@ -33,7 +33,7 @@ pub struct Inventory {
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ParsedTemplates {
|
||||
global: Vec<TaskTemplate>,
|
||||
global: HashMap<PathBuf, Vec<TaskTemplate>>,
|
||||
worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,
|
||||
}
|
||||
|
||||
@@ -324,22 +324,20 @@ impl Inventory {
|
||||
) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
|
||||
self.templates_from_settings
|
||||
.global
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|template| {
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: match template.task_type {
|
||||
task::TaskType::Script => Cow::Borrowed("global tasks.json"),
|
||||
task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
|
||||
.iter()
|
||||
.flat_map(|(file_path, templates)| {
|
||||
templates.into_iter().map(|template| {
|
||||
(
|
||||
TaskSourceKind::AbsPath {
|
||||
id_base: match template.task_type {
|
||||
task::TaskType::Script => Cow::Borrowed("global tasks.json"),
|
||||
task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
|
||||
},
|
||||
abs_path: file_path.clone(),
|
||||
},
|
||||
abs_path: match template.task_type {
|
||||
task::TaskType::Script => paths::tasks_file().clone(),
|
||||
task::TaskType::Debug(_) => paths::debug_tasks_file().clone(),
|
||||
},
|
||||
},
|
||||
template,
|
||||
)
|
||||
template.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -377,7 +375,7 @@ impl Inventory {
|
||||
/// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
|
||||
pub(crate) fn update_file_based_tasks(
|
||||
&mut self,
|
||||
location: Option<SettingsLocation<'_>>,
|
||||
location: TaskSettingsLocation<'_>,
|
||||
raw_tasks_json: Option<&str>,
|
||||
task_kind: TaskKind,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -395,7 +393,13 @@ impl Inventory {
|
||||
|
||||
let parsed_templates = &mut self.templates_from_settings;
|
||||
match location {
|
||||
Some(location) => {
|
||||
TaskSettingsLocation::Global(path) => {
|
||||
parsed_templates
|
||||
.global
|
||||
.entry(path.to_owned())
|
||||
.insert_entry(new_templates.collect());
|
||||
}
|
||||
TaskSettingsLocation::Worktree(location) => {
|
||||
let new_templates = new_templates.collect::<Vec<_>>();
|
||||
if new_templates.is_empty() {
|
||||
if let Some(worktree_tasks) =
|
||||
@@ -411,8 +415,8 @@ impl Inventory {
|
||||
.insert((Arc::from(location.path), task_kind), new_templates);
|
||||
}
|
||||
}
|
||||
None => parsed_templates.global = new_templates.collect(),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -651,8 +655,10 @@ impl ContextProvider for ContextProviderWithTasks {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use paths::tasks_file;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use settings::SettingsLocation;
|
||||
|
||||
use crate::task_store::TaskStore;
|
||||
|
||||
@@ -684,7 +690,7 @@ mod tests {
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
None,
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
expected_initial_state.iter().map(|name| name.as_str()),
|
||||
)),
|
||||
@@ -738,7 +744,7 @@ mod tests {
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
None,
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
["10_hello", "11_hello"]
|
||||
.into_iter()
|
||||
@@ -863,7 +869,7 @@ mod tests {
|
||||
inventory.update(cx, |inventory, _| {
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
None,
|
||||
TaskSettingsLocation::Global(tasks_file()),
|
||||
Some(&mock_tasks_from_names(
|
||||
worktree_independent_tasks
|
||||
.iter()
|
||||
@@ -874,7 +880,7 @@ mod tests {
|
||||
.unwrap();
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
Some(SettingsLocation {
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id: worktree_1,
|
||||
path: Path::new(".zed"),
|
||||
}),
|
||||
@@ -886,7 +892,7 @@ mod tests {
|
||||
.unwrap();
|
||||
inventory
|
||||
.update_file_based_tasks(
|
||||
Some(SettingsLocation {
|
||||
TaskSettingsLocation::Worktree(SettingsLocation {
|
||||
worktree_id: worktree_2,
|
||||
path: Path::new(".zed"),
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use collections::HashMap;
|
||||
@@ -48,6 +51,12 @@ enum StoreMode {
|
||||
|
||||
impl EventEmitter<crate::Event> for TaskStore {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TaskSettingsLocation<'a> {
|
||||
Global(&'a Path),
|
||||
Worktree(SettingsLocation<'a>),
|
||||
}
|
||||
|
||||
impl TaskStore {
|
||||
pub fn init(client: Option<&AnyProtoClient>) {
|
||||
if let Some(client) = client {
|
||||
@@ -286,7 +295,7 @@ impl TaskStore {
|
||||
|
||||
pub(super) fn update_user_tasks(
|
||||
&self,
|
||||
location: Option<SettingsLocation<'_>>,
|
||||
location: TaskSettingsLocation<'_>,
|
||||
raw_tasks_json: Option<&str>,
|
||||
task_type: TaskKind,
|
||||
cx: &mut Context<'_, Self>,
|
||||
@@ -310,13 +319,19 @@ impl TaskStore {
|
||||
file_path: PathBuf,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) -> Task<()> {
|
||||
let mut user_tasks_file_rx = watch_config_file(&cx.background_executor(), fs, file_path);
|
||||
let mut user_tasks_file_rx =
|
||||
watch_config_file(&cx.background_executor(), fs, file_path.clone());
|
||||
let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
|
||||
cx.spawn(async move |task_store, cx| {
|
||||
if let Some(user_tasks_content) = user_tasks_content {
|
||||
let Ok(_) = task_store.update(cx, |task_store, cx| {
|
||||
task_store
|
||||
.update_user_tasks(None, Some(&user_tasks_content), task_kind, cx)
|
||||
.update_user_tasks(
|
||||
TaskSettingsLocation::Global(&file_path),
|
||||
Some(&user_tasks_content),
|
||||
task_kind,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
}) else {
|
||||
return;
|
||||
@@ -325,7 +340,7 @@ impl TaskStore {
|
||||
while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
|
||||
let Ok(()) = task_store.update(cx, |task_store, cx| {
|
||||
let result = task_store.update_user_tasks(
|
||||
None,
|
||||
TaskSettingsLocation::Global(&file_path),
|
||||
Some(&user_tasks_content),
|
||||
task_kind,
|
||||
cx,
|
||||
|
||||
@@ -98,7 +98,8 @@ impl ThemeStyleContent {
|
||||
/// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeContent`].
|
||||
#[inline(always)]
|
||||
pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement {
|
||||
self.colors.theme_colors_refinement()
|
||||
self.colors
|
||||
.theme_colors_refinement(&self.status_colors_refinement())
|
||||
}
|
||||
|
||||
/// Returns a [`StatusColorsRefinement`] based on the colors in the [`ThemeContent`].
|
||||
@@ -589,7 +590,10 @@ pub struct ThemeColorsContent {
|
||||
|
||||
impl ThemeColorsContent {
|
||||
/// Returns a [`ThemeColorsRefinement`] based on the colors in the [`ThemeColorsContent`].
|
||||
pub fn theme_colors_refinement(&self) -> ThemeColorsRefinement {
|
||||
pub fn theme_colors_refinement(
|
||||
&self,
|
||||
status_colors: &StatusColorsRefinement,
|
||||
) -> ThemeColorsRefinement {
|
||||
let border = self
|
||||
.border
|
||||
.as_ref()
|
||||
@@ -1000,27 +1004,39 @@ impl ThemeColorsContent {
|
||||
version_control_added: self
|
||||
.version_control_added
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `created`, for backwards compatibility.
|
||||
.or(status_colors.created),
|
||||
version_control_deleted: self
|
||||
.version_control_deleted
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `deleted`, for backwards compatibility.
|
||||
.or(status_colors.deleted),
|
||||
version_control_modified: self
|
||||
.version_control_modified
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `modified`, for backwards compatibility.
|
||||
.or(status_colors.modified),
|
||||
version_control_renamed: self
|
||||
.version_control_renamed
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `modified`, for backwards compatibility.
|
||||
.or(status_colors.modified),
|
||||
version_control_conflict: self
|
||||
.version_control_conflict
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `ignored`, for backwards compatibility.
|
||||
.or(status_colors.ignored),
|
||||
version_control_ignored: self
|
||||
.version_control_ignored
|
||||
.as_ref()
|
||||
.and_then(|color| try_parse_color(color).ok()),
|
||||
.and_then(|color| try_parse_color(color).ok())
|
||||
// Fall back to `conflict`, for backwards compatibility.
|
||||
.or(status_colors.ignored),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.179.0"
|
||||
version = "0.179.2"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
Reference in New Issue
Block a user