load_project.rs

  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}