Fix NewWindow flicker by creating buffer synchronously (#44915)

Copilot , copilot-swe-agent[bot] , ConradIrwin , and Conrad Irwin created

Closes #20613

Release Notes:

- Fixed: New windows no longer flicker between "Open a file or project
to get started" and an empty editor.

---

When opening a new window (`cmd-shift-n`), the window rendered showing
the empty state message before the editor was created, causing a visible
flicker.

**Changes:**

- Modified `Workspace::new_local` to accept an optional `init` callback
that executes inside the window build closure
- The init callback runs within `cx.new` (the `build_root_view`
closure), before `window.draw()` is called for the first render
- Changed the NewWindow action handler to use
`Project::create_local_buffer()` (synchronous) instead of
`Editor::new_file()` (asynchronous)
- Updated `open_new` to pass the editor creation callback to `new_local`
- All other `new_local` call sites pass `None` to maintain existing
behavior

**Key Technical Detail:**

The window creation sequence in `cx.open_window()` is:
1. `build_root_view` closure is called (creates workspace via `cx.new`)
2. `window.draw(cx)` is called (first render)
3. `open_window` returns

The fix uses `Project::create_local_buffer()` which creates a buffer
**synchronously** (returns `Entity<Buffer>` directly), rather than
`Editor::new_file()` which is asynchronous (calls
`project.create_buffer()` which returns a `Task`). The editor is created
from this buffer inside the `cx.new` closure (step 1), ensuring it
exists before step 2 renders the first frame.

**Before:**
```rust
let task = Workspace::new_local(Vec::new(), app_state, None, env, cx);
cx.spawn(async move |cx| {
    let (workspace, _) = task.await?;  // Window already drawn
    workspace.update(cx, |workspace, window, cx| {
        Editor::new_file(workspace, ...)  // Async - editor not present for first render
    })?;
})
```

**After:**
```rust
cx.open_window(options, {
    move |window, cx| {
        cx.new(|cx| {
            let mut workspace = Workspace::new(...);
            // Create buffer synchronously, then create editor
            if let Some(init) = init {
                init(&mut workspace, window, cx);  // Uses create_local_buffer (sync)
            }
            workspace
        })
    }
})?
```

The editor is now part of the workspace before the window's first frame
is rendered, eliminating the flicker.

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Opening a new window flickers before opening an empty
buffer</issue_title>
> <issue_description>### Check for existing issues
> 
> - [x] Completed
> 
> ### Describe the bug / provide steps to reproduce it
> 
> Opening a new window, with e.g. `cmd-shift-n`, flickers for a fraction
of a second. The new window first shows the startup page, "Open a file
or project to get started.". Then, a frame or two later, a new empty
buffer opens.
> 
> Not sure if I'm sensitive or something but these kinds of flashes can
knock me out of focus/flow pretty easily.
> 
> It'd be great to either have the empty buffer open from the first
frame, or to have an option to simply not open that empty buffer when a
new window is opened.
> 
> ### Zed Version and System Specs
> 
> Zed: v0.170.4 (Zed)
> OS: macOS 14.6.1
> Memory: 36 GiB
> Architecture: aarch64
> 
> ### If applicable, add screenshots or screencasts of the incorrect
state / behavior
> 
>
https://github.com/user-attachments/assets/6d9ba791-8a02-4e13-857c-66a33eb0905b
> 
> ### If applicable, attach your Zed.log file to this issue.
> 
> N/A</issue_description>
> 
> <agent_instructions>We should make sure that the window is created in
the correct state, and not have an intermediate render before the editor
opens.</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@ConradIrwin</author><body>
> Ugh, no. I don't believe I never noticed this before, but now I can't
unsee it :s
> 
> If you'd like to pair on this: https://cal.com/conradirwin/pairing,
otherwise I'll see if I get around to it.</body></comment_new>
> <comment_new><author>@ConradIrwin</author><body>
> Yeah... I wonder if that can be a preview tab or something. It's nice
when you want it, but not so nice when you don't.
> 
> Fixing this will also make zed-industries/zed#33334 feel much
smoother.</body></comment_new>
> <comment_new><author>@zelenenka</author><body>
> @robinplace do you maybe have an opportunity to test it with the
latest stable version, 0.213.3?</body></comment_new>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes zed-industries/zed#23742

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ConradIrwin <94272+ConradIrwin@users.noreply.github.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/workspace/src/workspace.rs | 46 +++++++++++++++++++++++++-------
crates/zed/src/zed.rs             | 16 ++++++++++
2 files changed, 50 insertions(+), 12 deletions(-)

Detailed changes

crates/workspace/src/workspace.rs 🔗

@@ -1627,6 +1627,7 @@ impl Workspace {
         app_state: Arc<AppState>,
         requesting_window: Option<WindowHandle<Workspace>>,
         env: Option<HashMap<String, String>>,
+        init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
         cx: &mut App,
     ) -> Task<
         anyhow::Result<(
@@ -1734,6 +1735,12 @@ impl Workspace {
                         );
 
                         workspace.centered_layout = centered_layout;
+
+                        // Call init callback to add items before window renders
+                        if let Some(init) = init {
+                            init(&mut workspace, window, cx);
+                        }
+
                         workspace
                     });
                 })?;
