Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions LICENSE-GPL
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007

UNSTAGED CHANGES

Copyright © 2007 Free Software Foundation, Inc. <https://fsf.org/>

Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Welcome to Zed, a high-performance, multiplayer code editor from the creators of

---

STAGED CHANGES

### Installation

On macOS, Linux, and Windows you can [download Zed directly](https://zed.dev/download) or install Zed via your local package manager ([macOS](https://zed.dev/docs/installation#macos)/[Linux](https://zed.dev/docs/linux#installing-via-a-package-manager)/[Windows](https://zed.dev/docs/windows#package-managers)).
Expand Down
493 changes: 493 additions & 0 deletions crates/gpui/docs/read_tracked_invalidation.md

Large diffs are not rendered by default.

37 changes: 18 additions & 19 deletions crates/gpui/examples/view_example/example_editor.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
//! `Editor` — the workhorse entity. It owns the cursor, blink, focus, keyboard
//! handling, and the specialized text-shaping renderer. The *text itself* lives
//! in a shared `Entity<String>` it's handed at construction, so the value is
//! behind a `ProjectionMut<String>` it's handed at construction, so the value is
//! readable/writable from outside while the editing machinery stays in here.
//!
//! Taking a projection rather than an `Entity<String>` is what lets one form
//! entity back several editors: the caller decides whether the text is a whole
//! entity or one field of a larger struct, and the editor can't tell.
//!
//! This is the piece that proves the point: a text input is genuinely
//! complicated, and `View` lets all of that complexity live in one entity that
//! anything can embed.
Expand All @@ -12,15 +16,16 @@ use std::time::Duration;

use gpui::{
App, Bounds, Context, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable,
InteractiveElement, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Subscription, Task,
TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px, relative, size,
InteractiveElement, LayoutId, PaintQuad, Pixels, ProjectionMut, ShapedLine, SharedString,
Subscription, Task, TextRun, UTF16Selection, Window, fill, hsla, point, prelude::*, px,
relative, size,
};
use unicode_segmentation::*;

use crate::{Backspace, Delete, End, Home, Left, Right};

pub struct Editor {
pub value: Entity<String>,
pub value: ProjectionMut<String>,
pub focus_handle: FocusHandle,
pub cursor: usize,
pub cursor_visible: bool,
Expand All @@ -32,12 +37,14 @@ impl Editor {
/// An editor that owns its own string internally, seeded with `text`.
/// Nothing to allocate or wire up at the call site.
pub fn new(text: impl Into<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let value = cx.new(|_| text.into());
// A whole entity converts into a projection of itself, so the editor
// below doesn't need a second code path for the owned case.
let value = cx.new(|_| text.into()).into();
Self::over(value, window, cx)
}

/// An editor over a string *you* own, so the value is shared in and out.
pub fn over(value: Entity<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
pub fn over(value: ProjectionMut<String>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();

let focus_sub = cx.on_focus(&focus_handle, window, |this, _window, cx| {
Expand All @@ -52,7 +59,7 @@ impl Editor {
// boundary before the next IME round-trip can slice out of bounds, and
// (b) notify us, so an `editor.cached(..)` subtree re-renders — the cache
// is keyed on *our* notify, not the value's.
let value_sub = cx.observe(&value, |this, value, cx| {
let value_sub = value.observe(cx, |this, value, cx| {
let content = value.read(cx);
let mut cursor = this.cursor.min(content.len());
while cursor > 0 && !content.is_char_boundary(cursor) {
Expand Down Expand Up @@ -145,9 +152,8 @@ impl Editor {
if self.cursor > 0 {
let prev = previous_boundary(&content, self.cursor);
let cursor = self.cursor;
self.value.update(cx, |s, cx| {
self.value.update(cx, |s| {
s.drain(prev..cursor);
cx.notify();
});
self.cursor = prev;
}
Expand All @@ -160,9 +166,8 @@ impl Editor {
if self.cursor < content.len() {
let next = next_boundary(&content, self.cursor);
let cursor = self.cursor;
self.value.update(cx, |s, cx| {
self.value.update(cx, |s| {
s.drain(cursor..next);
cx.notify();
});
}
self.reset_blink(cx);
Expand All @@ -171,10 +176,7 @@ impl Editor {

pub fn insert_newline(&mut self, cx: &mut Context<Self>) {
let cursor = self.cursor;
self.value.update(cx, |s, cx| {
s.insert(cursor, '\n');
cx.notify();
});
self.value.update(cx, |s| s.insert(cursor, '\n'));
self.cursor += 1;
self.reset_blink(cx);
cx.notify();
Expand Down Expand Up @@ -289,10 +291,7 @@ impl EntityInputHandler for Editor {

let new_content = content[..range.start].to_owned() + new_text + &content[range.end..];
self.cursor = range.start + new_text.len();
self.value.update(cx, |s, cx| {
*s = new_content;
cx.notify();
});
self.value.update(cx, |s| *s = new_content);
self.reset_blink(cx);
cx.notify();
}
Expand Down
35 changes: 21 additions & 14 deletions crates/gpui/examples/view_example/example_input.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
//! `Input` — a single-line text input. The shaping layer over `Editor`.
//!
//! Construct it two ways, depending on how much state you want to own:
//! * `Input::new(value: Entity<String>)` — you hold just the string; the input
//! allocates the `Editor` internally via `use_state`. Value readable, cursor hidden.
//! * `Input::new(value: ProjectionMut<String>)` — you hold just the text; the
//! input allocates the `Editor` internally via `use_state`. Value readable,
//! cursor hidden. The text can be a whole `Entity<String>` (via `.into()`) or
//! one field of a bigger struct (via `project!`) — the input can't tell.
//! * `Input::editor(editor: Entity<Editor>)` — you hold the editor; cursor/selection
//! are now yours to read and drive too.
//!
//! Either way the chrome is identical. Because the string (or editor) is the
//! input's *identity*, the internal `use_state(Editor)` is collision-safe across
//! any number of inputs.
//! Either way the chrome is identical.

use gpui::{
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, StyleRefinement,
Window, div, hsla, point, prelude::*, px, white,
App, BoxShadow, CursorStyle, Entity, EntityId, Hsla, IntoElement, Pixels, ProjectionMut,
StyleRefinement, Window, div, hsla, point, prelude::*, px, white,
};

use crate::example_editor::{Editor, standard_actions};

enum Source {
Value(Entity<String>),
Value(ProjectionMut<String>),
Editor(Entity<Editor>),
}

Expand All @@ -30,8 +30,8 @@ pub struct Input {
}

impl Input {
/// Backed by a bare string; the editor is allocated internally.
pub fn new(value: Entity<String>) -> Self {
/// Backed by a projected string; the editor is allocated internally.
pub fn new(value: ProjectionMut<String>) -> Self {
Self {
source: Source::Value(value),
width: None,
Expand Down Expand Up @@ -61,10 +61,17 @@ impl Input {

impl gpui::View for Input {
fn entity_id(&self) -> Option<EntityId> {
Some(match &self.source {
Source::Value(value) => value.entity_id(),
Source::Editor(editor) => editor.entity_id(),
})
match &self.source {
// A view's identity is the notify target for state allocated inside
// it. The editor below is allocated here and observes the value, so
// identifying this view by the value would route the editor's
// notifications back into the thing it observes and spin forever.
// Positional identity is correct here.
Source::Value(_) => None,
// Nothing is allocated in this branch, so the editor we were handed
// is a safe identity and survives moving around the tree.
Source::Editor(editor) => Some(editor.entity_id()),
}
}

fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
Expand Down
112 changes: 95 additions & 17 deletions crates/gpui/examples/view_example/example_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@

#[cfg(test)]
mod tests {
use gpui::{Context, Entity, KeyBinding, TestAppContext, Window, prelude::*};
use gpui::{
Context, Entity, IntoElement, KeyBinding, ProjectionMut, TestAppContext, Window,
prelude::*, project,
};

use crate::example_editor::Editor;
use crate::example_input::Input;
use crate::{Backspace, Delete, End, Home, Left, Right};

/// Two inputs, each backed by an editor we own (so the test can focus and
/// read them). Proves data flows through the shared `String` and that
/// read them). Proves data flows through the projected `String` and that
/// sibling inputs stay isolated.
struct Harness {
a: Entity<Editor>,
Expand Down Expand Up @@ -45,15 +48,17 @@ mod tests {
cx: &mut TestAppContext,
) -> (
Entity<Editor>,
Entity<String>,
Entity<String>,
ProjectionMut<String>,
ProjectionMut<String>,
&mut gpui::VisualTestContext,
) {
bind_keys(cx);

let (harness, cx) = cx.add_window_view(|window, cx| {
let a_value = cx.new(|_| String::new());
let b_value = cx.new(|_| String::new());
// A whole entity projects to itself, so an editor over an entity and
// an editor over one field of a form are the same thing to `Editor`.
let a_value = cx.new(|_| String::new()).into();
let b_value = cx.new(|_| String::new()).into();
let a = cx.new(|cx| Editor::over(a_value, window, cx));
let b = cx.new(|cx| Editor::over(b_value, window, cx));
Harness { a, b }
Expand All @@ -79,7 +84,7 @@ mod tests {

cx.simulate_input("hello");

cx.read_entity(&a_value, |value, _| assert_eq!(value, "hello"));
cx.update(|_, cx| assert_eq!(a_value.read(cx), "hello"));
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 5));
}

Expand All @@ -89,9 +94,13 @@ mod tests {

cx.simulate_input("x");

cx.read_entity(&a_value, |value, _| assert_eq!(value, "x"));
cx.read_entity(&b_value, |value, _| {
assert_eq!(value, "", "typing in input A must not touch input B")
cx.update(|_, cx| {
assert_eq!(a_value.read(cx), "x");
assert_eq!(
b_value.read(cx),
"",
"typing in input A must not touch input B"
);
});
}

Expand All @@ -105,14 +114,9 @@ mod tests {
// Write the shared value from outside the editor. The old cursor (5)
// now points into the middle of a multi-byte character; the editor's
// observation must clamp it back onto a boundary.
cx.update(|_, cx| {
a_value.update(cx, |value, cx| {
*value = "日本".to_string();
cx.notify();
})
});
cx.update(|_, cx| a_value.update(cx, |value| *value = "日本".to_string()));

cx.read_entity(&a_value, |value, _| assert_eq!(value, "日本"));
cx.update(|_, cx| assert_eq!(a_value.read(cx), "日本"));
cx.read_entity(&editor, |editor, _| {
assert_eq!(editor.cursor, 3, "cursor must clamp to a char boundary");
});
Expand All @@ -128,4 +132,78 @@ mod tests {
cx.simulate_keystrokes("left left");
cx.read_entity(&editor, |editor, _| assert_eq!(editor.cursor, 1));
}

/// Guards a feedback loop: a view's identity is the notify target for state
/// allocated inside it, so a subform identified by the projection it also
/// projects from will notify itself forever. This test hangs if that
/// regresses.
#[gpui::test]
fn nested_subforms_do_not_feed_back(cx: &mut TestAppContext) {
let (root, cx) = cx.add_window_view(|_, cx| SubformHarness {
profile: cx.new(|_| Profile {
primary: Person::default(),
secondary: Person::default(),
}),
});

let profile = cx.read_entity(&root, |root, _| root.profile.clone());

// Writing the source notifies the projections the subforms read, which
// in turn notify the editors allocated inside them. If any of those
// notifications routes back into the projection graph, this never
// settles.
cx.update(|_, cx| {
profile.update(cx, |profile, cx| {
profile.primary.name = "hi".to_string();
cx.notify();
})
});
cx.run_until_parked();

cx.read_entity(&profile, |profile, _| {
assert_eq!(profile.primary.name, "hi");
assert_eq!(profile.secondary.name, "", "subforms must stay isolated");
});
}

#[derive(Default)]
struct Person {
name: String,
}

struct Profile {
primary: Person,
secondary: Person,
}

/// Two instances of one subform over two projected people, mirroring the
/// example's `PersonForm`.
struct SubformHarness {
profile: Entity<Profile>,
}

impl Render for SubformHarness {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let primary = project!(window, cx, &self.profile, mut primary);
let secondary = project!(window, cx, &self.profile, mut secondary);
gpui::div()
.child(Subform { person: primary })
.child(Subform { person: secondary })
}
}

#[derive(IntoElement)]
struct Subform {
person: ProjectionMut<Person>,
}

impl gpui::View for Subform {
fn entity_id(&self) -> Option<gpui::EntityId> {
None
}

fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
Input::new(project!(window, cx, &self.person, mut name))
}
}
}
Loading
Loading