Compare commits
21 Commits
gpui-butto
...
v0.179.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
363c205ec6 | ||
|
|
656d4a7104 | ||
|
|
d637307bd7 | ||
|
|
d0398a5f80 | ||
|
|
83d9f19234 | ||
|
|
5c2fe76d48 | ||
|
|
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.5"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
//
|
||||
// Default: not set, defaults to "bar"
|
||||
"cursor_shape": null,
|
||||
// Determines whether the mouse cursor is hidden when typing in an editor or input box.
|
||||
"hide_mouse_while_typing": true,
|
||||
// How to highlight the current line in the editor.
|
||||
//
|
||||
// 1. Don't highlight the current line:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +570,7 @@ async fn test_following_tab_order(
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
util::path!("/a"),
|
||||
json!({
|
||||
"1.txt": "one",
|
||||
"2.txt": "two",
|
||||
@@ -578,7 +578,7 @@ async fn test_following_tab_order(
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(util::path!("/a"), cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
|
||||
@@ -777,6 +777,8 @@ pub struct Editor {
|
||||
toggle_fold_multiple_buffers: Task<()>,
|
||||
_scroll_cursor_center_top_bottom_task: Task<()>,
|
||||
serialize_selections: Task<()>,
|
||||
mouse_cursor_hidden: bool,
|
||||
hide_mouse_while_typing: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
|
||||
@@ -1516,6 +1518,10 @@ impl Editor {
|
||||
serialize_selections: Task::ready(()),
|
||||
text_style_refinement: None,
|
||||
load_diff_task: load_uncommitted_diff,
|
||||
mouse_cursor_hidden: false,
|
||||
hide_mouse_while_typing: EditorSettings::get_global(cx)
|
||||
.hide_mouse_while_typing
|
||||
.unwrap_or(true),
|
||||
};
|
||||
if let Some(breakpoints) = this.breakpoint_store.as_ref() {
|
||||
this._subscriptions
|
||||
@@ -2911,6 +2917,8 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
@@ -3316,6 +3324,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
@@ -3431,6 +3440,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -3488,6 +3499,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -7637,6 +7650,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.select_autoclose_pair(window, cx);
|
||||
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
|
||||
@@ -7735,6 +7749,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
@@ -7756,7 +7771,7 @@ impl Editor {
|
||||
if self.move_to_prev_snippet_tabstop(window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.outdent(&Outdent, window, cx);
|
||||
}
|
||||
|
||||
@@ -7764,7 +7779,7 @@ impl Editor {
|
||||
if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
let mut selections = self.selections.all_adjusted(cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -16357,6 +16372,11 @@ impl Editor {
|
||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||
self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default();
|
||||
self.hide_mouse_while_typing = editor_settings.hide_mouse_while_typing.unwrap_or(true);
|
||||
|
||||
if !self.hide_mouse_while_typing {
|
||||
self.mouse_cursor_hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
if old_cursor_shape != self.cursor_shape {
|
||||
|
||||
@@ -37,6 +37,7 @@ pub struct EditorSettings {
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
pub jupyter: Jupyter,
|
||||
pub hide_mouse_while_typing: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -271,6 +272,10 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: None
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
/// Determines whether the mouse cursor should be hidden while typing in an editor or input box.
|
||||
///
|
||||
/// Default: true
|
||||
pub hide_mouse_while_typing: Option<bool>,
|
||||
/// How to highlight the current line in the editor.
|
||||
///
|
||||
/// Default: all
|
||||
|
||||
@@ -890,6 +890,7 @@ impl EditorElement {
|
||||
let modifiers = event.modifiers;
|
||||
let gutter_hovered = gutter_hitbox.is_hovered(window);
|
||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||
editor.mouse_cursor_hidden = false;
|
||||
|
||||
if gutter_hovered {
|
||||
editor.gutter_breakpoint_indicator = Some(
|
||||
@@ -4384,7 +4385,7 @@ impl EditorElement {
|
||||
let is_singleton = self.editor.read(cx).is_singleton(cx);
|
||||
|
||||
let line_height = layout.position_map.line_height;
|
||||
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
|
||||
|
||||
for LineNumberLayout {
|
||||
shaped_line,
|
||||
@@ -4417,9 +4418,9 @@ impl EditorElement {
|
||||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||||
if is_singleton {
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4639,7 +4640,7 @@ impl EditorElement {
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
{
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4711,18 +4712,24 @@ impl EditorElement {
|
||||
bounds: layout.position_map.text_hitbox.bounds,
|
||||
}),
|
||||
|window| {
|
||||
let cursor_style = if self
|
||||
.editor
|
||||
.read(cx)
|
||||
let editor = self.editor.read(cx);
|
||||
if editor.mouse_cursor_hidden {
|
||||
window.set_cursor_style(CursorStyle::None, None);
|
||||
} else if editor
|
||||
.hovered_link_state
|
||||
.as_ref()
|
||||
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
|
||||
{
|
||||
CursorStyle::PointingHand
|
||||
window.set_cursor_style(
|
||||
CursorStyle::PointingHand,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
} else {
|
||||
CursorStyle::IBeam
|
||||
window.set_cursor_style(
|
||||
CursorStyle::IBeam,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||
|
||||
self.paint_lines_background(layout, window, cx);
|
||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||
@@ -4896,7 +4903,7 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
|
||||
|
||||
window.on_mouse_event({
|
||||
let editor = self.editor.clone();
|
||||
@@ -5056,7 +5063,7 @@ impl EditorElement {
|
||||
});
|
||||
}
|
||||
|
||||
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
|
||||
|
||||
window.on_mouse_event({
|
||||
let editor = self.editor.clone();
|
||||
@@ -6794,6 +6801,7 @@ impl Element for EditorElement {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
// Offset the content_bounds from the text_bounds by the gutter margin (which
|
||||
// is roughly half a character wide) to make hit testing work more like how we want.
|
||||
let content_origin =
|
||||
|
||||
@@ -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. :)
|
||||
|
||||
@@ -314,6 +314,8 @@ pub enum Model {
|
||||
Gemini20FlashThinking,
|
||||
#[serde(rename = "gemini-2.0-flash-lite-preview")]
|
||||
Gemini20FlashLite,
|
||||
#[serde(rename = "gemini-2.5-pro-exp-03-25")]
|
||||
Gemini25ProExp0325,
|
||||
#[serde(rename = "custom")]
|
||||
Custom {
|
||||
name: String,
|
||||
@@ -332,6 +334,7 @@ impl Model {
|
||||
Model::Gemini20Flash => "gemini-2.0-flash",
|
||||
Model::Gemini20FlashThinking => "gemini-2.0-flash-thinking-exp",
|
||||
Model::Gemini20FlashLite => "gemini-2.0-flash-lite-preview",
|
||||
Model::Gemini25ProExp0325 => "gemini-2.5-pro-exp-03-25",
|
||||
Model::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
@@ -344,6 +347,7 @@ impl Model {
|
||||
Model::Gemini20Flash => "Gemini 2.0 Flash",
|
||||
Model::Gemini20FlashThinking => "Gemini 2.0 Flash Thinking",
|
||||
Model::Gemini20FlashLite => "Gemini 2.0 Flash Lite",
|
||||
Model::Gemini25ProExp0325 => "Gemini 2.5 Pro Exp",
|
||||
Self::Custom {
|
||||
name, display_name, ..
|
||||
} => display_name.as_ref().unwrap_or(name),
|
||||
@@ -358,6 +362,7 @@ impl Model {
|
||||
Model::Gemini20Flash => 1_000_000,
|
||||
Model::Gemini20FlashThinking => 1_000_000,
|
||||
Model::Gemini20FlashLite => 1_000_000,
|
||||
Model::Gemini25ProExp0325 => 1_000_000,
|
||||
Model::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Render for WindowShadow {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1617,7 +1617,7 @@ impl Interactivity {
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(mouse_cursor) = style.mouse_cursor {
|
||||
window.set_cursor_style(mouse_cursor, hitbox);
|
||||
window.set_cursor_style(mouse_cursor, Some(hitbox));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -705,7 +705,7 @@ impl Element for InteractiveText {
|
||||
.iter()
|
||||
.any(|range| range.contains(&ix))
|
||||
{
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1228,6 +1228,9 @@ pub enum CursorStyle {
|
||||
/// A cursor indicating that the operation will result in a context menu
|
||||
/// corresponds to the CSS cursor value `context-menu`
|
||||
ContextualMenu,
|
||||
|
||||
/// Hide the cursor
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for CursorStyle {
|
||||
|
||||
@@ -666,6 +666,12 @@ impl CursorStyle {
|
||||
CursorStyle::DragLink => "alias",
|
||||
CursorStyle::DragCopy => "copy",
|
||||
CursorStyle::ContextualMenu => "context-menu",
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
"default"
|
||||
}
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -35,6 +35,12 @@ impl CursorStyle {
|
||||
CursorStyle::DragLink => Shape::Alias,
|
||||
CursorStyle::DragCopy => Shape::Copy,
|
||||
CursorStyle::ContextualMenu => Shape::ContextMenu,
|
||||
CursorStyle::None => {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!("CursorStyle::None should be handled separately in the client");
|
||||
#[cfg(not(debug_assertions))]
|
||||
Shape::Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +667,13 @@ impl LinuxClient for WaylandClient {
|
||||
let serial = state.serial_tracker.get(SerialKind::MouseEnter);
|
||||
state.cursor_style = Some(style);
|
||||
|
||||
if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
if let CursorStyle::None = style {
|
||||
let wl_pointer = state
|
||||
.wl_pointer
|
||||
.clone()
|
||||
.expect("window is focused by pointer");
|
||||
wl_pointer.set_cursor(serial, None, 0, 0);
|
||||
} else if let Some(cursor_shape_device) = &state.cursor_shape_device {
|
||||
cursor_shape_device.set_shape(serial, style.to_shape());
|
||||
} else if let Some(focused_window) = &state.mouse_focused_window {
|
||||
// cursor-shape-v1 isn't supported, set the cursor using a surface.
|
||||
|
||||
@@ -1438,13 +1438,16 @@ impl LinuxClient for X11Client {
|
||||
let cursor = match state.cursor_cache.get(&style) {
|
||||
Some(cursor) => *cursor,
|
||||
None => {
|
||||
let Some(cursor) = state
|
||||
.cursor_handle
|
||||
.load_cursor(&state.xcb_connection, &style.to_icon_name())
|
||||
.log_err()
|
||||
else {
|
||||
let Some(cursor) = (match style {
|
||||
CursorStyle::None => create_invisible_cursor(&state.xcb_connection).log_err(),
|
||||
_ => state
|
||||
.cursor_handle
|
||||
.load_cursor(&state.xcb_connection, &style.to_icon_name())
|
||||
.log_err(),
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
state.cursor_cache.insert(style, cursor);
|
||||
cursor
|
||||
}
|
||||
@@ -1938,3 +1941,19 @@ fn make_scroll_wheel_event(
|
||||
touch_phase: TouchPhase::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_invisible_cursor(
|
||||
connection: &XCBConnection,
|
||||
) -> anyhow::Result<crate::platform::linux::x11::client::xproto::Cursor> {
|
||||
let empty_pixmap = connection.generate_id()?;
|
||||
let root = connection.setup().roots[0].root;
|
||||
connection.create_pixmap(1, empty_pixmap, root, 1, 1)?;
|
||||
|
||||
let cursor = connection.generate_id()?;
|
||||
connection.create_cursor(cursor, empty_pixmap, empty_pixmap, 0, 0, 0, 0, 0, 0, 0, 0)?;
|
||||
|
||||
connection.free_pixmap(empty_pixmap)?;
|
||||
|
||||
connection.flush()?;
|
||||
Ok(cursor)
|
||||
}
|
||||
|
||||
@@ -891,6 +891,11 @@ impl Platform for MacPlatform {
|
||||
/// in macOS's [NSCursor](https://developer.apple.com/documentation/appkit/nscursor).
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
unsafe {
|
||||
if style == CursorStyle::None {
|
||||
let _: () = msg_send![class!(NSCursor), setHiddenUntilMouseMoves:YES];
|
||||
return;
|
||||
}
|
||||
|
||||
let new_cursor: id = match style {
|
||||
CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor],
|
||||
CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor],
|
||||
@@ -925,6 +930,7 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::DragLink => msg_send![class!(NSCursor), dragLinkCursor],
|
||||
CursorStyle::DragCopy => msg_send![class!(NSCursor), dragCopyCursor],
|
||||
CursorStyle::ContextualMenu => msg_send![class!(NSCursor), contextualMenuCursor],
|
||||
CursorStyle::None => unreachable!(),
|
||||
};
|
||||
|
||||
let old_cursor: id = msg_send![class!(NSCursor), currentCursor];
|
||||
|
||||
@@ -1127,7 +1127,19 @@ fn handle_nc_mouse_up_msg(
|
||||
}
|
||||
|
||||
fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
state_ptr.state.borrow_mut().current_cursor = HCURSOR(lparam.0 as _);
|
||||
let mut state = state_ptr.state.borrow_mut();
|
||||
let had_cursor = state.current_cursor.is_some();
|
||||
|
||||
state.current_cursor = if lparam.0 == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(HCURSOR(lparam.0 as _))
|
||||
};
|
||||
|
||||
if had_cursor != state.current_cursor.is_some() {
|
||||
unsafe { SetCursor(state.current_cursor) };
|
||||
}
|
||||
|
||||
Some(0)
|
||||
}
|
||||
|
||||
@@ -1138,7 +1150,9 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
unsafe { SetCursor(Some(state_ptr.state.borrow().current_cursor)) };
|
||||
unsafe {
|
||||
SetCursor(state_ptr.state.borrow().current_cursor);
|
||||
};
|
||||
Some(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ pub(crate) struct WindowsPlatformState {
|
||||
menus: Vec<OwnedMenu>,
|
||||
dock_menu_actions: Vec<Box<dyn Action>>,
|
||||
// NOTE: standard cursor handles don't need to close.
|
||||
pub(crate) current_cursor: HCURSOR,
|
||||
pub(crate) current_cursor: Option<HCURSOR>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -558,11 +558,11 @@ impl Platform for WindowsPlatform {
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let hcursor = load_cursor(style);
|
||||
let mut lock = self.state.borrow_mut();
|
||||
if lock.current_cursor.0 != hcursor.0 {
|
||||
if lock.current_cursor.map(|c| c.0) != hcursor.map(|c| c.0) {
|
||||
self.post_message(
|
||||
WM_GPUI_CURSOR_STYLE_CHANGED,
|
||||
WPARAM(0),
|
||||
LPARAM(hcursor.0 as isize),
|
||||
LPARAM(hcursor.map_or(0, |c| c.0 as isize)),
|
||||
);
|
||||
lock.current_cursor = hcursor;
|
||||
}
|
||||
@@ -683,7 +683,7 @@ impl Drop for WindowsPlatform {
|
||||
pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) icon: HICON,
|
||||
pub(crate) executor: ForegroundExecutor,
|
||||
pub(crate) current_cursor: HCURSOR,
|
||||
pub(crate) current_cursor: Option<HCURSOR>,
|
||||
pub(crate) windows_version: WindowsVersion,
|
||||
pub(crate) validation_number: usize,
|
||||
pub(crate) main_receiver: flume::Receiver<Runnable>,
|
||||
|
||||
@@ -106,7 +106,7 @@ pub(crate) fn windows_credentials_target_name(url: &str) -> String {
|
||||
format!("zed:url={}", url)
|
||||
}
|
||||
|
||||
pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
|
||||
pub(crate) fn load_cursor(style: CursorStyle) -> Option<HCURSOR> {
|
||||
static ARROW: OnceLock<SafeCursor> = OnceLock::new();
|
||||
static IBEAM: OnceLock<SafeCursor> = OnceLock::new();
|
||||
static CROSS: OnceLock<SafeCursor> = OnceLock::new();
|
||||
@@ -127,17 +127,20 @@ pub(crate) fn load_cursor(style: CursorStyle) -> HCURSOR {
|
||||
| CursorStyle::ResizeUpDown
|
||||
| CursorStyle::ResizeRow => (&SIZENS, IDC_SIZENS),
|
||||
CursorStyle::OperationNotAllowed => (&NO, IDC_NO),
|
||||
CursorStyle::None => return None,
|
||||
_ => (&ARROW, IDC_ARROW),
|
||||
};
|
||||
*(*lock.get_or_init(|| {
|
||||
HCURSOR(
|
||||
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
|
||||
.log_err()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
)
|
||||
.into()
|
||||
}))
|
||||
Some(
|
||||
*(*lock.get_or_init(|| {
|
||||
HCURSOR(
|
||||
unsafe { LoadImageW(None, name, IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED) }
|
||||
.log_err()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
)
|
||||
.into()
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
/// This function is used to configure the dark mode for the window built-in title bar.
|
||||
|
||||
@@ -48,7 +48,7 @@ pub struct WindowsWindowState {
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
pub current_cursor: HCURSOR,
|
||||
pub current_cursor: Option<HCURSOR>,
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
pub display: WindowsDisplay,
|
||||
@@ -76,7 +76,7 @@ impl WindowsWindowState {
|
||||
hwnd: HWND,
|
||||
transparent: bool,
|
||||
cs: &CREATESTRUCTW,
|
||||
current_cursor: HCURSOR,
|
||||
current_cursor: Option<HCURSOR>,
|
||||
display: WindowsDisplay,
|
||||
gpu_context: &BladeContext,
|
||||
) -> Result<Self> {
|
||||
@@ -351,7 +351,7 @@ struct WindowCreateContext<'a> {
|
||||
transparent: bool,
|
||||
is_movable: bool,
|
||||
executor: ForegroundExecutor,
|
||||
current_cursor: HCURSOR,
|
||||
current_cursor: Option<HCURSOR>,
|
||||
windows_version: WindowsVersion,
|
||||
validation_number: usize,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -407,7 +407,7 @@ pub(crate) type AnyMouseListener =
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CursorStyleRequest {
|
||||
pub(crate) hitbox_id: HitboxId,
|
||||
pub(crate) hitbox_id: Option<HitboxId>, // None represents whole window
|
||||
pub(crate) style: CursorStyle,
|
||||
}
|
||||
|
||||
@@ -1928,10 +1928,10 @@ impl Window {
|
||||
|
||||
/// Updates the cursor style at the platform level. This method should only be called
|
||||
/// during the prepaint phase of element drawing.
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
self.next_frame.cursor_styles.push(CursorStyleRequest {
|
||||
hitbox_id: hitbox.id,
|
||||
hitbox_id: hitbox.map(|hitbox| hitbox.id),
|
||||
style,
|
||||
});
|
||||
}
|
||||
@@ -2980,7 +2980,11 @@ impl Window {
|
||||
.cursor_styles
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|request| request.hitbox_id.is_hovered(self))
|
||||
.find(|request| {
|
||||
request
|
||||
.hitbox_id
|
||||
.map_or(true, |hitbox_id| hitbox_id.is_hovered(self))
|
||||
})
|
||||
.map(|request| request.style)
|
||||
.unwrap_or(CursorStyle::Arrow);
|
||||
cx.platform.set_cursor_style(style);
|
||||
@@ -3237,6 +3241,7 @@ impl Window {
|
||||
keystroke,
|
||||
&dispatch_path,
|
||||
);
|
||||
|
||||
if !match_result.to_replay.is_empty() {
|
||||
self.replay_pending_input(match_result.to_replay, cx)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,13 @@ pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets cursor style when hovering over an element to `none`.
|
||||
/// [Docs](https://tailwindcss.com/docs/cursor)
|
||||
#visibility fn cursor_none(mut self, cursor: CursorStyle) -> Self {
|
||||
self.style().mouse_cursor = Some(gpui::CursorStyle::None);
|
||||
self
|
||||
}
|
||||
};
|
||||
|
||||
output.into()
|
||||
|
||||
@@ -104,6 +104,7 @@ impl CloudModel {
|
||||
| google_ai::Model::Gemini20Flash
|
||||
| google_ai::Model::Gemini20FlashThinking
|
||||
| google_ai::Model::Gemini20FlashLite
|
||||
| google_ai::Model::Gemini25ProExp0325
|
||||
| google_ai::Model::Custom { .. } => {
|
||||
LanguageModelAvailability::RequiresPlan(Plan::ZedPro)
|
||||
}
|
||||
|
||||
@@ -407,9 +407,9 @@ impl MarkdownElement {
|
||||
.is_some();
|
||||
|
||||
if is_hovering_link {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
|
||||
}
|
||||
|
||||
self.on_mouse_event(window, cx, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -885,9 +885,9 @@ impl Element for TerminalElement {
|
||||
&& bounds.contains(&window.mouse_position())
|
||||
&& self.terminal_view.read(cx).hover_target_tooltip.is_some()
|
||||
{
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(&layout.hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::IBeam, Some(&layout.hitbox));
|
||||
}
|
||||
|
||||
let cursor = layout.cursor.take();
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ mod uniform_list {
|
||||
});
|
||||
let mut hovered_hitbox_id = None;
|
||||
for (i, hitbox) in hitboxes.iter().enumerate() {
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
|
||||
window.set_cursor_style(gpui::CursorStyle::PointingHand, Some(hitbox));
|
||||
let indent_guide = &self.indent_guides[i];
|
||||
let fill_color = if hitbox.is_hovered(window) {
|
||||
hovered_hitbox_id = Some(hitbox.id);
|
||||
|
||||
@@ -1175,7 +1175,7 @@ mod element {
|
||||
Axis::Vertical => CursorStyle::ResizeRow,
|
||||
Axis::Horizontal => CursorStyle::ResizeColumn,
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &handle.hitbox);
|
||||
window.set_cursor_style(cursor_style, Some(&handle.hitbox));
|
||||
window.paint_quad(gpui::fill(
|
||||
handle.divider_bounds,
|
||||
cx.theme().colors().pane_group_border,
|
||||
|
||||
@@ -6688,7 +6688,7 @@ pub fn client_side_decorations(
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.179.0"
|
||||
version = "0.179.5"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
stable
|
||||
@@ -533,6 +533,16 @@ List of `string` values
|
||||
"cursor_shape": "hollow"
|
||||
```
|
||||
|
||||
## Hide Mouse While Typing
|
||||
|
||||
- Description: Determines whether the mouse cursor should be hidden while typing in an editor or input box.
|
||||
- Setting: `hide_mouse_while_typing`
|
||||
- Default: `true`
|
||||
|
||||
**Options**
|
||||
|
||||
`boolean` values
|
||||
|
||||
## Editor Scrollbar
|
||||
|
||||
- Description: Whether or not to show the editor scrollbar and various elements in it.
|
||||
|
||||
Reference in New Issue
Block a user