1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::Context as _;
7use collections::HashMap;
8use fs::Fs;
9use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
10use language::{
11 ContextLocation, ContextProvider as _, LanguageToolchainStore, Location,
12 proto::{deserialize_anchor, serialize_anchor},
13};
14use rpc::{AnyProtoClient, TypedEnvelope, proto};
15use settings::{InvalidSettingsError, SettingsLocation};
16use task::{TaskContext, TaskVariables, VariableName};
17use text::{BufferId, OffsetRangeExt};
18use util::ResultExt;
19
20use crate::{
21 BasicContextProvider, Inventory, ProjectEnvironment, buffer_store::BufferStore,
22 worktree_store::WorktreeStore,
23};
24
25// platform-dependent warning
26pub enum TaskStore {
27 Functional(StoreState),
28 Noop,
29}
30
31pub struct StoreState {
32 mode: StoreMode,
33 task_inventory: Entity<Inventory>,
34 buffer_store: WeakEntity<BufferStore>,
35 worktree_store: Entity<WorktreeStore>,
36 toolchain_store: Arc<dyn LanguageToolchainStore>,
37}
38
39enum StoreMode {
40 Local {
41 downstream_client: Option<(AnyProtoClient, u64)>,
42 environment: Entity<ProjectEnvironment>,
43 },
44 Remote {
45 upstream_client: AnyProtoClient,
46 project_id: u64,
47 },
48}
49
50impl EventEmitter<crate::Event> for TaskStore {}
51
52#[derive(Debug)]
53pub enum TaskSettingsLocation<'a> {
54 Global(&'a Path),
55 Worktree(SettingsLocation<'a>),
56}
57
58impl TaskStore {
59 pub fn init(client: Option<&AnyProtoClient>) {
60 if let Some(client) = client {
61 client.add_entity_request_handler(Self::handle_task_context_for_location);
62 }
63 }
64
65 async fn handle_task_context_for_location(
66 store: Entity<Self>,
67 envelope: TypedEnvelope<proto::TaskContextForLocation>,
68 mut cx: AsyncApp,
69 ) -> anyhow::Result<proto::TaskContext> {
70 let location = envelope
71 .payload
72 .location
73 .context("no location given for task context handling")?;
74 let (buffer_store, is_remote) = store.read_with(&cx, |store, _| {
75 Ok(match store {
76 TaskStore::Functional(state) => (
77 state.buffer_store.clone(),
78 match &state.mode {
79 StoreMode::Local { .. } => false,
80 StoreMode::Remote { .. } => true,
81 },
82 ),
83 TaskStore::Noop => {
84 anyhow::bail!("empty task store cannot handle task context requests")
85 }
86 })
87 })??;
88 let buffer_store = buffer_store
89 .upgrade()
90 .context("no buffer store when handling task context request")?;
91
92 let buffer_id = BufferId::new(location.buffer_id).with_context(|| {
93 format!(
94 "cannot handle task context request for invalid buffer id: {}",
95 location.buffer_id
96 )
97 })?;
98
99 let start = location
100 .start
101 .and_then(deserialize_anchor)
102 .context("missing task context location start")?;
103 let end = location
104 .end
105 .and_then(deserialize_anchor)
106 .context("missing task context location end")?;
107 let buffer = buffer_store
108 .update(&mut cx, |buffer_store, cx| {
109 if is_remote {
110 buffer_store.wait_for_remote_buffer(buffer_id, cx)
111 } else {
112 Task::ready(
113 buffer_store
114 .get(buffer_id)
115 .with_context(|| format!("no local buffer with id {buffer_id}")),
116 )
117 }
118 })?
119 .await?;
120
121 let location = Location {
122 buffer,
123 range: start..end,
124 };
125 let context_task = store.update(&mut cx, |store, cx| {
126 let captured_variables = {
127 let mut variables = TaskVariables::from_iter(
128 envelope
129 .payload
130 .task_variables
131 .into_iter()
132 .filter_map(|(k, v)| Some((k.parse().log_err()?, v))),
133 );
134
135 let snapshot = location.buffer.read(cx).snapshot();
136 let range = location.range.to_offset(&snapshot);
137
138 for range in snapshot.runnable_ranges(range) {
139 for (capture_name, value) in range.extra_captures {
140 variables.insert(VariableName::Custom(capture_name.into()), value);
141 }
142 }
143 variables
144 };
145 store.task_context_for_location(captured_variables, location, cx)
146 })?;
147 let task_context = context_task.await.unwrap_or_default();
148 Ok(proto::TaskContext {
149 project_env: task_context.project_env.into_iter().collect(),
150 cwd: task_context
151 .cwd
152 .map(|cwd| cwd.to_string_lossy().into_owned()),
153 task_variables: task_context
154 .task_variables
155 .into_iter()
156 .map(|(variable_name, variable_value)| (variable_name.to_string(), variable_value))
157 .collect(),
158 })
159 }
160
161 pub fn local(
162 buffer_store: WeakEntity<BufferStore>,
163 worktree_store: Entity<WorktreeStore>,
164 toolchain_store: Arc<dyn LanguageToolchainStore>,
165 environment: Entity<ProjectEnvironment>,
166 cx: &mut Context<Self>,
167 ) -> Self {
168 Self::Functional(StoreState {
169 mode: StoreMode::Local {
170 downstream_client: None,
171 environment,
172 },
173 task_inventory: Inventory::new(cx),
174 buffer_store,
175 toolchain_store,
176 worktree_store,
177 })
178 }
179
180 pub fn remote(
181 buffer_store: WeakEntity<BufferStore>,
182 worktree_store: Entity<WorktreeStore>,
183 toolchain_store: Arc<dyn LanguageToolchainStore>,
184 upstream_client: AnyProtoClient,
185 project_id: u64,
186 cx: &mut Context<Self>,
187 ) -> Self {
188 Self::Functional(StoreState {
189 mode: StoreMode::Remote {
190 upstream_client,
191 project_id,
192 },
193 task_inventory: Inventory::new(cx),
194 buffer_store,
195 toolchain_store,
196 worktree_store,
197 })
198 }
199
200 pub fn task_context_for_location(
201 &self,
202 captured_variables: TaskVariables,
203 location: Location,
204 cx: &mut App,
205 ) -> Task<Option<TaskContext>> {
206 match self {
207 TaskStore::Functional(state) => match &state.mode {
208 StoreMode::Local { environment, .. } => local_task_context_for_location(
209 state.worktree_store.clone(),
210 state.toolchain_store.clone(),
211 environment.clone(),
212 captured_variables,
213 location,
214 cx,
215 ),
216 StoreMode::Remote {
217 upstream_client,
218 project_id,
219 } => remote_task_context_for_location(
220 *project_id,
221 upstream_client.clone(),
222 state.worktree_store.clone(),
223 captured_variables,
224 location,
225 state.toolchain_store.clone(),
226 cx,
227 ),
228 },
229 TaskStore::Noop => Task::ready(None),
230 }
231 }
232
233 pub const fn task_inventory(&self) -> Option<&Entity<Inventory>> {
234 match self {
235 TaskStore::Functional(state) => Some(&state.task_inventory),
236 TaskStore::Noop => None,
237 }
238 }
239
240 pub fn shared(&mut self, remote_id: u64, new_downstream_client: AnyProtoClient, _cx: &mut App) {
241 if let Self::Functional(StoreState {
242 mode: StoreMode::Local {
243 downstream_client, ..
244 },
245 ..
246 }) = self
247 {
248 *downstream_client = Some((new_downstream_client, remote_id));
249 }
250 }
251
252 pub fn unshared(&mut self, _: &mut Context<Self>) {
253 if let Self::Functional(StoreState {
254 mode: StoreMode::Local {
255 downstream_client, ..
256 },
257 ..
258 }) = self
259 {
260 *downstream_client = None;
261 }
262 }
263
264 pub(super) fn update_user_tasks(
265 &self,
266 location: TaskSettingsLocation<'_>,
267 raw_tasks_json: Option<&str>,
268 cx: &mut Context<Self>,
269 ) -> Result<(), InvalidSettingsError> {
270 let task_inventory = match self {
271 TaskStore::Functional(state) => &state.task_inventory,
272 TaskStore::Noop => return Ok(()),
273 };
274 let raw_tasks_json = raw_tasks_json
275 .map(|json| json.trim())
276 .filter(|json| !json.is_empty());
277
278 task_inventory.update(cx, |inventory, _| {
279 inventory.update_file_based_tasks(location, raw_tasks_json)
280 })
281 }
282
283 pub(super) fn update_user_debug_scenarios(
284 &self,
285 location: TaskSettingsLocation<'_>,
286 raw_tasks_json: Option<&str>,
287 cx: &mut Context<Self>,
288 ) -> Result<(), InvalidSettingsError> {
289 let task_inventory = match self {
290 TaskStore::Functional(state) => &state.task_inventory,
291 TaskStore::Noop => return Ok(()),
292 };
293 let raw_tasks_json = raw_tasks_json
294 .map(|json| json.trim())
295 .filter(|json| !json.is_empty());
296
297 task_inventory.update(cx, |inventory, _| {
298 inventory.update_file_based_scenarios(location, raw_tasks_json)
299 })
300 }
301}
302
303fn local_task_context_for_location(
304 worktree_store: Entity<WorktreeStore>,
305 toolchain_store: Arc<dyn LanguageToolchainStore>,
306 environment: Entity<ProjectEnvironment>,
307 captured_variables: TaskVariables,
308 location: Location,
309 cx: &App,
310) -> Task<Option<TaskContext>> {
311 let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));
312 let worktree_abs_path = worktree_id
313 .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
314 .and_then(|worktree| worktree.read(cx).root_dir());
315 let fs = worktree_store.read(cx).fs();
316
317 cx.spawn(async move |cx| {
318 let project_env = environment
319 .update(cx, |environment, cx| {
320 environment.get_buffer_environment(&location.buffer, &worktree_store, cx)
321 })
322 .ok()?
323 .await;
324
325 let mut task_variables = cx
326 .update(|cx| {
327 combine_task_variables(
328 captured_variables,
329 fs,
330 worktree_store.clone(),
331 location,
332 project_env.clone(),
333 BasicContextProvider::new(worktree_store),
334 toolchain_store,
335 cx,
336 )
337 })
338 .ok()?
339 .await
340 .log_err()?;
341 // Remove all custom entries starting with _, as they're not intended for use by the end user.
342 task_variables.sweep();
343
344 Some(TaskContext {
345 project_env: project_env.unwrap_or_default(),
346 cwd: worktree_abs_path.map(|p| p.to_path_buf()),
347 task_variables,
348 })
349 })
350}
351
352fn remote_task_context_for_location(
353 project_id: u64,
354 upstream_client: AnyProtoClient,
355 worktree_store: Entity<WorktreeStore>,
356 captured_variables: TaskVariables,
357 location: Location,
358 toolchain_store: Arc<dyn LanguageToolchainStore>,
359 cx: &mut App,
360) -> Task<Option<TaskContext>> {
361 cx.spawn(async move |cx| {
362 // We need to gather a client context, as the headless one may lack certain information (e.g. tree-sitter parsing is disabled there, so symbols are not available).
363 let mut remote_context = cx
364 .update(|cx| {
365 let worktree_root = worktree_root(&worktree_store, &location, cx);
366
367 BasicContextProvider::new(worktree_store).build_context(
368 &TaskVariables::default(),
369 ContextLocation {
370 fs: None,
371 worktree_root,
372 file_location: &location,
373 },
374 None,
375 toolchain_store,
376 cx,
377 )
378 })
379 .ok()?
380 .await
381 .log_err()
382 .unwrap_or_default();
383 remote_context.extend(captured_variables);
384
385 let buffer_id = cx
386 .update(|cx| location.buffer.read(cx).remote_id().to_proto())
387 .ok()?;
388 let context_task = upstream_client.request(proto::TaskContextForLocation {
389 project_id,
390 location: Some(proto::Location {
391 buffer_id,
392 start: Some(serialize_anchor(&location.range.start)),
393 end: Some(serialize_anchor(&location.range.end)),
394 }),
395 task_variables: remote_context
396 .into_iter()
397 .map(|(k, v)| (k.to_string(), v))
398 .collect(),
399 });
400 let task_context = context_task.await.log_err()?;
401 Some(TaskContext {
402 cwd: task_context.cwd.map(PathBuf::from),
403 task_variables: task_context
404 .task_variables
405 .into_iter()
406 .filter_map(
407 |(variable_name, variable_value)| match variable_name.parse() {
408 Ok(variable_name) => Some((variable_name, variable_value)),
409 Err(()) => {
410 log::error!("Unknown variable name: {variable_name}");
411 None
412 }
413 },
414 )
415 .collect(),
416 project_env: task_context.project_env.into_iter().collect(),
417 })
418 })
419}
420
421fn worktree_root(
422 worktree_store: &Entity<WorktreeStore>,
423 location: &Location,
424 cx: &mut App,
425) -> Option<PathBuf> {
426 location
427 .buffer
428 .read(cx)
429 .file()
430 .map(|f| f.worktree_id(cx))
431 .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
432 .and_then(|worktree| {
433 let worktree = worktree.read(cx);
434 if !worktree.is_visible() {
435 return None;
436 }
437 let root_entry = worktree.root_entry()?;
438 if !root_entry.is_dir() {
439 return None;
440 }
441 Some(worktree.absolutize(&root_entry.path))
442 })
443}
444
445fn combine_task_variables(
446 mut captured_variables: TaskVariables,
447 fs: Option<Arc<dyn Fs>>,
448 worktree_store: Entity<WorktreeStore>,
449 location: Location,
450 project_env: Option<HashMap<String, String>>,
451 baseline: BasicContextProvider,
452 toolchain_store: Arc<dyn LanguageToolchainStore>,
453 cx: &mut App,
454) -> Task<anyhow::Result<TaskVariables>> {
455 let language_context_provider = location
456 .buffer
457 .read(cx)
458 .language()
459 .and_then(|language| language.context_provider());
460 cx.spawn(async move |cx| {
461 let baseline = cx
462 .update(|cx| {
463 let worktree_root = worktree_root(&worktree_store, &location, cx);
464 baseline.build_context(
465 &captured_variables,
466 ContextLocation {
467 fs: fs.clone(),
468 worktree_root,
469 file_location: &location,
470 },
471 project_env.clone(),
472 toolchain_store.clone(),
473 cx,
474 )
475 })?
476 .await
477 .context("building basic default context")?;
478 captured_variables.extend(baseline);
479 if let Some(provider) = language_context_provider {
480 captured_variables.extend(
481 cx.update(|cx| {
482 let worktree_root = worktree_root(&worktree_store, &location, cx);
483 provider.build_context(
484 &captured_variables,
485 ContextLocation {
486 fs,
487 worktree_root,
488 file_location: &location,
489 },
490 project_env,
491 toolchain_store,
492 cx,
493 )
494 })?
495 .await
496 .context("building provider context")?,
497 );
498 }
499 Ok(captured_variables)
500 })
501}