Compare commits

...

21 Commits

Author SHA1 Message Date
Joseph T. Lyons
363c205ec6 zed 0.179.5 2025-03-31 13:25:26 -04:00
Michael Sloan
656d4a7104 Add support for Gemini 2.5 Pro Experimental model (#27468)
Release Notes:

- Added support for Gemini 2.5 Pro Experimental model to Zed AI.

Co-authored-by: Wilhelm Klopp <wil.klopp@gmail.com>
2025-03-31 13:04:05 -04:00
Zed Bot
d637307bd7 Bump to 0.179.4 for @osiewicz 2025-03-28 12:43:33 +00:00
gcp-cherry-pick-bot[bot]
d0398a5f80 language server: Fix restarts sometimes not working for buffers open in go-to-definition view (cherry-pick #27655) (#27657)
Cherry-picked language server: Fix restarts sometimes not working for
buffers open in go-to-definition view (#27655)

Closes #ISSUE

Release Notes:

- Fixed language server restarts sometimes not restarting a language
server.

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
2025-03-28 13:41:09 +01:00
Zed Bot
83d9f19234 Bump to 0.179.3 for @maxbrunsfeld 2025-03-27 16:52:33 +00:00
Smit Barmase
5c2fe76d48 Hide the mouse when the user is typing in the editor - take 2 (#27519)
Closes #4461

Take 2 on https://github.com/zed-industries/zed/pull/25040.

Fixes panic caused due to using `setHiddenUntilMouseMoves` return type
to `set` cursor on macOS.

Release Notes:

- Now cursor hides when the user is typing in editor. It will stay
hidden until it is moved again. This behavior is `true` by default, and
can be configured with `hide_mouse_while_typing` in settings.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Thomas Mickley-Doyle <thomas@zed.dev>
Co-authored-by: Agus <agus@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Angelk90 <angelo.k90@hotmail.it>
2025-03-27 22:03:35 +05:30
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
47 changed files with 980 additions and 990 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.5"
dependencies = [
"activity_indicator",
"anyhow",

View File

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

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

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

View File

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

View File

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

View File

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

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

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

View File

@@ -61,7 +61,7 @@ impl Render for WindowShadow {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -6688,7 +6688,7 @@ pub fn client_side_decorations(
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
Some(&hitbox),
);
},
)

View File

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

View File

@@ -1 +1 @@
dev
stable

View File

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