@@ -1785,6 +1792,12 @@ impl Workspace {
                                 cx,
                             );
                             workspace.centered_layout = centered_layout;
+
+                            // Call init callback to add items before window renders
+                            if let Some(init) = init {
+                                init(&mut workspace, window, cx);
+                            }
+
                             workspace
                         })
                     }
@@ -2350,7 +2363,7 @@ impl Workspace {
             Task::ready(Ok(callback(self, window, cx)))
         } else {
             let env = self.project.read(cx).cli_environment(cx);
-            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
+            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
             cx.spawn_in(window, async move |_vh, cx| {
                 let (workspace, _) = task.await?;
                 workspace.update(cx, callback)
@@ -7758,7 +7771,14 @@ pub fn join_channel(
             // no open workspaces, make one to show the error in (blergh)
             let (window_handle, _) = cx
                 .update(|cx| {
-                    Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx)
+                    Workspace::new_local(
+                        vec![],
+                        app_state.clone(),
+                        requesting_window,
+                        None,
+                        None,
+                        cx,
+                    )
                 })?
                 .await?;
 
@@ -7824,7 +7844,7 @@ pub async fn get_any_active_workspace(
     // find an existing workspace to focus and show call controls
     let active_window = activate_any_workspace_window(&mut cx);
     if active_window.is_none() {
-        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))?
+        cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, None, cx))?
             .await?;
     }
     activate_any_workspace_window(&mut cx).context("could not open zed")
@@ -7991,6 +8011,7 @@ pub fn open_paths(
                     app_state.clone(),
                     open_options.replace_window,
                     open_options.env,
+                    None,
                     cx,
                 )
             })?
@@ -8035,14 +8056,17 @@ pub fn open_new(
     cx: &mut App,
     init: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + 'static + Send,
 ) -> Task<anyhow::Result<()>> {
-    let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx);
-    cx.spawn(async move |cx| {
-        let (workspace, opened_paths) = task.await?;
-        workspace.update(cx, |workspace, window, cx| {
-            if opened_paths.is_empty() {
-                init(workspace, window, cx)
-            }
-        })?;
+    let task = Workspace::new_local(
+        Vec::new(),
+        app_state,
+        None,
+        open_options.env,
+        Some(Box::new(init)),
+        cx,
+    );
+    cx.spawn(async move |_cx| {
+        let (_workspace, _opened_paths) = task.await?;
+        // Init callback is called synchronously during workspace creation
         Ok(())
     })
 }

crates/zed/src/zed.rs 🔗

@@ -1111,7 +1111,21 @@ fn register_actions(
                         cx,
                         |workspace, window, cx| {
                             cx.activate(true);
-                            Editor::new_file(workspace, &Default::default(), window, cx)
+                            // Create buffer synchronously to avoid flicker
+                            let project = workspace.project().clone();
+                            let buffer = project.update(cx, |project, cx| {
+                                project.create_local_buffer("", None, true, cx)
+                            });
+                            let editor = cx.new(|cx| {
+                                Editor::for_buffer(buffer, Some(project), window, cx)
+                            });
+                            workspace.add_item_to_active_pane(
+                                Box::new(editor),
+                                None,
+                                true,
+                                window,
+                                cx,
+                            );
                         },
                     )
                     .detach();