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