From 1e0481854ae7620ffb6c394dd947dc91646107b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Soares?= <37777652+Dnreikronos@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:51:21 -0300 Subject: [PATCH] editor: Hide run button in gutter for unsaved buffers (#53195) ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [X] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Closes #52942 ## Demo: ### Before: https://github.com/user-attachments/assets/fd3b69fc-468a-4fd1-82c9-4f1aa83c6474 ### After: https://github.com/user-attachments/assets/0a6c70a9-0a4a-4657-9fe8-21988fff9e80 ## Release Notes: - Fixed play button appearing in gutter for unsaved buffers where clicking it was a no-op. --------- Co-authored-by: Lukas Wirth --- crates/editor/src/runnables.rs | 105 +++++++++++++++++++++++++++++++-- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/runnables.rs b/crates/editor/src/runnables.rs index dc97a3ea310b5b519c13887996c3bb3c0d6274a8..b17b9944173629c347b8300a45b9f82f80b511b1 100644 --- a/crates/editor/src/runnables.rs +++ b/crates/editor/src/runnables.rs @@ -118,11 +118,14 @@ impl Editor { return; } if let Some(buffer) = self.buffer().read(cx).as_singleton() { - let buffer_id = buffer.read(cx).remote_id(); + let buffer_read = buffer.read(cx); + if buffer_read.file().is_none() { + self.clear_runnables(None); + return; + } + let buffer_id = buffer_read.remote_id(); if invalidate_buffer_data != Some(buffer_id) - && self - .runnables - .has_cached(buffer_id, &buffer.read(cx).version()) + && self.runnables.has_cached(buffer_id, &buffer_read.version()) { return; } @@ -711,13 +714,14 @@ mod tests { use lsp::LanguageServerName; use multi_buffer::{MultiBuffer, PathKey}; use project::{ - FakeFs, Project, + FakeFs, Project, ProjectPath, lsp_store::lsp_ext_command::{CargoRunnableArgs, Runnable, RunnableArgs, RunnableKind}, }; use serde_json::json; use task::{TaskTemplate, TaskTemplates}; use text::Point; use util::path; + use util::rel_path::rel_path; use crate::{ Editor, UPDATE_DEBOUNCE, editor_tests::init_test, scroll::scroll_amount::ScrollAmount, @@ -1079,4 +1083,95 @@ mod tests { "Runnables should be removed after #[test] is deleted and LSP returns empty" ); } + + #[gpui::test] + async fn test_no_runnables_for_unsaved_buffer(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/project"), json!({})).await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + language_registry.add(rust_lang_with_task_context()); + + let rust_language = language_registry.language_for_name("Rust").await.unwrap(); + let buffer = cx.new(|cx| { + let mut buffer = language::Buffer::local( + indoc! {" + fn main() { + println!(\"hello\"); + } + + #[test] + fn test_one() { + assert!(true); + } + "}, + cx, + ); + buffer.set_language(Some(rust_language), cx); + buffer + }); + + let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); + let editor = cx.add_window(|window, cx| { + build_editor_with_project(project.clone(), multi_buffer, window, cx) + }); + + editor + .update(cx, |editor, window, cx| { + editor.refresh_runnables(None, window, cx); + }) + .expect("editor update"); + cx.executor().advance_clock(UPDATE_DEBOUNCE); + cx.executor().run_until_parked(); + + let labels = editor + .update(cx, |editor, _, _| collect_runnable_labels(editor)) + .expect("editor update"); + assert_eq!( + labels, + Vec::<(text::BufferId, language::BufferRow, Vec)>::new(), + "No runnables should appear for an unsaved buffer without a file on disk" + ); + + let worktree_id = project.update(cx, |project, cx| { + project + .worktrees(cx) + .next() + .expect("worktree") + .read(cx) + .id() + }); + project + .update(cx, |project, cx| { + project.save_buffer_as( + buffer.clone(), + ProjectPath { + worktree_id, + path: rel_path("main.rs").into(), + }, + cx, + ) + }) + .await + .expect("save buffer as"); + + editor + .update(cx, |editor, window, cx| { + editor.refresh_runnables(None, window, cx); + }) + .expect("editor update"); + cx.executor().advance_clock(UPDATE_DEBOUNCE); + cx.executor().run_until_parked(); + + let labels = editor + .update(cx, |editor, _, _| collect_runnable_labels(editor)) + .expect("editor update"); + assert!( + !labels.is_empty(), + "Runnables should appear after the buffer is saved to disk" + ); + } }