Track read_only per project and buffer

Conrad Irwin created

This uses a new enum to avoid confusing booleans

Change summary

crates/assistant/src/assistant_panel.rs |  6 +-
crates/channel/src/channel_buffer.rs    |  7 +++
crates/channel/src/channel_store.rs     |  9 ++++-
crates/collab_ui/src/channel_view.rs    | 13 +------
crates/diagnostics/src/diagnostics.rs   |  7 +++
crates/editor/src/editor.rs             | 28 +++++++++--------
crates/editor/src/editor_tests.rs       | 21 +++++++------
crates/editor/src/git.rs                |  3 +
crates/editor/src/inlay_hint_cache.rs   |  7 ++-
crates/editor/src/items.rs              |  3 +
crates/editor/src/movement.rs           |  3 +
crates/language/src/buffer.rs           | 28 +++++++++++++++++
crates/language/src/buffer_tests.rs     | 14 ++++++--
crates/multi_buffer/src/multi_buffer.rs | 39 +++++++++++++++----------
crates/project/src/project.rs           | 41 +++++++++++++++-----------
crates/project/src/worktree.rs          | 12 ++++++-
crates/search/src/buffer_search.rs      |  2 
crates/search/src/project_search.rs     |  6 ++-
script/sqlx                             |  2 
19 files changed, 161 insertions(+), 90 deletions(-)

Detailed changes

crates/assistant/src/assistant_panel.rs 🔗

