Compare commits

...

15 Commits

Author SHA1 Message Date
Joseph T. Lyons
f6f061838d v0.179.x stable 2025-03-26 11:14:49 -04:00
gcp-cherry-pick-bot[bot]
7fe42e860f Fix strikethrough and underline in Linux (cherry-pick #27267) (#27380)
Cherry-picked Fix strikethrough and underline in Linux (#27267)

Follow up to #26827 and #24721, which introduced a bug in Linux.

|before|now|
|---|---|


|![image](https://github.com/user-attachments/assets/6471502d-bf92-4808-ad42-9e0c66569d4f)|!![image](https://github.com/user-attachments/assets/ae45510a-8bc9-4f89-90a0-7496842fecb6)|


Release Notes:

- N/A

Co-authored-by: Jason Lee <huacnlee@gmail.com>

Co-authored-by: João Marcos <marcospb19@hotmail.com>
Co-authored-by: Jason Lee <huacnlee@gmail.com>
2025-03-25 14:40:42 +02:00
gcp-cherry-pick-bot[bot]
3386e34f1a Don't assume that the excerpt can be found (cherry-pick #27395) (#27398)
Cherry-picked Don't assume that the excerpt can be found (#27395)

Release Notes:

- Fix (rare) panic in the project diff view

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-03-25 14:37:56 +02:00
Joseph T. Lyons
087f58206a zed 0.179.2 2025-03-21 15:50:39 -04:00
Conrad Irwin
bdd39bb012 Bump up default timeout (#27250)
Release Notes:

- Extended timeout used when connecting to remote instances
2025-03-21 15:48:42 -04:00
Nate Butler
f24086ecf8 editor: Fix regression in git label colors due to status color changes (#27272)
This PR fixes the new awkward-looking git status labels due to the
change in version control colors. We want to enable styling version
control colors distinctly from other statuses, but these colors aren't
great for labels as they are meant to be quite high contrast.

We may need to split version control colors into a primary color and a
text color if we want to improve theming this overall.

| Before | After |
|--------|-------|
| ![CleanShot 2025-03-21 at 14 12
22@2x](https://github.com/user-attachments/assets/fadb93b1-06b6-44cc-bf16-7e1279166ed0)
| ![CleanShot 2025-03-21 at 14 12
49@2x](https://github.com/user-attachments/assets/262ffc23-60b9-4cee-8a2b-9e864130912f)
|

Release Notes:

- Fixes a regression in git status colors in the project panel
2025-03-21 14:52:52 -04:00
Joseph T. Lyons
c86019ec68 Pre-fill body of email with system specs (#27210)
I think we still want to be able to easily capture system spec info from
users. They can decide if they want to include it or not.

Release Notes:

- N/A
2025-03-20 14:35:09 -04:00
Zed Bot
64892107e1 Bump to 0.179.1 for @SomeoneToIgnore 2025-03-20 17:07:39 +00:00
gcp-cherry-pick-bot[bot]
3c538f40d3 Fix code action formatters creating separate transaction (cherry-pick #26311) (#27198)
Cherry-picked Fix code action formatters creating separate transaction
(#26311)

Closes #24588
Closes #25419

Restructures `LspStore.format_local` a decent bit in order to make how
the transaction history is preserved more clear, and in doing so fix
various bugs with how the transaction history is handled during a format
request (especially when formatting in remote dev)

Release Notes:

- Fixed an issue that prevented formatting from working when working
with remote dev
- Fixed an issue when using code actions as a format step where the
edits made by the code actions would not be grouped with the other
format edits in the undo history

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-03-20 10:30:20 -05:00
Joseph T. Lyons
494cccf3fc Rework feedback modal (#27186)
After our last community sync, we came to the conclusion that feedback
being sent outside of email is difficult to reply to. Our decision was
to use the old, tried and true email system, so that we can better
respond to people asking questions.

<img width="392" alt="SCR-20250320-igub"
src="https://github.com/user-attachments/assets/f1d01771-30eb-4b6f-b031-c68ddaac5700"
/>

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
2025-03-20 10:34:22 -04:00
gcp-cherry-pick-bot[bot]
e840961f1c debugger: Ensure both debug and regular global tasks are correctly merged (cherry-pick #27184) (#27187)
Cherry-picked debugger: Ensure both debug and regular global tasks are
correctly merged (#27184)

Follow-up of https://github.com/zed-industries/zed/pull/13433
Closes https://github.com/zed-industries/zed/issues/27124
Closes https://github.com/zed-industries/zed/issues/27066

After this change, both old global task source, `tasks.json` and new,
`debug.json` started to call for the same task update method:



14920ab910/crates/project/src/task_inventory.rs (L414)

erasing previous declarations.

The PR puts this data under different paths instead and adjusts the code
around it.

Release Notes:

- Fixed custom tasks not shown

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
2025-03-20 15:42:21 +02:00
Peter Tripp
bfbbe881c3 ci: Send emails for weekly release (#27102)
Release Notes:

- N/A
2025-03-19 16:19:12 -04:00
gcp-cherry-pick-bot[bot]
e2fc17d486 theme: Add fallback colors for version_control.<variant> properties (cherry-pick #27104) (#27106)
Cherry-picked theme: Add fallback colors for `version_control.<variant>`
properties (#27104)

This PR adds fallback colors for the `version_control.<variant>` theme
properties.

This fixes the colors when themes do not provide the properties.

Related to  https://github.com/zed-industries/zed/pull/26951.

Release Notes:

- Added fallback colors for the `version_control.<variant>` theme
properties.

Co-authored-by: Marshall Bowers <git@maxdeviant.com>
2025-03-19 15:11:59 -04:00
Joseph T. Lyons
9b75c88498 Fix release notes API call with heredoc syntax (#27096)
Release Notes:

- N/A
2025-03-19 13:51:20 -04:00
Joseph T. Lyons
f2c27bdcc3 v0.179.x preview 2025-03-19 11:32:35 -04:00
19 changed files with 798 additions and 934 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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();

View File

@@ -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. :)

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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(),
}),

View File

@@ -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",

View File

@@ -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"),
}),

View File

@@ -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,

View File

@@ -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),
}
}
}

View File

@@ -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>"]

View File

@@ -1 +1 @@
dev
stable