diff --git a/Dockerfile-collab b/Dockerfile-collab index c1621d6ee6..2dafe296c7 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.89-bookworm as builder +FROM rust:1.88-bookworm as builder WORKDIR app COPY . . diff --git a/assets/settings/default.json b/assets/settings/default.json index 4734b5d118..9c579b858d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -596,6 +596,8 @@ // when a corresponding project entry becomes active. // Gitignored entries are never auto revealed. "auto_reveal_entries": true, + // Whether the project panel should open on startup. + "starts_open": true, // Whether to fold directories automatically and show compact folders // (e.g. "a/b/c" ) when a directory has only one subdirectory inside. "auto_fold_dirs": true, @@ -1171,6 +1173,9 @@ // Sets a delay after which the inline blame information is shown. // Delay is restarted with every cursor movement. "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. "show_commit_summary": false, // The minimum column number to show the inline blame information at @@ -1233,6 +1238,11 @@ // 2. hour24 "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 "terminal": { // What shell to use when opening a terminal. May take 3 values: diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 7a00bd2320..54bfe56a15 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -8,7 +8,6 @@ use agent_client_protocol as acp; use anyhow::{Context as _, Result}; use assistant_tool::ActionLog; use editor::Bias; -use futures::future::{Fuse, FusedFuture}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; @@ -482,7 +481,7 @@ pub struct AcpThread { project: Entity, action_log: Entity, shared_buffers: HashMap, BufferSnapshot>, - send_task: Option>>, + send_task: Option>, connection: Rc, session_id: acp::SessionId, } @@ -572,11 +571,7 @@ impl AcpThread { } pub fn status(&self) -> ThreadStatus { - if self - .send_task - .as_ref() - .map_or(false, |t| !t.is_terminated()) - { + if self.send_task.is_some() { if self.waiting_for_tool_confirmation() { ThreadStatus::WaitingForToolConfirmation } else { @@ -966,31 +961,29 @@ impl AcpThread { let (tx, rx) = oneshot::channel(); let cancel_task = self.cancel(cx); - self.send_task = Some( - cx.spawn(async move |this, cx| { - async { - cancel_task.await; + self.send_task = Some(cx.spawn(async move |this, cx| { + async { + cancel_task.await; - let result = this - .update(cx, |this, cx| { - this.connection.prompt( - acp::PromptRequest { - prompt: message, - session_id: this.session_id.clone(), - }, - cx, - ) - })? - .await; + let result = this + .update(cx, |this, cx| { + this.connection.prompt( + acp::PromptRequest { + prompt: message, + session_id: this.session_id.clone(), + }, + cx, + ) + })? + .await; - tx.send(result).log_err(); - anyhow::Ok(()) - } - .await - .log_err(); - }) - .fuse(), - ); + tx.send(result).log_err(); + + anyhow::Ok(()) + } + .await + .log_err(); + })); cx.spawn(async move |this, cx| match rx.await { Ok(Err(e)) => { @@ -998,7 +991,23 @@ impl AcpThread { .log_err(); 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)) .log_err(); Ok(()) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 8754b53f6e..7b95fdd458 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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 let inline_blame_off_settings = Some(InlineBlameSettings { enabled: false, - delay_ms: None, - min_column: None, - show_commit_summary: false, + ..Default::default() }); cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 14f46c0e60..3d132651b8 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -20,6 +20,7 @@ pub struct EditorSettings { pub lsp_highlight_debounce: u64, pub hover_popover_enabled: bool, pub hover_popover_delay: u64, + pub status_bar: StatusBar, pub toolbar: Toolbar, pub scrollbar: Scrollbar, pub minimap: Minimap, @@ -125,6 +126,14 @@ pub struct JupyterContent { pub enabled: Option, } +#[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)] pub struct Toolbar { pub breadcrumbs: bool, @@ -440,6 +449,8 @@ pub struct EditorSettingsContent { /// /// Default: 300 pub hover_popover_delay: Option, + /// Status bar related settings + pub status_bar: Option, /// Toolbar related settings pub toolbar: Option, /// Scrollbar related settings @@ -567,6 +578,15 @@ pub struct EditorSettingsContent { pub lsp_document_colors: Option, } +// 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, +} + // Toolbar related settings #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ToolbarContent { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 034fff970d..a7fd0abf88 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -86,8 +86,6 @@ use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; 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. #[derive(Clone, Copy, Default)] struct LineHighlightSpec { @@ -2428,10 +2426,13 @@ impl EditorElement { let editor = self.editor.read(cx); let blame = editor.blame.clone()?; let padding = { - const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; 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() { match &edit_prediction.completion { @@ -2469,7 +2470,7 @@ impl EditorElement { let min_column_in_pixels = ProjectSettings::get_global(cx) .git .inline_blame - .and_then(|settings| settings.min_column) + .map(|settings| settings.min_column) .map(|col| self.column_pixels(col as usize, window)) .unwrap_or(px(0.)); 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, needs_horizontal_autoscroll, ) = 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 = autoscroll_request.is_some() || editor.has_pending_selection(); 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 { snapshot = editor.snapshot(window, cx); } @@ -8357,7 +8366,13 @@ impl Element for EditorElement { }) .flatten()?; 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( element .layout_as_root(AvailableSpace::min_size(), window, cx) @@ -8425,7 +8440,11 @@ impl Element for EditorElement { Ok(blocks) => blocks, Err(resized_blocks) => { 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); } @@ -8470,6 +8489,7 @@ impl Element for EditorElement { scroll_width, em_advance, &line_layouts, + autoscroll_request, window, cx, ) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index ecaf7c11e4..08ff23f8f7 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -348,8 +348,8 @@ impl ScrollManager { self.show_scrollbars } - pub fn autoscroll_request(&self) -> Option { - self.autoscroll_request.map(|(autoscroll, _)| autoscroll) + pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> { + self.autoscroll_request.take() } pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e8a1f8da73..88d3b52d76 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -102,15 +102,12 @@ impl AutoscrollStrategy { pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); impl Editor { - pub fn autoscroll_request(&self) -> Option { - self.scroll_manager.autoscroll_request() - } - pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds, line_height: Pixels, max_scroll_top: f32, + autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> (NeedsHorizontalAutoscroll, WasScrolled) { @@ -137,7 +134,7 @@ impl Editor { 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); }; @@ -284,9 +281,12 @@ impl Editor { scroll_width: Pixels, em_advance: Pixels, layouts: &[LineWithInvisibles], + autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> Option> { + let (_, local) = autoscroll_request?; + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(cx); 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 { 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 { 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 { WasScrolled(false) }; diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 73da63fd47..04ba656232 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -402,11 +402,11 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { unimplemented!() } - fn stash_pop(&self, _env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, _env: Arc>) -> BoxFuture> { unimplemented!() } diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 518b6c4f46..dc7ab0af65 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -399,9 +399,9 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>>; + ) -> BoxFuture>; - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>>; + fn stash_pop(&self, env: Arc>) -> BoxFuture>; fn push( &self, @@ -1203,7 +1203,7 @@ impl GitRepository for RealGitRepository { &self, paths: Vec, env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + ) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { @@ -1227,7 +1227,7 @@ impl GitRepository for RealGitRepository { .boxed() } - fn stash_pop(&self, env: Arc>) -> BoxFuture<'_, Result<()>> { + fn stash_pop(&self, env: Arc>) -> BoxFuture> { let working_directory = self.working_directory(); self.executor .spawn(async move { diff --git a/crates/git_hosting_providers/src/providers/bitbucket.rs b/crates/git_hosting_providers/src/providers/bitbucket.rs index 074a169135..26df7b567a 100644 --- a/crates/git_hosting_providers/src/providers/bitbucket.rs +++ b/crates/git_hosting_providers/src/providers/bitbucket.rs @@ -1,12 +1,22 @@ use std::str::FromStr; +use std::sync::LazyLock; +use regex::Regex; use url::Url; use git::{ BuildCommitPermalinkParams, BuildPermalinkParams, GitHostingProvider, ParsedGitRemote, - RemoteUrl, + PullRequest, RemoteUrl, }; +fn pull_request_regex() -> &'static Regex { + static PULL_REQUEST_REGEX: LazyLock = LazyLock::new(|| { + // This matches Bitbucket PR reference pattern: (pull request #xxx) + Regex::new(r"\(pull request #(\d+)\)").unwrap() + }); + &PULL_REQUEST_REGEX +} + pub struct Bitbucket { name: String, base_url: Url, @@ -96,6 +106,22 @@ impl GitHostingProvider for Bitbucket { ); permalink } + + fn extract_pull_request(&self, remote: &ParsedGitRemote, message: &str) -> Option { + // 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::().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)] @@ -203,4 +229,34 @@ mod tests { "https://bitbucket.org/zed-industries/zed/src/f00b4r/main.rs#lines-24:48"; 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" + ); + } } diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index 281035fe97..f4b878ae77 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -461,8 +461,6 @@ fn skip_whitespace(source: &str) -> &str { #[cfg(test)] mod tests { - use core::slice; - use super::*; use crate as gpui; use KeyBindingContextPredicate::*; @@ -676,11 +674,11 @@ mod tests { assert!(predicate.eval(&contexts)); assert!(!predicate.eval(&[])); - assert!(!predicate.eval(slice::from_ref(&child_context))); + assert!(!predicate.eval(&[child_context.clone()])); assert!(!predicate.eval(&[parent_context])); 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()])); } @@ -692,13 +690,13 @@ mod tests { let parent_context = KeyContext::try_from("parent").unwrap(); let child_context = KeyContext::try_from("child").unwrap(); - assert!(not_predicate.eval(slice::from_ref(&workspace_context))); - assert!(!not_predicate.eval(slice::from_ref(&editor_context))); + assert!(not_predicate.eval(&[workspace_context.clone()])); + assert!(!not_predicate.eval(&[editor_context.clone()])); assert!(!not_predicate.eval(&[editor_context.clone(), workspace_context.clone()])); assert!(!not_predicate.eval(&[workspace_context.clone(), editor_context.clone()])); 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()])); let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); @@ -711,18 +709,18 @@ mod tests { assert!(not_mode_predicate.eval(&[other_mode_context])); let not_descendant = KeyBindingContextPredicate::parse("!(parent > child)").unwrap(); - assert!(not_descendant.eval(slice::from_ref(&parent_context))); - assert!(not_descendant.eval(slice::from_ref(&child_context))); + assert!(not_descendant.eval(&[parent_context.clone()])); + assert!(not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let not_descendant = KeyBindingContextPredicate::parse("parent > !child").unwrap(); - assert!(!not_descendant.eval(slice::from_ref(&parent_context))); - assert!(!not_descendant.eval(slice::from_ref(&child_context))); + assert!(!not_descendant.eval(&[parent_context.clone()])); + assert!(!not_descendant.eval(&[child_context.clone()])); assert!(!not_descendant.eval(&[parent_context.clone(), child_context.clone()])); let double_not = KeyBindingContextPredicate::parse("!!editor").unwrap(); - assert!(double_not.eval(slice::from_ref(&editor_context))); - assert!(!double_not.eval(slice::from_ref(&workspace_context))); + assert!(double_not.eval(&[editor_context.clone()])); + assert!(!double_not.eval(&[workspace_context.clone()])); // Test complex descendant cases let workspace_context = KeyContext::try_from("Workspace").unwrap(); @@ -756,9 +754,9 @@ mod tests { // !Workspace - shouldn't match when Workspace is in the context let not_workspace = KeyBindingContextPredicate::parse("!Workspace").unwrap(); - assert!(!not_workspace.eval(slice::from_ref(&workspace_context))); - assert!(not_workspace.eval(slice::from_ref(&pane_context))); - assert!(not_workspace.eval(slice::from_ref(&editor_context))); + assert!(!not_workspace.eval(&[workspace_context.clone()])); + assert!(not_workspace.eval(&[pane_context.clone()])); + assert!(not_workspace.eval(&[editor_context.clone()])); assert!(!not_workspace.eval(&workspace_pane_editor)); } } diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/crates/gpui/src/platform/windows/wrapper.rs index a1fe98a392..6015dffdab 100644 --- a/crates/gpui/src/platform/windows/wrapper.rs +++ b/crates/gpui/src/platform/windows/wrapper.rs @@ -1,6 +1,28 @@ 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 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)] pub(crate) struct SafeCursor { diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 250d0c23d8..c5c5eceab5 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -1,8 +1,9 @@ -use editor::Editor; +use editor::{Editor, EditorSettings}; use gpui::{ Context, Entity, IntoElement, ParentElement, Render, Subscription, WeakEntity, Window, div, }; use language::LanguageName; +use settings::Settings as _; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -39,6 +40,13 @@ impl ActiveBufferLanguage { impl Render for ActiveBufferLanguage { fn render(&mut self, _: &mut Window, cx: &mut Context) -> 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| { let active_language_text = if let Some(active_language_text) = active_language { active_language_text.to_string() diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index fab88266d7..7e24415f9d 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -1,6 +1,6 @@ name = "C++" 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 = ["// ", "/// ", "//! "] decrease_indent_patterns = [ { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index 73cb1a5e45..9d5ebbaf71 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -146,6 +146,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index e2837c61fd..5e2fbbf63a 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -146,6 +146,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 486e5a7684..af37ef6415 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -167,6 +167,7 @@ "&&=" "||=" "??=" + "..." ] @operator (regex "/" @string.regex) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 20be7fef85..12e3aa88ad 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -431,10 +431,9 @@ impl GitSettings { pub fn inline_blame_delay(&self) -> Option { match self.inline_blame { - Some(InlineBlameSettings { - delay_ms: Some(delay_ms), - .. - }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)), + Some(InlineBlameSettings { delay_ms, .. }) if delay_ms > 0 => { + Some(Duration::from_millis(delay_ms)) + } _ => None, } } @@ -470,7 +469,7 @@ pub enum GitGutterSetting { Hide, } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InlineBlameSettings { /// 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. /// /// Default: 0 - pub delay_ms: Option, + #[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 /// /// Default: 0 - pub min_column: Option, + #[serde(default)] + pub min_column: u32, /// Whether to show commit summary as part of the inline blame. /// /// Default: false @@ -495,6 +502,22 @@ pub struct InlineBlameSettings { 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)] pub struct BinarySettings { pub path: Option, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 45581a97c4..94d543ed0c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -5625,6 +5625,10 @@ impl Panel for ProjectPanel { } fn starts_open(&self, _: &Window, cx: &App) -> bool { + if !ProjectPanelSettings::get_global(cx).starts_open { + return false; + } + let project = &self.project.read(cx); project.visible_worktrees(cx).any(|tree| { tree.read(cx) diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 9057480972..8a243589ed 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -43,6 +43,7 @@ pub struct ProjectPanelSettings { pub sticky_scroll: bool, pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, + pub starts_open: bool, pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, @@ -139,6 +140,10 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub auto_fold_dirs: Option, + /// Whether the project panel should open on startup. + /// + /// Default: true + pub starts_open: Option, /// Scrollbar-related settings pub scrollbar: Option, /// Which files containing diagnostic errors/warnings to mark in the project panel. diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 6c1be9d5e7..083c07de9c 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -136,7 +136,7 @@ impl BatchedTextRun { .shape_line( self.text.clone().into(), self.font_size.to_pixels(window.rem_size()), - std::slice::from_ref(&self.style), + &[self.style.clone()], Some(dimensions.cell_width), ) .paint(pos, dimensions.line_height, window, cx); diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico index 15e06a6e17..165e4ce1f7 100644 Binary files a/crates/zed/resources/windows/app-icon-nightly.ico and b/crates/zed/resources/windows/app-icon-nightly.ico differ diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 5fd27abad6..1996e1c4ee 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -1275,6 +1275,18 @@ Each option controls displaying of a particular toolbar element. If all elements `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 - Description: Configuration for language servers. @@ -1795,7 +1807,6 @@ Example: { "git": { "inline_blame": { - "enabled": true, "delay_ms": 500 } } @@ -1808,7 +1819,6 @@ Example: { "git": { "inline_blame": { - "enabled": true, "show_commit_summary": true } } @@ -1821,13 +1831,24 @@ Example: { "git": { "inline_blame": { - "enabled": true, "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 - 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": { "show": "always" }, - "hide_root": false + "hide_root": false, + "starts_open": true } } ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 8b307d97d5..46de078d89 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -223,6 +223,7 @@ TBD: Centered layout related settings "enabled": true, // Show/hide inline blame "delay": 0, // Show after delay (ms) "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 }, "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 ```json diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 99aa80a2d4..9fa14d091f 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -9,7 +9,7 @@ repository = "https://github.com/zed-industries/zed" [language_servers.emmet-language-server] name = "Emmet Language Server" 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] "HTML" = "html" @@ -21,3 +21,4 @@ languages = ["HTML", "PHP", "ERB", "HTML/ERB", "JavaScript", "TSX", "CSS", "HEEX "CSS" = "css" "HEEX" = "heex" "Elixir" = "heex" +"Vue.js" = "vue" diff --git a/flake.lock b/flake.lock index 80022f7b55..fa0d51d90d 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "crane": { "locked": { - "lastModified": 1754269165, - "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=", + "lastModified": 1750266157, + "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=", "owner": "ipetkov", "repo": "crane", - "rev": "444e81206df3f7d92780680e45858e31d2f07a08", + "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48", "type": "github" }, "original": { @@ -33,10 +33,10 @@ "nixpkgs": { "locked": { "lastModified": 315532800, - "narHash": "sha256-5VYevX3GccubYeccRGAXvCPA1ktrGmIX1IFC0icX07g=", - "rev": "a683adc19ff5228af548c6539dbc3440509bfed3", + "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=", + "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff", "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": { "type": "tarball", @@ -58,11 +58,11 @@ ] }, "locked": { - "lastModified": 1754575663, - "narHash": "sha256-afOx8AG0KYtw7mlt6s6ahBBy7eEHZwws3iCRoiuRQS4=", + "lastModified": 1750964660, + "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6db0fb0e9cec2e9729dc52bf4898e6c135bb8a0f", + "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390", "type": "github" }, "original": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 3d87025a27..f80eab8fbc 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.89" +channel = "1.88" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ diff --git a/script/install.sh b/script/install.sh index 9cd21119b7..feb140c984 100755 --- a/script/install.sh +++ b/script/install.sh @@ -9,7 +9,12 @@ main() { platform="$(uname -s)" arch="$(uname -m)" 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 platform="macos" diff --git a/script/lib/deploy-helpers.sh b/script/lib/deploy-helpers.sh index c0feb2f861..bd7b3c4d6f 100644 --- a/script/lib/deploy-helpers.sh +++ b/script/lib/deploy-helpers.sh @@ -5,7 +5,7 @@ function export_vars_for_environment { echo "Invalid environment name '${environment}'" >&2 exit 1 fi - export $(cat $env_file) + export $(grep -v '^#' $env_file | grep -v '^[[:space:]]*$') } function target_zed_kube_cluster {