1use crate::{
2 example::{Example, ExampleBuffer, ExampleState},
3 git,
4 headless::EpAppState,
5 progress::{InfoStyle, Progress, Step, StepProgress},
6};
7use anyhow::{Context as _, Result};
8use edit_prediction::EditPredictionStore;
9use edit_prediction::udiff::{OpenedBuffers, refresh_worktree_entries};
10use futures::AsyncWriteExt as _;
11use gpui::{AsyncApp, Entity};
12use language::{Anchor, Buffer, LanguageNotFound, ToOffset, ToPoint};
13use project::Project;
14use project::buffer_store::BufferStoreEvent;
15use std::{fs, path::PathBuf, sync::Arc};
16
17pub async fn run_load_project(
18 example: &mut Example,
19 app_state: Arc<EpAppState>,
20 mut cx: AsyncApp,
21) -> Result<()> {
22 if example.state.is_some() {
23 return Ok(());
24 }
25
26 let progress = Progress::global().start(Step::LoadProject, &example.spec.name);
27
28 let project = setup_project(example, &app_state, &progress, &mut cx).await?;
29
30 let _open_buffers = apply_edit_history(example, &project, &mut cx).await?;
31
32 let (buffer, cursor_position) = cursor_position(example, &project, &mut cx).await?;
33 let (example_buffer, language_name) = buffer.read_with(&cx, |buffer, _cx| {
34 let cursor_point = cursor_position.to_point(&buffer);
35 let language_name = buffer
36 .language()
37 .map(|l| l.name().to_string())
38 .unwrap_or_else(|| "Unknown".to_string());
39 (
40 ExampleBuffer {
41 content: buffer.text(),
42 cursor_row: cursor_point.row,
43 cursor_column: cursor_point.column,
44 cursor_offset: cursor_position.to_offset(&buffer),
45 },
46 language_name,
47 )
48 })?;
49
50 progress.set_info(language_name, InfoStyle::Normal);
51
52 example.buffer = Some(example_buffer);
53 example.state = Some(ExampleState {
54 buffer,
55 project,
56 cursor_position,
57 _open_buffers,
58 });
59 Ok(())
60}
61
62async fn cursor_position(
63 example: &Example,
64 project: &Entity<Project>,
65 cx: &mut AsyncApp,
66) -> Result<(Entity<Buffer>, Anchor)> {
67 let language_registry = project.read_with(cx, |project, _| project.languages().clone())?;
68 let result = language_registry
69 .load_language_for_file_path(&example.spec.cursor_path)
70 .await;
71
72 if let Err(error) = result
73 && !error.is::<LanguageNotFound>()
74 {
75 return Err(error);
76 }
77
78 // Since the worktree scanner is disabled, manually refresh entries for the cursor path.
79 if let Some(worktree) = project.read_with(cx, |project, cx| project.worktrees(cx).next())? {
80 refresh_worktree_entries(&worktree, [&*example.spec.cursor_path], cx).await?;
81 }
82
83 let cursor_path = project
84 .read_with(cx, |project, cx| {
85 project.find_project_path(&example.spec.cursor_path, cx)
86 })?
87 .with_context(|| {
88 format!(
89 "failed to find cursor path {}",
90 example.spec.cursor_path.display()
91 )
92 })?;
93
94 let cursor_buffer = project
95 .update(cx, |project, cx| project.open_buffer(cursor_path, cx))?
96 .await?;
97
98 let (cursor_excerpt, cursor_offset_within_excerpt) = example.spec.cursor_excerpt()?;
99
100 let excerpt_offset = cursor_buffer.read_with(cx, |buffer, _cx| {
101 let text = buffer.text();
102
103 let mut matches = text.match_indices(&cursor_excerpt);
104 let (excerpt_offset, _) = matches.next().with_context(|| {
105 format!(
106 "\nExcerpt:\n\n{cursor_excerpt}\nBuffer text:\n{text}\n.Example: {}\nCursor excerpt did not exist in buffer.",
107 example.spec.name
108 )
109 })?;
110 anyhow::ensure!(
111 matches.next().is_none(),
112 "More than one cursor position match found for {}",
113 &example.spec.name
114 );
115 Ok(excerpt_offset)
116 })??;
117
118 let cursor_offset = excerpt_offset + cursor_offset_within_excerpt;
119 let cursor_anchor =
120 cursor_buffer.read_with(cx, |buffer, _| buffer.anchor_after(cursor_offset))?;
121
122 Ok((cursor_buffer, cursor_anchor))
123}
124
125async fn setup_project(
126 example: &mut Example,
127 app_state: &Arc<EpAppState>,
128 step_progress: &StepProgress,
129 cx: &mut AsyncApp,
130) -> Result<Entity<Project>> {
131 let ep_store = cx
132 .update(|cx| EditPredictionStore::try_global(cx))?
133 .context("Store should be initialized at init")?;
134
135 let worktree_path = setup_worktree(example, step_progress).await?;
136
137 if let Some(project) = app_state.project_cache.get(&example.spec.repository_url) {
138 ep_store.update(cx, |ep_store, _| {
139 ep_store.clear_history_for_project(&project);
140 })?;
141 let buffer_store = project.read_with(cx, |project, _| project.buffer_store().clone())?;
142 let buffers = buffer_store.read_with(cx, |buffer_store, _| {
143 buffer_store.buffers().collect::<Vec<_>>()
144 })?;
145 for buffer in buffers {
146 buffer
147 .update(cx, |buffer, cx| buffer.reload(cx))?
148 .await
149 .ok();
150 }
151 return Ok(project);
152 }
153
154 let project = cx.update(|cx| {
155 Project::local(
156 app_state.client.clone(),
157 app_state.node_runtime.clone(),
158 app_state.user_store.clone(),
159 app_state.languages.clone(),
160 app_state.fs.clone(),
161 None,
162 false,
163 cx,
164 )
165 })?;
166
167 project
168 .update(cx, |project, cx| {
169 project.disable_worktree_scanner(cx);
170 project.create_worktree(&worktree_path, true, cx)
171 })?
172 .await?;
173
174 app_state
175 .project_cache
176 .insert(example.spec.repository_url.clone(), project.clone());
177
178 let buffer_store = project.read_with(cx, |project, _| project.buffer_store().clone())?;
179 cx.subscribe(&buffer_store, {
180 let project = project.clone();
181 move |_, event, cx| match event {
182 BufferStoreEvent::BufferAdded(buffer) => {
183 ep_store.update(cx, |store, cx| store.register_buffer(&buffer, &project, cx));
184 }
185 _ => {}
186 }
187 })?
188 .detach();
189
190 Ok(project)
191}
192
193async fn setup_worktree(example: &Example, step_progress: &StepProgress) -> Result<PathBuf> {
194 let repo_name = example.repo_name().context("failed to get repo name")?;
195 let repo_dir = git::repo_path_for_url(&example.spec.repository_url)?;
196 let worktree_path = repo_name.worktree_path();
197 let repo_lock = git::lock_repo(&repo_dir).await;
198
199 if !repo_dir.is_dir() {
200 step_progress.set_substatus(format!("cloning {}", repo_name.name));
201 fs::create_dir_all(&repo_dir)?;
202 git::run_git(&repo_dir, &["init"]).await?;
203 git::run_git(
204 &repo_dir,
205 &["remote", "add", "origin", &example.spec.repository_url],
206 )
207 .await?;
208 }
209
210 // Resolve the example to a revision, fetching it if needed.
211 step_progress.set_substatus("fetching");
212 let revision = git::fetch_if_needed(&repo_dir, &example.spec.revision).await?;
213
214 // Create the worktree for this example if needed.
215 step_progress.set_substatus("preparing worktree");
216 if worktree_path.is_dir() {
217 git::run_git(&worktree_path, &["clean", "--force", "-d"]).await?;
218 git::run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?;
219 git::run_git(&worktree_path, &["checkout", revision.as_str()]).await?;
220 } else {
221 let worktree_path_string = worktree_path.to_string_lossy();
222 let branch_name = example.spec.filename();
223 git::run_git(
224 &repo_dir,
225 &["branch", "-f", &branch_name, revision.as_str()],
226 )
227 .await?;
228 git::run_git(
229 &repo_dir,
230 &["worktree", "add", "-f", &worktree_path_string, &branch_name],
231 )
232 .await?;
233 }
234 drop(repo_lock);
235
236 // Apply the uncommitted diff for this example.
237 if !example.spec.uncommitted_diff.is_empty() {
238 step_progress.set_substatus("applying diff");
239 let mut apply_process = smol::process::Command::new("git")
240 .current_dir(&worktree_path)
241 .args(&["apply", "-"])
242 .stdin(std::process::Stdio::piped())
243 .spawn()?;
244
245 let mut stdin = apply_process.stdin.take().context("Failed to get stdin")?;
246 stdin
247 .write_all(example.spec.uncommitted_diff.as_bytes())
248 .await?;
249 stdin.close().await?;
250 drop(stdin);
251
252 let apply_result = apply_process.output().await?;
253 anyhow::ensure!(
254 apply_result.status.success(),
255 "Failed to apply uncommitted diff patch with status: {}\nstderr:\n{}\nstdout:\n{}",
256 apply_result.status,
257 String::from_utf8_lossy(&apply_result.stderr),
258 String::from_utf8_lossy(&apply_result.stdout),
259 );
260 }
261
262 step_progress.clear_substatus();
263 Ok(worktree_path)
264}
265
266async fn apply_edit_history(
267 example: &Example,
268 project: &Entity<Project>,
269 cx: &mut AsyncApp,
270) -> Result<OpenedBuffers> {
271 edit_prediction::udiff::apply_diff(&example.spec.edit_history, project, cx).await
272}