Add a setting to control the number of context lines in excerpts (#37138)

Mikayla Maki created

Fixes https://github.com/zed-industries/zed/discussions/28739

Release Notes:

- Added a setting, `excerpt_context_lines`, for setting the number of
context lines shown in a multibuffer

Change summary

assets/settings/default.json                 |  2 +
crates/acp_thread/src/diff.rs                |  8 +++---
crates/agent_ui/src/agent_diff.rs            |  3 +
crates/assistant_tools/src/edit_file_tool.rs | 10 +++++---
crates/diagnostics/src/diagnostics.rs        |  6 +++-
crates/editor/src/editor.rs                  | 12 ++++++++--
crates/editor/src/editor_settings.rs         |  6 +++++
crates/editor/src/editor_tests.rs            |  2 
crates/git_ui/src/commit_view.rs             |  4 +-
crates/git_ui/src/project_diff.rs            |  3 +
crates/search/src/project_search.rs          |  3 +
crates/settings/src/settings_store.rs        | 23 ++++++++++++++++++++++
docs/src/configuring-zed.md                  | 10 +++++++++
docs/src/development/releases.md             |  2 -
docs/src/visual-customization.md             |  4 ++
15 files changed, 76 insertions(+), 22 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -279,6 +279,8 @@
   "redact_private_values": false,
   // The default number of lines to expand excerpts in the multibuffer by.
   "expand_excerpt_lines": 5,
+  // The default number of context lines shown in multibuffer excerpts.
+  "excerpt_context_lines": 2,
   // Globs to match against file paths to determine if a file is private.
   "private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
   // Whether to use additional LSP queries to format (and amend) the code after

crates/acp_thread/src/diff.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::Result;
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{MultiBuffer, PathKey};
+use editor::{MultiBuffer, PathKey, multibuffer_context_lines};
 use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task};
 use itertools::Itertools;
 use language::{
@@ -64,7 +64,7 @@ impl Diff {
                             PathKey::for_buffer(&buffer, cx),
                             buffer.clone(),
                             hunk_ranges,
-                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                            multibuffer_context_lines(cx),
                             cx,
                         );
                         multibuffer.add_diff(diff, cx);
@@ -279,7 +279,7 @@ impl PendingDiff {
                         path_key,
                         buffer,
                         ranges,
-                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                        multibuffer_context_lines(cx),
                         cx,
                     );
                     multibuffer.add_diff(buffer_diff.clone(), cx);
@@ -305,7 +305,7 @@ impl PendingDiff {
                 PathKey::for_buffer(&self.new_buffer, cx),
                 self.new_buffer.clone(),
                 ranges,
-                editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                multibuffer_context_lines(cx),
                 cx,
             );
             let end = multibuffer.len(cx);

crates/agent_ui/src/agent_diff.rs 🔗

@@ -10,6 +10,7 @@ use editor::{
     Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
     SelectionEffects, ToPoint,
     actions::{GoToHunk, GoToPreviousHunk},
+    multibuffer_context_lines,
     scroll::Autoscroll,
 };
 use gpui::{
@@ -257,7 +258,7 @@ impl AgentDiffPane {
                         path_key.clone(),
                         buffer.clone(),
                         diff_hunk_ranges,
-                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                        multibuffer_context_lines(cx),
                         cx,
                     );
                     multibuffer.add_diff(diff_handle, cx);

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -11,7 +11,9 @@ use assistant_tool::{
     AnyToolCard, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
 };
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
+use editor::{
+    Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, multibuffer_context_lines,
+};
 use futures::StreamExt;
 use gpui::{
     Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -474,7 +476,7 @@ impl Tool for EditFileTool {
                             PathKey::for_buffer(&buffer, cx),
                             buffer,
                             diff_hunk_ranges,
-                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                            multibuffer_context_lines(cx),
                             cx,
                         );
                         multibuffer.add_diff(buffer_diff, cx);
@@ -703,7 +705,7 @@ impl EditFileToolCard {
                 PathKey::for_buffer(buffer, cx),
                 buffer.clone(),
                 ranges,
-                editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                multibuffer_context_lines(cx),
                 cx,
             );
             let end = multibuffer.len(cx);
@@ -791,7 +793,7 @@ impl EditFileToolCard {
                         path_key,
                         buffer,
                         ranges,
-                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                        multibuffer_context_lines(cx),
                         cx,
                     );
                     multibuffer.add_diff(buffer_diff.clone(), cx);

crates/diagnostics/src/diagnostics.rs 🔗

@@ -10,8 +10,9 @@ use anyhow::Result;
 use collections::{BTreeSet, HashMap};
 use diagnostic_renderer::DiagnosticBlock;
 use editor::{
-    DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
+    Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
     display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
+    multibuffer_context_lines,
 };
 use gpui::{
     AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
@@ -493,10 +494,11 @@ impl ProjectDiagnosticsEditor {
             }
 
             let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
+            let context_lines = cx.update(|_, cx| multibuffer_context_lines(cx))?;
             for b in blocks.iter() {
                 let excerpt_range = context_range_for_entry(
                     b.initial_range.clone(),
-                    DEFAULT_MULTIBUFFER_CONTEXT,
+                    context_lines,
                     buffer_snapshot.clone(),
                     cx,
                 )

crates/editor/src/editor.rs 🔗

@@ -219,7 +219,6 @@ use crate::{
 
 pub const FILE_HEADER_HEIGHT: u32 = 2;
 pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1;
-pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2;
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
@@ -6402,7 +6401,7 @@ impl Editor {
                     PathKey::for_buffer(buffer_handle, cx),
                     buffer_handle.clone(),
                     edited_ranges,
-                    DEFAULT_MULTIBUFFER_CONTEXT,
+                    multibuffer_context_lines(cx),
                     cx,
                 );
 
@@ -16237,7 +16236,7 @@ impl Editor {
                     PathKey::for_buffer(&location.buffer, cx),
                     location.buffer.clone(),
                     ranges_for_buffer,
-                    DEFAULT_MULTIBUFFER_CONTEXT,
+                    multibuffer_context_lines(cx),
                     cx,
                 );
                 ranges.extend(new_ranges)
@@ -24078,3 +24077,10 @@ fn render_diff_hunk_controls(
         )
         .into_any_element()
 }
+
+pub fn multibuffer_context_lines(cx: &App) -> u32 {
+    EditorSettings::try_get(cx)
+        .map(|settings| settings.excerpt_context_lines)
+        .unwrap_or(2)
+        .clamp(1, 32)
+}

crates/editor/src/editor_settings.rs 🔗

@@ -37,6 +37,7 @@ pub struct EditorSettings {
     pub multi_cursor_modifier: MultiCursorModifier,
     pub redact_private_values: bool,
     pub expand_excerpt_lines: u32,
+    pub excerpt_context_lines: u32,
     pub middle_click_paste: bool,
     #[serde(default)]
     pub double_click_in_multibuffer: DoubleClickInMultibuffer,
@@ -515,6 +516,11 @@ pub struct EditorSettingsContent {
     /// Default: 3
     pub expand_excerpt_lines: Option<u32>,
 
+    /// How many lines of context to provide in multibuffer excerpts by default
+    ///
+    /// Default: 2
+    pub excerpt_context_lines: Option<u32>,
+
     /// Whether to enable middle-click paste on Linux
     ///
     /// Default: true

crates/editor/src/editor_tests.rs 🔗

@@ -19867,7 +19867,7 @@ async fn test_display_diff_hunks(cx: &mut TestAppContext) {
                 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
                 buffer.clone(),
                 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
-                DEFAULT_MULTIBUFFER_CONTEXT,
+                2,
                 cx,
             );
         }

crates/git_ui/src/commit_view.rs 🔗

@@ -1,6 +1,6 @@
 use anyhow::{Context as _, Result};
 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects};
+use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects, multibuffer_context_lines};
 use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
 use gpui::{
     AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
@@ -195,7 +195,7 @@ impl CommitView {
                             PathKey::namespaced(FILE_NAMESPACE, path),
                             buffer,
                             diff_hunk_ranges,
-                            editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                            multibuffer_context_lines(cx),
                             cx,
                         );
                         multibuffer.add_diff(buffer_diff, cx);

crates/git_ui/src/project_diff.rs 🔗

@@ -10,6 +10,7 @@ use collections::HashSet;
 use editor::{
     Editor, EditorEvent, SelectionEffects,
     actions::{GoToHunk, GoToPreviousHunk},
+    multibuffer_context_lines,
     scroll::Autoscroll,
 };
 use futures::StreamExt;
@@ -465,7 +466,7 @@ impl ProjectDiff {
                 path_key.clone(),
                 buffer,
                 excerpt_ranges,
-                editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                multibuffer_context_lines(cx),
                 cx,
             );
             (was_empty, is_newly_added)

crates/search/src/project_search.rs 🔗

@@ -11,6 +11,7 @@ use editor::{
     Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, SelectionEffects,
     actions::{Backtab, SelectAll, Tab},
     items::active_match_index,
+    multibuffer_context_lines,
 };
 use futures::{StreamExt, stream::FuturesOrdered};
 use gpui::{
@@ -345,7 +346,7 @@ impl ProjectSearch {
                                     excerpts.set_anchored_excerpts_for_path(
                                         buffer,
                                         ranges,
-                                        editor::DEFAULT_MULTIBUFFER_CONTEXT,
+                                        multibuffer_context_lines(cx),
                                         cx,
                                     )
                                 })

crates/settings/src/settings_store.rs 🔗

@@ -103,6 +103,18 @@ pub trait Settings: 'static + Send + Sync {
         cx.global::<SettingsStore>().get(None)
     }
 
+    #[track_caller]
+    fn try_get(cx: &App) -> Option<&Self>
+    where
+        Self: Sized,
+    {
+        if cx.has_global::<SettingsStore>() {
+            cx.global::<SettingsStore>().try_get(None)
+        } else {
+            None
+        }
+    }
+
     #[track_caller]
     fn try_read_global<R>(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option<R>
     where
@@ -407,6 +419,17 @@ impl SettingsStore {
             .expect("no default value for setting type")
     }
 
+    /// Get the value of a setting.
+    ///
+    /// Panics if the given setting type has not been registered, or if there is no
+    /// value for this setting.
+    pub fn try_get<T: Settings>(&self, path: Option<SettingsLocation>) -> Option<&T> {
+        self.setting_values
+            .get(&TypeId::of::<T>())
+            .map(|value| value.value_for_path(path))
+            .and_then(|value| value.downcast_ref::<T>())
+    }
+
     /// Get all values from project specific settings
     pub fn get_all_locals<T: Settings>(&self) -> Vec<(WorktreeId, Arc<Path>, &T)> {
         self.setting_values

docs/src/configuring-zed.md 🔗

@@ -1461,6 +1461,16 @@ This setting enables integration with macOS’s native window tabbing feature. W
 
 Positive `integer` values
 
+## Excerpt Context Lines
+
+- Description: The number of lines of context to provide when showing excerpts in the multibuffer.
+- Setting: `excerpt_context_lines`
+- Default: `2`
+
+**Options**
+
+Positive `integer` value between 1 and 32. Values outside of this range will be clamped to this range.
+
 ## Extend Comment On Newline
 
 - Description: Whether to start a new line with a comment when a previous line is a comment as well.

docs/src/development/releases.md 🔗

@@ -51,7 +51,6 @@ Credentials for various services used in this process can be found in 1Password.
    - We sometimes correct things here and there that didn't translate from GitHub's renderer to Kit's.
 
 1. Build social media posts based on the popular items in stable.
-
    - You can use the [prior week's post chain](https://zed.dev/channel/tweets-23331) as your outline.
    - Stage the copy and assets using [Buffer](https://buffer.com), for both X and BlueSky.
    - Publish both, one at a time, ensuring both are posted to each respective platform.
@@ -89,7 +88,6 @@ You will need write access to the Zed repository to do this:
    - Download the artifacts for each release draft and test that you can run them locally.
 
 1. Publish stable / preview drafts, one at a time.
-
    - Use [Vercel](https://vercel.com/zed-industries/zed-dev) to check the progress of the website rebuild.
      The release will be public once the rebuild has completed.
 

docs/src/visual-customization.md 🔗

@@ -334,7 +334,9 @@ TBD: Centered layout related settings
 ```json
 {
   // The default number of lines to expand excerpts in the multibuffer by.
-  "expand_excerpt_lines": 5
+  "expand_excerpt_lines": 5,
+  // The default number of lines of context provided for excerpts in the multibuffer by.
+  "excerpt_context_lines": 2
 }
 ```