@@ -2818,8 +2818,8 @@ impl InlineAssistant {
 
     fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
         let is_read_only = !self.codegen.read(cx).idle();
-        self.prompt_editor.update(cx, |editor, _cx| {
-            let was_read_only = editor.read_only();
+        self.prompt_editor.update(cx, |editor, cx| {
+            let was_read_only = editor.read_only(cx);
             if was_read_only != is_read_only {
                 if is_read_only {
                     editor.set_read_only(true);
@@ -3054,7 +3054,7 @@ impl InlineAssistant {
     fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
         let text_style = TextStyle {
-            color: if self.prompt_editor.read(cx).read_only() {
+            color: if self.prompt_editor.read(cx).read_only(cx) {
                 cx.theme().colors().text_disabled
             } else {
                 cx.theme().colors().text

crates/channel/src/channel_buffer.rs 🔗

@@ -62,7 +62,12 @@ impl ChannelBuffer {
             .collect::<Result<Vec<_>, _>>()?;
 
         let buffer = cx.new_model(|_| {
-            language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
+            language::Buffer::remote(
+                response.buffer_id,
+                response.replica_id as u16,
+                channel.channel_buffer_capability(),
+                base_text,
+            )
         })?;
         buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))??;
 

crates/channel/src/channel_store.rs 🔗

@@ -11,6 +11,7 @@ use gpui::{
     AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
     WeakModel,
 };
+use language::Capability;
 use rpc::{
     proto::{self, ChannelVisibility},
     TypedEnvelope,
@@ -74,8 +75,12 @@ impl Channel {
         slug.trim_matches(|c| c == '-').to_string()
     }
 
-    pub fn can_edit_notes(&self) -> bool {
-        self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin
+    pub fn channel_buffer_capability(&self) -> Capability {
+        if self.role == proto::ChannelRole::Member || self.role == proto::ChannelRole::Admin {
+            Capability::ReadWrite
+        } else {
+            Capability::ReadOnly
+        }
     }
 }
 

crates/collab_ui/src/channel_view.rs 🔗

@@ -138,12 +138,6 @@ impl ChannelView {
             editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
                 channel_buffer.clone(),
             )));
-            editor.set_read_only(
-                !channel_buffer
-                    .read(cx)
-                    .channel(cx)
-                    .is_some_and(|c| c.can_edit_notes()),
-            );
             editor
         });
         let _editor_event_subscription =
@@ -179,7 +173,6 @@ impl ChannelView {
             }),
             ChannelBufferEvent::ChannelChanged => {
                 self.editor.update(cx, |editor, cx| {
-                    editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
                     cx.emit(editor::EditorEvent::TitleChanged);
                     cx.notify()
                 });
@@ -254,11 +247,11 @@ impl Item for ChannelView {
     fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
         let label = if let Some(channel) = self.channel(cx) {
             match (
-                channel.can_edit_notes(),
+                self.channel_buffer.read(cx).buffer().read(cx).read_only(),
                 self.channel_buffer.read(cx).is_connected(),
             ) {
-                (true, true) => format!("#{}", channel.name),
-                (false, true) => format!("#{} (read-only)", channel.name),
+                (false, true) => format!("#{}", channel.name),
+                (true, true) => format!("#{} (read-only)", channel.name),
                 (_, false) => format!("#{} (disconnected)", channel.name),
             }
         } else {

crates/diagnostics/src/diagnostics.rs 🔗

@@ -151,7 +151,12 @@ impl ProjectDiagnosticsEditor {
         let focus_in_subscription =
             cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
 
-        let excerpts = cx.new_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
+        let excerpts = cx.new_model(|cx| {
+            MultiBuffer::new(
+                project_handle.read(cx).replica_id(),
+                project_handle.read(cx).capability(),
+            )
+        });
         let editor = cx.new_view(|cx| {
             let mut editor =
                 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx);

crates/editor/src/editor.rs 🔗

@@ -54,10 +54,10 @@ use itertools::Itertools;
 pub use language::{char_kind, CharKind};
 use language::{
     language_settings::{self, all_language_settings, InlayHintSettings},
-    markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
-    Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language,
-    LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal,
-    TransactionId,
+    markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction,
+    CodeLabel, Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize,
+    Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection,
+    SelectionGoal, TransactionId,
 };
 
 use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
@@ -2050,8 +2050,8 @@ impl Editor {
         }
     }
 
-    pub fn read_only(&self) -> bool {
-        self.read_only
+    pub fn read_only(&self, cx: &AppContext) -> bool {
+        self.read_only || self.buffer.read(cx).read_only()
     }
 
     pub fn set_read_only(&mut self, read_only: bool) {
@@ -2200,7 +2200,7 @@ impl Editor {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
-        if self.read_only {
+        if self.read_only(cx) {
             return;
         }
 
@@ -2214,7 +2214,7 @@ impl Editor {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
-        if self.read_only {
+        if self.read_only(cx) {
             return;
         }
 
@@ -2233,7 +2233,7 @@ impl Editor {
         S: ToOffset,
         T: Into<Arc<str>>,
     {
-        if self.read_only {
+        if self.read_only(cx) {
             return;
         }
 
@@ -2597,7 +2597,7 @@ impl Editor {
     pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
         let text: Arc<str> = text.into();
 
-        if self.read_only {
+        if self.read_only(cx) {
             return;
         }
 
@@ -3050,7 +3050,7 @@ impl Editor {
         autoindent_mode: Option<AutoindentMode>,
         cx: &mut ViewContext<Self>,
     ) {
-        if self.read_only {
+        if self.read_only(cx) {
             return;
         }
 
@@ -3787,7 +3787,8 @@ impl Editor {
 
         let mut ranges_to_highlight = Vec::new();
         let excerpt_buffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
+            let mut multibuffer =
+                MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
             for (buffer_handle, transaction) in &entries {
                 let buffer = buffer_handle.read(cx);
                 ranges_to_highlight.extend(
@@ -7492,9 +7493,10 @@ impl Editor {
         locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
         let mut locations = locations.into_iter().peekable();
         let mut ranges_to_highlight = Vec::new();
+        let capability = workspace.project().read(cx).capability();
 
         let excerpt_buffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(replica_id);
+            let mut multibuffer = MultiBuffer::new(replica_id, capability);
             while let Some(location) = locations.next() {
                 let buffer = location.buffer.read(cx);
                 let mut ranges_for_buffer = Vec::new();

crates/editor/src/editor_tests.rs 🔗

@@ -17,8 +17,9 @@ use gpui::{
 use indoc::indoc;
 use language::{
     language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
-    BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
-    Override, Point,
+    BracketPairConfig,
+    Capability::ReadWrite,
+    FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry, Override, Point,
 };
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -2355,7 +2356,7 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
             .with_language(rust_language, cx)
     });
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         multibuffer.push_excerpts(
             toml_buffer.clone(),
             [ExcerptRange {
@@ -6019,7 +6020,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
 
     let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         multibuffer.push_excerpts(
             buffer.clone(),
             [
@@ -6103,7 +6104,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
     });
     let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), initial_text));
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
         multibuffer
     });
@@ -6162,7 +6163,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
     let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
     let mut excerpt1_id = None;
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         excerpt1_id = multibuffer
             .push_excerpts(
                 buffer.clone(),
@@ -6247,7 +6248,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
     let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(3, 4, 'a')));
     let mut excerpt1_id = None;
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         excerpt1_id = multibuffer
             .push_excerpts(
                 buffer.clone(),
@@ -6636,7 +6637,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
 
     let leader = pane.update(cx, |_, cx| {
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, ReadWrite));
         cx.new_view(|cx| build_editor(multibuffer.clone(), cx))
     });
 
@@ -7425,7 +7426,7 @@ async fn test_copilot_multibuffer(executor: BackgroundExecutor, cx: &mut gpui::T
     let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "a = 1\nb = 2\n"));
     let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "c = 3\nd = 4\n"));
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         multibuffer.push_excerpts(
             buffer_1.clone(),
             [ExcerptRange {
@@ -7552,7 +7553,7 @@ async fn test_copilot_disabled_globs(executor: BackgroundExecutor, cx: &mut gpui
         .unwrap();
 
     let multibuffer = cx.new_model(|cx| {
-        let mut multibuffer = MultiBuffer::new(0);
+        let mut multibuffer = MultiBuffer::new(0, ReadWrite);
         multibuffer.push_excerpts(
             private_buffer.clone(),
             [ExcerptRange {

crates/editor/src/git.rs 🔗

@@ -93,6 +93,7 @@ mod tests {
     use crate::editor_tests::init_test;
     use crate::Point;
     use gpui::{Context, TestAppContext};
+    use language::Capability::ReadWrite;
     use multi_buffer::{ExcerptRange, MultiBuffer};
     use project::{FakeFs, Project};
     use unindent::Unindent;
@@ -183,7 +184,7 @@ mod tests {
         cx.background_executor.run_until_parked();
 
         let multibuffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
+            let mut multibuffer = MultiBuffer::new(0, ReadWrite);
             multibuffer.push_excerpts(
                 buffer_1.clone(),
                 [

crates/editor/src/inlay_hint_cache.rs 🔗

@@ -1206,7 +1206,8 @@ pub mod tests {
     use gpui::{Context, TestAppContext, WindowHandle};
     use itertools::Itertools;
     use language::{
-        language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+        language_settings::AllLanguageSettingsContent, Capability, FakeLspAdapter, Language,
+        LanguageConfig,
     };
     use lsp::FakeLanguageServer;
     use parking_lot::Mutex;
@@ -2459,7 +2460,7 @@ pub mod tests {
             .await
             .unwrap();
         let multibuffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
+            let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
             multibuffer.push_excerpts(
                 buffer_1.clone(),
                 [
@@ -2798,7 +2799,7 @@ pub mod tests {
             })
             .await
             .unwrap();
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
             let buffer_1_excerpts = multibuffer.push_excerpts(
                 buffer_1.clone(),

crates/editor/src/items.rs 🔗

@@ -101,7 +101,8 @@ impl FollowableItem for Editor {
                         if state.singleton && buffers.len() == 1 {
                             multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
                         } else {
-                            multibuffer = MultiBuffer::new(replica_id);
+                            multibuffer =
+                                MultiBuffer::new(replica_id, project.read(cx).capability());
                             let mut excerpts = state.excerpts.into_iter().peekable();
                             while let Some(excerpt) = excerpts.peek() {
                                 let buffer_id = excerpt.buffer_id;

crates/editor/src/movement.rs 🔗

@@ -461,6 +461,7 @@ mod tests {
         Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
     };
     use gpui::{font, Context as _};
+    use language::Capability;
     use project::Project;
     use settings::SettingsStore;
     use util::post_inc;
@@ -766,7 +767,7 @@ mod tests {
             let buffer =
                 cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abc\ndefg\nhijkl\nmn"));
             let multibuffer = cx.new_model(|cx| {
-                let mut multibuffer = MultiBuffer::new(0);
+                let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
                 multibuffer.push_excerpts(
                     buffer.clone(),
                     [

crates/language/src/buffer.rs 🔗

@@ -57,6 +57,12 @@ lazy_static! {
     pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
 }
 
+#[derive(PartialEq, Clone, Copy, Debug)]
+pub enum Capability {
+    ReadWrite,
+    ReadOnly,
+}
+
 pub struct Buffer {
     text: TextBuffer,
     diff_base: Option<String>,
@@ -90,6 +96,7 @@ pub struct Buffer {
     completion_triggers: Vec<String>,
     completion_triggers_timestamp: clock::Lamport,
     deferred_ops: OperationQueue<Operation>,
+    capability: Capability,
 }
 
 pub struct BufferSnapshot {
@@ -405,19 +412,27 @@ impl Buffer {
             TextBuffer::new(replica_id, id, base_text.into()),
             None,
             None,
+            Capability::ReadWrite,
         )
     }
 
-    pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self {
+    pub fn remote(
+        remote_id: u64,
+        replica_id: ReplicaId,
+        capability: Capability,
+        base_text: String,
+    ) -> Self {
         Self::build(
             TextBuffer::new(replica_id, remote_id, base_text),
             None,
             None,
+            capability,
         )
     }
 
     pub fn from_proto(
         replica_id: ReplicaId,
+        capability: Capability,
         message: proto::BufferState,
         file: Option<Arc<dyn File>>,
     ) -> Result<Self> {
@@ -426,6 +441,7 @@ impl Buffer {
             buffer,
             message.diff_base.map(|text| text.into_boxed_str().into()),
             file,
+            capability,
         );
         this.text.set_line_ending(proto::deserialize_line_ending(
             rpc::proto::LineEnding::from_i32(message.line_ending)
@@ -504,10 +520,19 @@ impl Buffer {
         self
     }
 
+    pub fn capability(&self) -> Capability {
+        self.capability
+    }
+
+    pub fn read_only(&self) -> bool {
+        self.capability == Capability::ReadOnly
+    }
+
     pub fn build(
         buffer: TextBuffer,
         diff_base: Option<String>,
         file: Option<Arc<dyn File>>,
+        capability: Capability,
     ) -> Self {
         let saved_mtime = if let Some(file) = file.as_ref() {
             file.mtime()
@@ -526,6 +551,7 @@ impl Buffer {
             diff_base,
             git_diff: git::diff::BufferDiff::new(),
             file,
+            capability,
             syntax_map: Mutex::new(SyntaxMap::new()),
             parsing_in_background: false,
             parse_count: 0,

crates/language/src/buffer_tests.rs 🔗

@@ -1926,7 +1926,7 @@ fn test_serialization(cx: &mut gpui::AppContext) {
         .background_executor()
         .block(buffer1.read(cx).serialize_ops(None, cx));
     let buffer2 = cx.new_model(|cx| {
-        let mut buffer = Buffer::from_proto(1, state, None).unwrap();
+        let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
         buffer
             .apply_ops(
                 ops.into_iter()
@@ -1967,7 +1967,8 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
             let ops = cx
                 .background_executor()
                 .block(base_buffer.read(cx).serialize_ops(None, cx));
-            let mut buffer = Buffer::from_proto(i as ReplicaId, state, None).unwrap();
+            let mut buffer =
+                Buffer::from_proto(i as ReplicaId, Capability::ReadWrite, state, None).unwrap();
             buffer
                 .apply_ops(
                     ops.into_iter()
@@ -2083,8 +2084,13 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
                     replica_id
                 );
                 new_buffer = Some(cx.new_model(|cx| {
-                    let mut new_buffer =
-                        Buffer::from_proto(new_replica_id, old_buffer_state, None).unwrap();
+                    let mut new_buffer = Buffer::from_proto(
+                        new_replica_id,
+                        Capability::ReadWrite,
+                        old_buffer_state,
+                        None,
+                    )
+                    .unwrap();
                     new_buffer
                         .apply_ops(
                             old_buffer_ops

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -11,7 +11,7 @@ pub use language::Completion;
 use language::{
     char_kind,
     language_settings::{language_settings, LanguageSettings},
-    AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
+    AutoindentMode, Buffer, BufferChunks, BufferSnapshot, Capability, CharKind, Chunk, CursorShape,
     DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
     Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
     ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
@@ -55,6 +55,7 @@ pub struct MultiBuffer {
     replica_id: ReplicaId,
     history: History,
     title: Option<String>,
+    capability: Capability,
 }
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -225,13 +226,14 @@ struct ExcerptBytes<'a> {
 }
 
 impl MultiBuffer {
-    pub fn new(replica_id: ReplicaId) -> Self {
+    pub fn new(replica_id: ReplicaId, capability: Capability) -> Self {
         Self {
             snapshot: Default::default(),
             buffers: Default::default(),
             next_excerpt_id: 1,
             subscriptions: Default::default(),
             singleton: false,
+            capability,
             replica_id,
             history: History {
                 next_transaction_id: Default::default(),
@@ -271,6 +273,7 @@ impl MultiBuffer {
             next_excerpt_id: 1,
             subscriptions: Default::default(),
             singleton: self.singleton,
+            capability: self.capability,
             replica_id: self.replica_id,
             history: self.history.clone(),
             title: self.title.clone(),
@@ -282,8 +285,12 @@ impl MultiBuffer {
         self
     }
 
+    pub fn read_only(&self) -> bool {
+        self.capability == Capability::ReadOnly
+    }
+
     pub fn singleton(buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Self {
-        let mut this = Self::new(buffer.read(cx).replica_id());
+        let mut this = Self::new(buffer.read(cx).replica_id(), buffer.read(cx).capability());
         this.singleton = true;
         this.push_excerpts(
             buffer,
@@ -1657,7 +1664,7 @@ impl MultiBuffer {
         excerpts: [(&str, Vec<Range<Point>>); COUNT],
         cx: &mut gpui::AppContext,
     ) -> Model<Self> {
-        let multi = cx.new_model(|_| Self::new(0));
+        let multi = cx.new_model(|_| Self::new(0, Capability::ReadWrite));
         for (text, ranges) in excerpts {
             let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), text));
             let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
@@ -1678,7 +1685,7 @@ impl MultiBuffer {
 
     pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> Model<Self> {
         cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
+            let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
             let mutation_count = rng.gen_range(1..=5);
             multibuffer.randomly_edit_excerpts(rng, mutation_count, cx);
             multibuffer
@@ -4176,7 +4183,7 @@ mod tests {
             let ops = cx
                 .background_executor()
                 .block(host_buffer.read(cx).serialize_ops(None, cx));
-            let mut buffer = Buffer::from_proto(1, state, None).unwrap();
+            let mut buffer = Buffer::from_proto(1, Capability::ReadWrite, state, None).unwrap();
             buffer
                 .apply_ops(
                     ops.into_iter()
@@ -4205,7 +4212,7 @@ mod tests {
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'a')));
         let buffer_2 =
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(6, 6, 'g')));
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
 
         let events = Arc::new(RwLock::new(Vec::<Event>::new()));
         multibuffer.update(cx, |_, cx| {
@@ -4442,8 +4449,8 @@ mod tests {
         let buffer_2 =
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(10, 3, 'm')));
 
-        let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0));
-        let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let leader_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
+        let follower_multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let follower_edit_event_count = Arc::new(RwLock::new(0));
 
         follower_multibuffer.update(cx, |_, cx| {
@@ -4547,7 +4554,7 @@ mod tests {
     fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
         let buffer =
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.push_excerpts_with_context_lines(
                 buffer.clone(),
@@ -4584,7 +4591,7 @@ mod tests {
     async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
         let buffer =
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), sample_text(20, 3, 'a')));
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
             let snapshot = buffer.read(cx);
             let ranges = vec![
@@ -4619,7 +4626,7 @@ mod tests {
 
     #[gpui::test]
     fn test_empty_multibuffer(cx: &mut AppContext) {
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
 
         let snapshot = multibuffer.read(cx).snapshot(cx);
         assert_eq!(snapshot.text(), "");
@@ -4652,7 +4659,7 @@ mod tests {
         let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
         let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "efghi"));
         let multibuffer = cx.new_model(|cx| {
-            let mut multibuffer = MultiBuffer::new(0);
+            let mut multibuffer = MultiBuffer::new(0, Capability::ReadWrite);
             multibuffer.push_excerpts(
                 buffer_1.clone(),
                 [ExcerptRange {
@@ -4710,7 +4717,7 @@ mod tests {
         let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "abcd"));
         let buffer_2 =
             cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "ABCDEFGHIJKLMNOP"));
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
 
         // Create an insertion id in buffer 1 that doesn't exist in buffer 2.
         // Add an excerpt from buffer 1 that spans this new insertion.
@@ -4844,7 +4851,7 @@ mod tests {
             .unwrap_or(10);
 
         let mut buffers: Vec<Model<Buffer>> = Vec::new();
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let mut excerpt_ids = Vec::<ExcerptId>::new();
         let mut expected_excerpts = Vec::<(Model<Buffer>, Range<text::Anchor>)>::new();
         let mut anchors = Vec::new();
@@ -5266,7 +5273,7 @@ mod tests {
 
         let buffer_1 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "1234"));
         let buffer_2 = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "5678"));
-        let multibuffer = cx.new_model(|_| MultiBuffer::new(0));
+        let multibuffer = cx.new_model(|_| MultiBuffer::new(0, Capability::ReadWrite));
         let group_interval = multibuffer.read(cx).history.group_interval;
         multibuffer.update(cx, |multibuffer, cx| {
             multibuffer.push_excerpts(

crates/project/src/project.rs 🔗

@@ -39,11 +39,11 @@ use language::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
         serialize_anchor, serialize_version, split_operations,
     },
-    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction,
-    CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent,
-    File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate,
-    OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
-    ToOffset, ToPointUtf16, Transaction, Unclipped,
+    range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability,
+    CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff,
+    Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
+    LspAdapterDelegate, OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16,
+    TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
 use log::error;
 use lsp::{
@@ -262,8 +262,7 @@ enum ProjectClientState {
     },
     Remote {
         sharing_has_stopped: bool,
-        // todo!() this should be represented differently!
-        is_read_only: bool,
+        capability: Capability,
         remote_id: u64,
         replica_id: ReplicaId,
     },
@@ -760,7 +759,7 @@ impl Project {
                 client: client.clone(),
                 client_state: Some(ProjectClientState::Remote {
                     sharing_has_stopped: false,
-                    is_read_only: false,
+                    capability: Capability::ReadWrite,
                     remote_id,
                     replica_id,
                 }),
@@ -1625,9 +1624,13 @@ impl Project {
     }
 
     pub fn set_role(&mut self, role: proto::ChannelRole) {
-        if let Some(ProjectClientState::Remote { is_read_only, .. }) = &mut self.client_state {
-            *is_read_only =
-                !(role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin)
+        if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state {
+            *capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin
+            {
+                Capability::ReadWrite
+            } else {
+                Capability::ReadOnly
+            };
         }
     }
 
@@ -1682,12 +1685,15 @@ impl Project {
         }
     }
 
+    pub fn capability(&self) -> Capability {
+        match &self.client_state {
+            Some(ProjectClientState::Remote { capability, .. }) => *capability,
+            Some(ProjectClientState::Local { .. }) | None => Capability::ReadWrite,
+        }
+    }
+
     pub fn is_read_only(&self) -> bool {
-        self.is_disconnected()
-            || match &self.client_state {
-                Some(ProjectClientState::Remote { is_read_only, .. }) => *is_read_only,
-                _ => false,
-            }
+        self.is_disconnected() || self.capability() == Capability::ReadOnly
     }
 
     pub fn is_local(&self) -> bool {
@@ -7215,7 +7221,8 @@ impl Project {
 
                     let buffer_id = state.id;
                     let buffer = cx.new_model(|_| {
-                        Buffer::from_proto(this.replica_id(), state, buffer_file).unwrap()
+                        Buffer::from_proto(this.replica_id(), this.capability(), state, buffer_file)
+                            .unwrap()
                     });
                     this.incomplete_remote_buffers
                         .insert(buffer_id, Some(buffer));

crates/project/src/worktree.rs 🔗

@@ -32,7 +32,8 @@ use language::{
         deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
         serialize_version,
     },
-    Buffer, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,
+    Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint,
+    Unclipped,
 };
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -682,7 +683,14 @@ impl LocalWorktree {
                 .background_executor()
                 .spawn(async move { text::Buffer::new(0, id, contents) })
                 .await;
-            cx.new_model(|_| Buffer::build(text_buffer, diff_base, Some(Arc::new(file))))
+            cx.new_model(|_| {
+                Buffer::build(
+                    text_buffer,
+                    diff_base,
+                    Some(Arc::new(file)),
+                    Capability::ReadWrite,
+                )
+            })
         })
     }
 

crates/search/src/buffer_search.rs 🔗

@@ -70,7 +70,7 @@ impl BufferSearchBar {
     fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
         let text_style = TextStyle {
-            color: if editor.read(cx).read_only() {
+            color: if editor.read(cx).read_only(cx) {
                 cx.theme().colors().text_disabled
             } else {
                 cx.theme().colors().text

crates/search/src/project_search.rs 🔗

@@ -132,9 +132,11 @@ pub struct ProjectSearchBar {
 impl ProjectSearch {
     fn new(project: Model<Project>, cx: &mut ModelContext<Self>) -> Self {
         let replica_id = project.read(cx).replica_id();
+        let capability = project.read(cx).capability();
+
         Self {
             project,
-            excerpts: cx.new_model(|_| MultiBuffer::new(replica_id)),
+            excerpts: cx.new_model(|_| MultiBuffer::new(replica_id, capability)),
             pending_search: Default::default(),
             match_ranges: Default::default(),
             active_query: None,
@@ -1519,7 +1521,7 @@ impl ProjectSearchBar {
     fn render_text_input(&self, editor: &View<Editor>, cx: &ViewContext<Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
         let text_style = TextStyle {
-            color: if editor.read(cx).read_only() {
+            color: if editor.read(cx).read_only(cx) {
                 cx.theme().colors().text_disabled
             } else {
                 cx.theme().colors().text

script/sqlx 🔗

@@ -8,7 +8,7 @@ set -e
 cd crates/collab
 
 # Export contents of .env.toml
-eval "$(cargo run --quiet --bin dotenv)"
+eval "$(cargo run --quiet --bin dotenv2)"
 
 # Run sqlx command
 sqlx $@