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