Merge branch 'main' into edit-file-tool

This commit is contained in:
Agus Zubiaga 2025-08-08 02:34:11 -03:00
commit d52e0f47b5
30 changed files with 320 additions and 104 deletions

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2 # syntax = docker/dockerfile:1.2
FROM rust:1.89-bookworm as builder FROM rust:1.88-bookworm as builder
WORKDIR app WORKDIR app
COPY . . COPY . .

View file

@ -596,6 +596,8 @@
// when a corresponding project entry becomes active. // when a corresponding project entry becomes active.
// Gitignored entries are never auto revealed. // Gitignored entries are never auto revealed.
"auto_reveal_entries": true, "auto_reveal_entries": true,
// Whether the project panel should open on startup.
"starts_open": true,
// Whether to fold directories automatically and show compact folders // Whether to fold directories automatically and show compact folders
// (e.g. "a/b/c" ) when a directory has only one subdirectory inside. // (e.g. "a/b/c" ) when a directory has only one subdirectory inside.
"auto_fold_dirs": true, "auto_fold_dirs": true,
@ -1171,6 +1173,9 @@
// Sets a delay after which the inline blame information is shown. // Sets a delay after which the inline blame information is shown.
// Delay is restarted with every cursor movement. // Delay is restarted with every cursor movement.
"delay_ms": 0, "delay_ms": 0,
// The amount of padding between the end of the source line and the start
// of the inline blame in units of em widths.
"padding": 7,
// Whether or not to display the git commit summary on the same line. // Whether or not to display the git commit summary on the same line.
"show_commit_summary": false, "show_commit_summary": false,
// The minimum column number to show the inline blame information at // The minimum column number to show the inline blame information at
@ -1233,6 +1238,11 @@
// 2. hour24 // 2. hour24
"hour_format": "hour12" "hour_format": "hour12"
}, },
// Status bar-related settings.
"status_bar": {
// Whether to show the active language button in the status bar.
"active_language_button": true
},
// Settings specific to the terminal // Settings specific to the terminal
"terminal": { "terminal": {
// What shell to use when opening a terminal. May take 3 values: // What shell to use when opening a terminal. May take 3 values:

View file

@ -8,7 +8,6 @@ use agent_client_protocol as acp;
use anyhow::{Context as _, Result}; use anyhow::{Context as _, Result};
use assistant_tool::ActionLog; use assistant_tool::ActionLog;
use editor::Bias; use editor::Bias;
use futures::future::{Fuse, FusedFuture};
use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use futures::{FutureExt, channel::oneshot, future::BoxFuture};
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
use itertools::Itertools; use itertools::Itertools;
@ -482,7 +481,7 @@ pub struct AcpThread {
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>, shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
send_task: Option<Fuse<Task<()>>>, send_task: Option<Task<()>>,
connection: Rc<dyn AgentConnection>, connection: Rc<dyn AgentConnection>,
session_id: acp::SessionId, session_id: acp::SessionId,
} }
@ -572,11 +571,7 @@ impl AcpThread {
} }
pub fn status(&self) -> ThreadStatus { pub fn status(&self) -> ThreadStatus {
if self if self.send_task.is_some() {
.send_task
.as_ref()
.map_or(false, |t| !t.is_terminated())
{
if self.waiting_for_tool_confirmation() { if self.waiting_for_tool_confirmation() {
ThreadStatus::WaitingForToolConfirmation ThreadStatus::WaitingForToolConfirmation
} else { } else {
@ -966,31 +961,29 @@ impl AcpThread {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let cancel_task = self.cancel(cx); let cancel_task = self.cancel(cx);
self.send_task = Some( self.send_task = Some(cx.spawn(async move |this, cx| {
cx.spawn(async move |this, cx| { async {
async { cancel_task.await;
cancel_task.await;
let result = this let result = this
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.connection.prompt( this.connection.prompt(
acp::PromptRequest { acp::PromptRequest {
prompt: message, prompt: message,
session_id: this.session_id.clone(), session_id: this.session_id.clone(),
}, },
cx, cx,
) )
})? })?
.await; .await;
tx.send(result).log_err(); tx.send(result).log_err();
anyhow::Ok(())
} anyhow::Ok(())
.await }
.log_err(); .await
}) .log_err();
.fuse(), }));
);
cx.spawn(async move |this, cx| match rx.await { cx.spawn(async move |this, cx| match rx.await {
Ok(Err(e)) => { Ok(Err(e)) => {
@ -998,7 +991,23 @@ impl AcpThread {
.log_err(); .log_err();
Err(e)? Err(e)?
} }
_ => { result => {
let cancelled = matches!(
result,
Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled
}))
);
// We only take the task if the current prompt wasn't cancelled.
//
// This prompt may have been cancelled because another one was sent
// while it was still generating. In these cases, dropping `send_task`
// would cause the next generation to be cancelled.
if !cancelled {
this.update(cx, |this, _cx| this.send_task.take()).ok();
}
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped)) this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped))
.log_err(); .log_err();
Ok(()) Ok(())

View file

@ -3101,9 +3101,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
// Turn inline-blame-off by default so no state is transferred without us explicitly doing so // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
let inline_blame_off_settings = Some(InlineBlameSettings { let inline_blame_off_settings = Some(InlineBlameSettings {
enabled: false, enabled: false,
delay_ms: None, ..Default::default()
min_column: None,
show_commit_summary: false,
}); });
cx_a.update(|cx| { cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| { SettingsStore::update_global(cx, |store, cx| {

View file

@ -20,6 +20,7 @@ pub struct EditorSettings {
pub lsp_highlight_debounce: u64, pub lsp_highlight_debounce: u64,
pub hover_popover_enabled: bool, pub hover_popover_enabled: bool,
pub hover_popover_delay: u64, pub hover_popover_delay: u64,
pub status_bar: StatusBar,
pub toolbar: Toolbar, pub toolbar: Toolbar,
pub scrollbar: Scrollbar, pub scrollbar: Scrollbar,
pub minimap: Minimap, pub minimap: Minimap,
@ -125,6 +126,14 @@ pub struct JupyterContent {
pub enabled: Option<bool>, pub enabled: Option<bool>,
} }
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct StatusBar {
/// Whether to display the active language button in the status bar.
///
/// Default: true
pub active_language_button: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Toolbar { pub struct Toolbar {
pub breadcrumbs: bool, pub breadcrumbs: bool,
@ -440,6 +449,8 @@ pub struct EditorSettingsContent {
/// ///
/// Default: 300 /// Default: 300
pub hover_popover_delay: Option<u64>, pub hover_popover_delay: Option<u64>,
/// Status bar related settings
pub status_bar: Option<StatusBarContent>,
/// Toolbar related settings /// Toolbar related settings
pub toolbar: Option<ToolbarContent>, pub toolbar: Option<ToolbarContent>,
/// Scrollbar related settings /// Scrollbar related settings
@ -567,6 +578,15 @@ pub struct EditorSettingsContent {
pub lsp_document_colors: Option<DocumentColorsRenderMode>, pub lsp_document_colors: Option<DocumentColorsRenderMode>,
} }
// Status bar related settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct StatusBarContent {
/// Whether to display the active language button in the status bar.
///
/// Default: true
pub active_language_button: Option<bool>,
}
// Toolbar related settings // Toolbar related settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ToolbarContent { pub struct ToolbarContent {

View file

@ -86,8 +86,6 @@ use util::post_inc;
use util::{RangeExt, ResultExt, debug_panic}; use util::{RangeExt, ResultExt, debug_panic};
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt}; use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
/// Determines what kinds of highlights should be applied to a lines background. /// Determines what kinds of highlights should be applied to a lines background.
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
struct LineHighlightSpec { struct LineHighlightSpec {
@ -2428,10 +2426,13 @@ impl EditorElement {
let editor = self.editor.read(cx); let editor = self.editor.read(cx);
let blame = editor.blame.clone()?; let blame = editor.blame.clone()?;
let padding = { let padding = {
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.;
const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.; const INLINE_ACCEPT_SUGGESTION_EM_WIDTHS: f32 = 14.;
let mut padding = INLINE_BLAME_PADDING_EM_WIDTHS; let mut padding = ProjectSettings::get_global(cx)
.git
.inline_blame
.unwrap_or_default()
.padding as f32;
if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() { if let Some(edit_prediction) = editor.active_edit_prediction.as_ref() {
match &edit_prediction.completion { match &edit_prediction.completion {
@ -2469,7 +2470,7 @@ impl EditorElement {
let min_column_in_pixels = ProjectSettings::get_global(cx) let min_column_in_pixels = ProjectSettings::get_global(cx)
.git .git
.inline_blame .inline_blame
.and_then(|settings| settings.min_column) .map(|settings| settings.min_column)
.map(|col| self.column_pixels(col as usize, window)) .map(|col| self.column_pixels(col as usize, window))
.unwrap_or(px(0.)); .unwrap_or(px(0.));
let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels;
@ -8030,12 +8031,20 @@ impl Element for EditorElement {
autoscroll_containing_element, autoscroll_containing_element,
needs_horizontal_autoscroll, needs_horizontal_autoscroll,
) = self.editor.update(cx, |editor, cx| { ) = self.editor.update(cx, |editor, cx| {
let autoscroll_request = editor.autoscroll_request(); let autoscroll_request = editor.scroll_manager.take_autoscroll_request();
let autoscroll_containing_element = let autoscroll_containing_element =
autoscroll_request.is_some() || editor.has_pending_selection(); autoscroll_request.is_some() || editor.has_pending_selection();
let (needs_horizontal_autoscroll, was_scrolled) = editor let (needs_horizontal_autoscroll, was_scrolled) = editor
.autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx); .autoscroll_vertically(
bounds,
line_height,
max_scroll_top,
autoscroll_request,
window,
cx,
);
if was_scrolled.0 { if was_scrolled.0 {
snapshot = editor.snapshot(window, cx); snapshot = editor.snapshot(window, cx);
} }
@ -8357,7 +8366,13 @@ impl Element for EditorElement {
}) })
.flatten()?; .flatten()?;
let mut element = render_inline_blame_entry(blame_entry, &style, cx)?; let mut element = render_inline_blame_entry(blame_entry, &style, cx)?;
let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance; let inline_blame_padding = ProjectSettings::get_global(cx)
.git
.inline_blame
.unwrap_or_default()
.padding
as f32
* em_advance;
Some( Some(
element element
.layout_as_root(AvailableSpace::min_size(), window, cx) .layout_as_root(AvailableSpace::min_size(), window, cx)
@ -8425,7 +8440,11 @@ impl Element for EditorElement {
Ok(blocks) => blocks, Ok(blocks) => blocks,
Err(resized_blocks) => { Err(resized_blocks) => {
self.editor.update(cx, |editor, cx| { self.editor.update(cx, |editor, cx| {
editor.resize_blocks(resized_blocks, autoscroll_request, cx) editor.resize_blocks(
resized_blocks,
autoscroll_request.map(|(autoscroll, _)| autoscroll),
cx,
)
}); });
return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx); return self.prepaint(None, _inspector_id, bounds, &mut (), window, cx);
} }
@ -8470,6 +8489,7 @@ impl Element for EditorElement {
scroll_width, scroll_width,
em_advance, em_advance,
&line_layouts, &line_layouts,
autoscroll_request,
window, window,
cx, cx,
) )

View file

@ -348,8 +348,8 @@ impl ScrollManager {
self.show_scrollbars self.show_scrollbars
} }
pub fn autoscroll_request(&self) -> Option<Autoscroll> { pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
self.autoscroll_request.map(|(autoscroll, _)| autoscroll) self.autoscroll_request.take()
} }
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {

View file

@ -102,15 +102,12 @@ impl AutoscrollStrategy {
pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
impl Editor { impl Editor {
pub fn autoscroll_request(&self) -> Option<Autoscroll> {
self.scroll_manager.autoscroll_request()
}
pub(crate) fn autoscroll_vertically( pub(crate) fn autoscroll_vertically(
&mut self, &mut self,
bounds: Bounds<Pixels>, bounds: Bounds<Pixels>,
line_height: Pixels, line_height: Pixels,
max_scroll_top: f32, max_scroll_top: f32,
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Editor>, cx: &mut Context<Editor>,
) -> (NeedsHorizontalAutoscroll, WasScrolled) { ) -> (NeedsHorizontalAutoscroll, WasScrolled) {
@ -137,7 +134,7 @@ impl Editor {
WasScrolled(false) WasScrolled(false)
}; };
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { let Some((autoscroll, local)) = autoscroll_request else {
return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
}; };
@ -284,9 +281,12 @@ impl Editor {
scroll_width: Pixels, scroll_width: Pixels,
em_advance: Pixels, em_advance: Pixels,
layouts: &[LineWithInvisibles], layouts: &[LineWithInvisibles],
autoscroll_request: Option<(Autoscroll, bool)>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<gpui::Point<f32>> { ) -> Option<gpui::Point<f32>> {
let (_, local) = autoscroll_request?;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx); let selections = self.selections.all::<Point>(cx);
let mut scroll_position = self.scroll_manager.scroll_position(&display_map); let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
@ -335,10 +335,10 @@ impl Editor {
let was_scrolled = if target_left < scroll_left { let was_scrolled = if target_left < scroll_left {
scroll_position.x = target_left / em_advance; scroll_position.x = target_left / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx) self.set_scroll_position_internal(scroll_position, local, true, window, cx)
} else if target_right > scroll_right { } else if target_right > scroll_right {
scroll_position.x = (target_right - viewport_width) / em_advance; scroll_position.x = (target_right - viewport_width) / em_advance;
self.set_scroll_position_internal(scroll_position, true, true, window, cx) self.set_scroll_position_internal(scroll_position, local, true, window, cx)
} else { } else {
WasScrolled(false) WasScrolled(false)
}; };

View file

@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository {
&self, &self,
_paths: Vec<RepoPath>, _paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>, _env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> { ) -> BoxFuture<Result<()>> {
unimplemented!() unimplemented!()
} }
fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> { fn stash_pop(&self, _env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
unimplemented!() unimplemented!()
} }

View file

@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync {
&self, &self,
paths: Vec<RepoPath>, paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>, env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>>; ) -> BoxFuture<Result<()>>;
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>>; fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>>;
fn push( fn push(
&self, &self,
@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository {
&self, &self,
paths: Vec<RepoPath>, paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>, env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> { ) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory(); let working_directory = self.working_directory();
self.executor self.executor
.spawn(async move { .spawn(async move {
@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository {
.boxed() .boxed()
} }
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> { fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<Result<()>> {
let working_directory = self.working_directory(); let working_directory = self.working_directory();
self.executor self.executor
.spawn(async move { .spawn(async move {

View file

@ -1,12 +1,22 @@
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock;
use regex::Regex;
use url::Url; use url::Url;
use git::{ use git::{
BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote,
RemoteUrl, PullRequest, RemoteUrl,
}; };
fn pull_request_regex() -> &'static Regex {
static PULL_REQUEST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
// This matches Bitbucket PR reference pattern: (pull request #xxx)
Regex::new(r"\(pull request #(\d+)\)").unwrap()
});
&PULL_REQUEST_REGEX
}
pub struct Bitbucket { pub struct Bitbucket {
name: String, name: String,
base_url: Url, base_url: Url,
@ -96,6 +106,22 @@ impl GitHostingProvider for Bitbucket {
); );
permalink permalink
} }
fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option<PullRequest> {
// Check first line of commit message for PR references
let first_line = message.lines().next()?;
// Try to match against our PR patterns
let capture = pull_request_regex().captures(first_line)?;
let number = capture.get(1)?.as_str().parse::<u32>().ok()?;
// Construct the PR URL in Bitbucket format
let mut url = self.base_url();
let path = format!("/{}/{}/pull-requests/{}", remote.owner, remote.repo, number);
url.set_path(&path);
Some(PullRequest { number, url })
}
} }
#[cfg(test)] #[cfg(test)]
@ -203,4 +229,34 @@ mod tests {
"https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48"; "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48";
assert_eq!(permalink.to_string(), expected_url.to_string()) assert_eq!(permalink.to_string(), expected_url.to_string())
} }
#[test]
fn test_bitbucket_pull_requests() {
use indoc::indoc;
let remote = ParsedGitRemote {
owner: "zed-industries".into(),
repo: "zed".into(),
};
let bitbucket = Bitbucket::public_instance();
// Test message without PR reference
let message = "This does not contain a pull request";
assert!(bitbucket.extract_pull_request(&remote, message).is_none());
// Pull request number at end of first line
let message = indoc! {r#"
Merged in feature-branch (pull request #123)
Some detailed description of the changes.
"#};
let pr = bitbucket.extract_pull_request(&remote, message).unwrap();
assert_eq!(pr.number, 123);
assert_eq!(
pr.url.as_str(),
"https://bitbucket.org/zed-industries/zed/pull-requests/123"
);
}
} }

View file

@ -461,8 +461,6 @@ fn skip_whitespace(source: &str) -> &str {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use core::slice;
use super::*; use super::*;
use crate as gpui; use crate as gpui;
use KeyBindingContextPredicate::*; use KeyBindingContextPredicate::*;
@ -676,11 +674,11 @@ mod tests {
assert!(predicate.eval(&contexts)); assert!(predicate.eval(&contexts));
assert!(!predicate.eval(&[])); assert!(!predicate.eval(&[]));
assert!(!predicate.eval(slice::from_ref(&child_context))); assert!(!predicate.eval(&[child_context.clone()]));
assert!(!predicate.eval(&[parent_context])); assert!(!predicate.eval(&[parent_context]));
let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap();
assert!(!zany_predicate.eval(slice::from_ref(&child_context))); assert!(!zany_predicate.eval(&[child_context.clone()]));
assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()]));
} }
@ -692,13 +690,13 @@ mod tests {
let parent_context = KeyContext::try_from("parent").unwrap(); let parent_context = KeyContext::try_from("parent").unwrap();
let child_context = KeyContext::try_from("child").unwrap(); let child_context = KeyContext::try_from("child").unwrap();
assert!(not_predicate.eval(slice::from_ref(&workspace_context))); assert!(not_predicate.eval(&[workspace_context.clone()]));
assert!(!not_predicate.eval(slice::from_ref(&editor_context))); assert!(!not_predicate.eval(&[editor_context.clone()]));
assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()]));
assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()]));
let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap(); let complex_not = KeyBindingContextPredicate::parse("!editor && workspace").unwrap();
assert!(complex_not.eval(slice::from_ref(&workspace_context))); assert!(complex_not.eval(&[workspace_context.clone()]));
assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!complex_not.eval(&[editor_context.clone(), workspace_context.clone()]));
let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap();
@ -711,18 +709,18 @@ mod tests {
assert!(not_mode_predicate.eval(&[other_mode_context])); assert!(not_mode_predicate.eval(&[other_mode_context]));
let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap();
assert!(not_descendant.eval(slice::from_ref(&parent_context))); assert!(not_descendant.eval(&[parent_context.clone()]));
assert!(not_descendant.eval(slice::from_ref(&child_context))); assert!(not_descendant.eval(&[child_context.clone()]));
assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap();
assert!(!not_descendant.eval(slice::from_ref(&parent_context))); assert!(!not_descendant.eval(&[parent_context.clone()]));
assert!(!not_descendant.eval(slice::from_ref(&child_context))); assert!(!not_descendant.eval(&[child_context.clone()]));
assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()]));
let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap();
assert!(double_not.eval(slice::from_ref(&editor_context))); assert!(double_not.eval(&[editor_context.clone()]));
assert!(!double_not.eval(slice::from_ref(&workspace_context))); assert!(!double_not.eval(&[workspace_context.clone()]));
// Test complex descendant cases // Test complex descendant cases
let workspace_context = KeyContext::try_from("Workspace").unwrap(); let workspace_context = KeyContext::try_from("Workspace").unwrap();
@ -756,9 +754,9 @@ mod tests {
// !Workspace - shouldn't match when Workspace is in the context // !Workspace - shouldn't match when Workspace is in the context
let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap();
assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); assert!(!not_workspace.eval(&[workspace_context.clone()]));
assert!(not_workspace.eval(slice::from_ref(&pane_context))); assert!(not_workspace.eval(&[pane_context.clone()]));
assert!(not_workspace.eval(slice::from_ref(&editor_context))); assert!(not_workspace.eval(&[editor_context.clone()]));
assert!(!not_workspace.eval(&workspace_pane_editor)); assert!(!not_workspace.eval(&workspace_pane_editor));
} }
} }

View file

@ -1,6 +1,28 @@
use std::ops::Deref; use std::ops::Deref;
use windows::Win32::UI::WindowsAndMessaging::HCURSOR; use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR};
#[derive(Debug, Clone, Copy)]
pub(crate) struct SafeHandle {
raw: HANDLE,
}
unsafe impl Send for SafeHandle {}
unsafe impl Sync for SafeHandle {}
impl From<HANDLE> for SafeHandle {
fn from(value: HANDLE) -> Self {
SafeHandle { raw: value }
}
}
impl Deref for SafeHandle {
type Target = HANDLE;
fn deref(&self) -> &Self::Target {
&self.raw
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub(crate) struct SafeCursor { pub(crate) struct SafeCursor {

View file

@ -1,8 +1,9 @@
use editor::Editor; use editor::{Editor, EditorSettings};
use gpui::{ use gpui::{
Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div, Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div,
}; };
use language::LanguageName; use language::LanguageName;
use settings::Settings as _;
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip};
use workspace::{StatusItemView, Workspace, item::ItemHandle}; use workspace::{StatusItemView, Workspace, item::ItemHandle};
@ -39,6 +40,13 @@ impl ActiveBufferLanguage {
impl Render for ActiveBufferLanguage { impl Render for ActiveBufferLanguage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !EditorSettings::get_global(cx)
.status_bar
.active_language_button
{
return div();
}
div().when_some(self.active_language.as_ref(), |el, active_language| { div().when_some(self.active_language.as_ref(), |el, active_language| {
let active_language_text = if let Some(active_language_text) = active_language { let active_language_text = if let Some(active_language_text) = active_language {
active_language_text.to_string() active_language_text.to_string()

View file

@ -1,6 +1,6 @@
name = "C++" name = "C++"
grammar = "cpp" grammar = "cpp"
path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"] path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"]
line_comments = ["// ", "/// ", "//! "] line_comments = ["// ", "/// ", "//! "]
decrease_indent_patterns = [ decrease_indent_patterns = [
{ pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] },

View file

@ -146,6 +146,7 @@
"&&=" "&&="
"||=" "||="
"??=" "??="
"..."
] @operator ] @operator
(regex "/" @string.regex) (regex "/" @string.regex)

View file

@ -146,6 +146,7 @@
"&&=" "&&="
"||=" "||="
"??=" "??="
"..."
] @operator ] @operator
(regex "/" @string.regex) (regex "/" @string.regex)

View file

@ -167,6 +167,7 @@
"&&=" "&&="
"||=" "||="
"??=" "??="
"..."
] @operator ] @operator
(regex "/" @string.regex) (regex "/" @string.regex)

View file

@ -431,10 +431,9 @@ impl GitSettings {
pub fn inline_blame_delay(&self) -> Option<Duration> { pub fn inline_blame_delay(&self) -> Option<Duration> {
match self.inline_blame { match self.inline_blame {
Some(InlineBlameSettings { Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => {
delay_ms: Some(delay_ms), Some(Duration::from_millis(delay_ms))
.. }
}) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
_ => None, _ => None,
} }
} }
@ -470,7 +469,7 @@ pub enum GitGutterSetting {
Hide, Hide,
} }
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct InlineBlameSettings { pub struct InlineBlameSettings {
/// Whether or not to show git blame data inline in /// Whether or not to show git blame data inline in
@ -483,11 +482,19 @@ pub struct InlineBlameSettings {
/// after a delay once the cursor stops moving. /// after a delay once the cursor stops moving.
/// ///
/// Default: 0 /// Default: 0
pub delay_ms: Option<u64>, #[serde(default)]
pub delay_ms: u64,
/// The amount of padding between the end of the source line and the start
/// of the inline blame in units of columns.
///
/// Default: 7
#[serde(default = "default_inline_blame_padding")]
pub padding: u32,
/// The minimum column number to show the inline blame information at /// The minimum column number to show the inline blame information at
/// ///
/// Default: 0 /// Default: 0
pub min_column: Option<u32>, #[serde(default)]
pub min_column: u32,
/// Whether to show commit summary as part of the inline blame. /// Whether to show commit summary as part of the inline blame.
/// ///
/// Default: false /// Default: false
@ -495,6 +502,22 @@ pub struct InlineBlameSettings {
pub show_commit_summary: bool, pub show_commit_summary: bool,
} }
fn default_inline_blame_padding() -> u32 {
7
}
impl Default for InlineBlameSettings {
fn default() -> Self {
Self {
enabled: true,
delay_ms: 0,
padding: default_inline_blame_padding(),
min_column: 0,
show_commit_summary: false,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct BinarySettings { pub struct BinarySettings {
pub path: Option<String>, pub path: Option<String>,

View file

@ -5625,6 +5625,10 @@ impl Panel for ProjectPanel {
} }
fn starts_open(&self, _: &Window, cx: &App) -> bool { fn starts_open(&self, _: &Window, cx: &App) -> bool {
if !ProjectPanelSettings::get_global(cx).starts_open {
return false;
}
let project = &self.project.read(cx); let project = &self.project.read(cx);
project.visible_worktrees(cx).any(|tree| { project.visible_worktrees(cx).any(|tree| {
tree.read(cx) tree.read(cx)

View file

@ -43,6 +43,7 @@ pub struct ProjectPanelSettings {
pub sticky_scroll: bool, pub sticky_scroll: bool,
pub auto_reveal_entries: bool, pub auto_reveal_entries: bool,
pub auto_fold_dirs: bool, pub auto_fold_dirs: bool,
pub starts_open: bool,
pub scrollbar: ScrollbarSettings, pub scrollbar: ScrollbarSettings,
pub show_diagnostics: ShowDiagnostics, pub show_diagnostics: ShowDiagnostics,
pub hide_root: bool, pub hide_root: bool,
@ -139,6 +140,10 @@ pub struct ProjectPanelSettingsContent {
/// ///
/// Default: true /// Default: true
pub auto_fold_dirs: Option<bool>, pub auto_fold_dirs: Option<bool>,
/// Whether the project panel should open on startup.
///
/// Default: true
pub starts_open: Option<bool>,
/// Scrollbar-related settings /// Scrollbar-related settings
pub scrollbar: Option<ScrollbarSettingsContent>, pub scrollbar: Option<ScrollbarSettingsContent>,
/// Which files containing diagnostic errors/warnings to mark in the project panel. /// Which files containing diagnostic errors/warnings to mark in the project panel.

View file

@ -136,7 +136,7 @@ impl BatchedTextRun {
.shape_line( .shape_line(
self.text.clone().into(), self.text.clone().into(),
self.font_size.to_pixels(window.rem_size()), self.font_size.to_pixels(window.rem_size()),
std::slice::from_ref(&self.style), &[self.style.clone()],
Some(dimensions.cell_width), Some(dimensions.cell_width),
) )
.paint(pos, dimensions.line_height, window, cx); .paint(pos, dimensions.line_height, window, cx);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Before After
Before After

View file

@ -1275,6 +1275,18 @@ Each option controls displaying of a particular toolbar element. If all elements
`boolean` values `boolean` values
## Status Bar
- Description: Control various elements in the status bar. Note that some items in the status bar have their own settings set elsewhere.
- Setting: `status_bar`
- Default:
```json
"status_bar": {
"active_language_button": true,
},
```
## LSP ## LSP
- Description: Configuration for language servers. - Description: Configuration for language servers.
@ -1795,7 +1807,6 @@ Example:
{ {
"git": { "git": {
"inline_blame": { "inline_blame": {
"enabled": true,
"delay_ms": 500 "delay_ms": 500
} }
} }
@ -1808,7 +1819,6 @@ Example:
{ {
"git": { "git": {
"inline_blame": { "inline_blame": {
"enabled": true,
"show_commit_summary": true "show_commit_summary": true
} }
} }
@ -1821,13 +1831,24 @@ Example:
{ {
"git": { "git": {
"inline_blame": { "inline_blame": {
"enabled": true,
"min_column": 80 "min_column": 80
} }
} }
} }
``` ```
5. Set the padding between the end of the line and the inline blame hint, in ems:
```json
{
"git": {
"inline_blame": {
"padding": 10
}
}
}
```
### Hunk Style ### Hunk Style
- Description: What styling we should use for the diff hunks. - Description: What styling we should use for the diff hunks.
@ -3204,7 +3225,8 @@ Run the `theme selector: toggle` action in the command palette to see a current
"indent_guides": { "indent_guides": {
"show": "always" "show": "always"
}, },
"hide_root": false "hide_root": false,
"starts_open": true
} }
} }
``` ```

View file

@ -223,6 +223,7 @@ TBD: Centered layout related settings
"enabled": true, // Show/hide inline blame "enabled": true, // Show/hide inline blame
"delay": 0, // Show after delay (ms) "delay": 0, // Show after delay (ms)
"min_column": 0, // Minimum column to inline display blame "min_column": 0, // Minimum column to inline display blame
"padding": 7, // Padding between code and inline blame (em)
"show_commit_summary": false // Show/hide commit summary "show_commit_summary": false // Show/hide commit summary
}, },
"hunk_style": "staged_hollow" // staged_hollow, unstaged_hollow "hunk_style": "staged_hollow" // staged_hollow, unstaged_hollow
@ -305,6 +306,17 @@ TBD: Centered layout related settings
} }
``` ```
### Status Bar
```json
"status_bar": {
// Show/hide a button that displays the active buffer's language.
// Clicking the button brings up the language selector.
// Defaults to true.
"active_language_button": true,
},
```
### Multibuffer ### Multibuffer
```json ```json

View file

@ -9,7 +9,7 @@ repository = "https://github.com/zed-industries/zed"
[language_servers.emmet-language-server] [language_servers.emmet-language-server]
name = "Emmet Language Server" name = "Emmet Language Server"
language = "HTML" language = "HTML"
languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"] languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir", "Vue.js"]
[language_servers.emmet-language-server.language_ids] [language_servers.emmet-language-server.language_ids]
"HTML" = "html" "HTML" = "html"
@ -21,3 +21,4 @@ languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX
"CSS" = "css" "CSS" = "css"
"HEEX" = "heex" "HEEX" = "heex"
"Elixir" = "heex" "Elixir" = "heex"
"Vue.js" = "vue"

18
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1754269165, "lastModified": 1750266157,
"narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "444e81206df3f7d92780680e45858e31d2f07a08", "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -33,10 +33,10 @@
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 315532800, "lastModified": 315532800,
"narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=",
"rev": "a683adc19ff5228af548c6539dbc3440509bfed3", "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff",
"type": "tarball", "type": "tarball",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre840248.a683adc19ff5/nixexprs.tar.xz" "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@ -58,11 +58,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1754575663, "lastModified": 1750964660,
"narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,5 +1,5 @@
[toolchain] [toolchain]
channel = "1.89" channel = "1.88"
profile = "minimal" profile = "minimal"
components = [ "rustfmt", "clippy" ] components = [ "rustfmt", "clippy" ]
targets = [ targets = [

View file

@ -9,7 +9,12 @@ main() {
platform="$(uname -s)" platform="$(uname -s)"
arch="$(uname -m)" arch="$(uname -m)"
channel="${ZED_CHANNEL:-stable}" channel="${ZED_CHANNEL:-stable}"
temp="$(mktemp -d "/tmp/zed-XXXXXX")" # Use TMPDIR if available (for environments with non-standard temp directories)
if [ -n "${TMPDIR:-}" ] && [ -d "${TMPDIR}" ]; then
temp="$(mktemp -d "$TMPDIR/zed-XXXXXX")"
else
temp="$(mktemp -d "/tmp/zed-XXXXXX")"
fi
if [ "$platform" = "Darwin" ]; then if [ "$platform" = "Darwin" ]; then
platform="macos" platform="macos"

View file

@ -5,7 +5,7 @@ function export_vars_for_environment {
echo "Invalid environment name '${environment}'" >&2 echo "Invalid environment name '${environment}'" >&2
exit 1 exit 1
fi fi
export $(cat $env_file) export $(grep -v '^#' $env_file | grep -v '^[[:space:]]*$')
} }
function target_zed_kube_cluster { function target_zed_kube_cluster {