1use anyhow::Result;
2use fs::Fs;
3use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext};
4use project::{
5 buffer_store::{BufferStore, BufferStoreEvent},
6 worktree_store::WorktreeStore,
7 ProjectPath, WorktreeId, WorktreeSettings,
8};
9use remote::SshSession;
10use rpc::{
11 proto::{self, AnyProtoClient, PeerId},
12 TypedEnvelope,
13};
14use settings::{Settings as _, SettingsStore};
15use std::{
16 path::{Path, PathBuf},
17 sync::{atomic::AtomicUsize, Arc},
18};
19use util::ResultExt as _;
20use worktree::Worktree;
21
22const PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 };
23const PROJECT_ID: u64 = 0;
24
25pub struct HeadlessProject {
26 pub fs: Arc<dyn Fs>,
27 pub session: AnyProtoClient,
28 pub worktree_store: Model<WorktreeStore>,
29 pub buffer_store: Model<BufferStore>,
30 pub next_entry_id: Arc<AtomicUsize>,
31}
32
33impl HeadlessProject {
34 pub fn init(cx: &mut AppContext) {
35 cx.set_global(SettingsStore::new(cx));
36 WorktreeSettings::register(cx);
37 }
38
39 pub fn new(session: Arc<SshSession>, fs: Arc<dyn Fs>, cx: &mut ModelContext<Self>) -> Self {
40 let this = cx.weak_model();
41
42 let worktree_store = cx.new_model(|_| WorktreeStore::new(true));
43 let buffer_store = cx.new_model(|cx| BufferStore::new(worktree_store.clone(), true, cx));
44 cx.subscribe(&buffer_store, Self::on_buffer_store_event)
45 .detach();
46
47 session.add_request_handler(this.clone(), Self::handle_add_worktree);
48 session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
49 session.add_request_handler(this.clone(), Self::handle_update_buffer);
50 session.add_request_handler(this.clone(), Self::handle_save_buffer);
51 session.add_request_handler(
52 worktree_store.downgrade(),
53 WorktreeStore::handle_create_project_entry,
54 );
55 session.add_request_handler(
56 worktree_store.downgrade(),
57 WorktreeStore::handle_rename_project_entry,
58 );
59 session.add_request_handler(
60 worktree_store.downgrade(),
61 WorktreeStore::handle_copy_project_entry,
62 );
63 session.add_request_handler(
64 worktree_store.downgrade(),
65 WorktreeStore::handle_delete_project_entry,
66 );
67 session.add_request_handler(
68 worktree_store.downgrade(),
69 WorktreeStore::handle_expand_project_entry,
70 );
71
72 HeadlessProject {
73 session: session.into(),
74 fs,
75 worktree_store,
76 buffer_store,
77 next_entry_id: Default::default(),
78 }
79 }
80
81 pub async fn handle_add_worktree(
82 this: Model<Self>,
83 message: TypedEnvelope<proto::AddWorktree>,
84 mut cx: AsyncAppContext,
85 ) -> Result<proto::AddWorktreeResponse> {
86 let worktree = this
87 .update(&mut cx.clone(), |this, _| {
88 Worktree::local(
89 Path::new(&message.payload.path),
90 true,
91 this.fs.clone(),
92 this.next_entry_id.clone(),
93 &mut cx,
94 )
95 })?
96 .await?;
97
98 this.update(&mut cx, |this, cx| {
99 let session = this.session.clone();
100 this.worktree_store.update(cx, |worktree_store, cx| {
101 worktree_store.add(&worktree, cx);
102 });
103 worktree.update(cx, |worktree, cx| {
104 worktree.observe_updates(0, cx, move |update| {
105 session.send(update).ok();
106 futures::future::ready(true)
107 });
108 proto::AddWorktreeResponse {
109 worktree_id: worktree.id().to_proto(),
110 }
111 })
112 })
113 }
114
115 pub async fn handle_update_buffer(
116 this: Model<Self>,
117 envelope: TypedEnvelope<proto::UpdateBuffer>,
118 mut cx: AsyncAppContext,
119 ) -> Result<proto::Ack> {
120 this.update(&mut cx, |this, cx| {
121 this.buffer_store.update(cx, |buffer_store, cx| {
122 buffer_store.handle_update_buffer(envelope, false, cx)
123 })
124 })?
125 }
126
127 pub async fn handle_save_buffer(
128 this: Model<Self>,
129 envelope: TypedEnvelope<proto::SaveBuffer>,
130 mut cx: AsyncAppContext,
131 ) -> Result<proto::BufferSaved> {
132 let buffer_store = this.update(&mut cx, |this, _| this.buffer_store.clone())?;
133 BufferStore::handle_save_buffer(buffer_store, PROJECT_ID, envelope, cx).await
134 }
135
136 pub async fn handle_open_buffer_by_path(
137 this: Model<Self>,
138 message: TypedEnvelope<proto::OpenBufferByPath>,
139 mut cx: AsyncAppContext,
140 ) -> Result<proto::OpenBufferResponse> {
141 let worktree_id = WorktreeId::from_proto(message.payload.worktree_id);
142 let (buffer_store, buffer, session) = this.update(&mut cx, |this, cx| {
143 let buffer_store = this.buffer_store.clone();
144 let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
145 buffer_store.open_buffer(
146 ProjectPath {
147 worktree_id,
148 path: PathBuf::from(message.payload.path).into(),
149 },
150 cx,
151 )
152 });
153 anyhow::Ok((buffer_store, buffer, this.session.clone()))
154 })??;
155
156 let buffer = buffer.await?;
157 let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?;
158
159 cx.spawn(|mut cx| async move {
160 BufferStore::create_buffer_for_peer(
161 buffer_store,
162 PEER_ID,
163 buffer_id,
164 PROJECT_ID,
165 session,
166 &mut cx,
167 )
168 .await
169 })
170 .detach();
171
172 Ok(proto::OpenBufferResponse {
173 buffer_id: buffer_id.to_proto(),
174 })
175 }
176
177 pub fn on_buffer_store_event(
178 &mut self,
179 _: Model<BufferStore>,
180 event: &BufferStoreEvent,
181 cx: &mut ModelContext<Self>,
182 ) {
183 match event {
184 BufferStoreEvent::LocalBufferUpdated { buffer } => {
185 let buffer = buffer.read(cx);
186 let buffer_id = buffer.remote_id();
187 let Some(new_file) = buffer.file() else {
188 return;
189 };
190 self.session
191 .send(proto::UpdateBufferFile {
192 project_id: 0,
193 buffer_id: buffer_id.into(),
194 file: Some(new_file.to_proto(cx)),
195 })
196 .log_err();
197 }
198 BufferStoreEvent::DiffBaseUpdated { buffer } => {
199 let buffer = buffer.read(cx);
200 let buffer_id = buffer.remote_id();
201 let diff_base = buffer.diff_base();
202 self.session
203 .send(proto::UpdateDiffBase {
204 project_id: 0,
205 buffer_id: buffer_id.to_proto(),
206 diff_base: diff_base.map(|b| b.to_string()),
207 })
208 .log_err();
209 }
210 _ => {}
211 }
212 }
213}