# Objective Closes EP-9 - Describe the objective or issue this PR addresses. - If you're fixing a specific issue, use "Fixes #X" for each issue as [described in the GitHub docs](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). ## Solution - Describe the solution used to achieve the objective above. ## Testing - Did you test these changes? If so, how? - Are there any parts that need more testing? - How can other people (reviewers) test your changes? Is there anything specific they need to know? - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase > This section is optional. If this PR does not include a visual change or does not add a new user-facing feature, you can delete this section. - Help others understand the result of this PR by showcasing your awesome work! - If this PR includes a visual change, consider adding a screenshot, GIF, or video - A before/after comparison is very useful for changes to existing features! While a showcase should aim to be brief and digestible, you can use a toggleable section to save space on longer showcases: <details> <summary>Click to view showcase</summary> My super cool demos here </details> --- Release Notes: - N/A or Added/Fixed/Improved ... --------- Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com> Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
592 lines
21 KiB
Rust
592 lines
21 KiB
Rust
use crate::{
|
|
example::Example,
|
|
format_prompt::line_start_offset,
|
|
headless::EpAppState,
|
|
load_project::run_load_project,
|
|
progress::{ExampleProgress, InfoStyle, Step, StepProgress},
|
|
};
|
|
use anyhow::Context as _;
|
|
use clap::ValueEnum;
|
|
use collections::{HashMap, HashSet};
|
|
use edit_prediction::{DebugEvent, EditPredictionStore, udiff::refresh_worktree_entries};
|
|
use edit_prediction_context::OracleTarget;
|
|
use futures::{FutureExt as _, StreamExt as _, channel::mpsc};
|
|
use gpui::{AsyncApp, Entity};
|
|
use language::Buffer;
|
|
use project::Project;
|
|
use std::{
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
use zeta_prompt::{ContextSource, udiff::DiffLine};
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
|
|
pub enum ContextRetrievalType {
|
|
Lsp,
|
|
Editable,
|
|
CurrentFile,
|
|
EditHistory,
|
|
EditHistoryFile,
|
|
GitLog,
|
|
Bm25,
|
|
OracleFile,
|
|
OracleSnippet,
|
|
#[default]
|
|
All,
|
|
None,
|
|
}
|
|
|
|
impl std::fmt::Display for ContextRetrievalType {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ContextRetrievalType::Lsp => write!(f, "lsp"),
|
|
ContextRetrievalType::Editable => write!(f, "editable"),
|
|
ContextRetrievalType::CurrentFile => write!(f, "current-file"),
|
|
ContextRetrievalType::EditHistory => write!(f, "edit-history"),
|
|
ContextRetrievalType::EditHistoryFile => write!(f, "edit-history-file"),
|
|
ContextRetrievalType::GitLog => write!(f, "git-log"),
|
|
ContextRetrievalType::Bm25 => write!(f, "bm25"),
|
|
ContextRetrievalType::OracleFile => write!(f, "oracle-file"),
|
|
ContextRetrievalType::OracleSnippet => write!(f, "oracle-snippet"),
|
|
ContextRetrievalType::All => write!(f, "all"),
|
|
ContextRetrievalType::None => write!(f, "none"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ContextRetrievalType {
|
|
pub fn context_sources(self) -> Vec<ContextSource> {
|
|
match self {
|
|
ContextRetrievalType::Lsp => vec![ContextSource::Lsp],
|
|
ContextRetrievalType::Editable => editable_context_sources(),
|
|
ContextRetrievalType::CurrentFile => vec![ContextSource::CurrentFile],
|
|
ContextRetrievalType::EditHistory => vec![ContextSource::EditHistory],
|
|
ContextRetrievalType::EditHistoryFile => vec![ContextSource::EditHistoryFile],
|
|
ContextRetrievalType::GitLog => vec![ContextSource::GitLog],
|
|
ContextRetrievalType::Bm25 => vec![ContextSource::Bm25],
|
|
ContextRetrievalType::OracleFile => vec![ContextSource::OracleFile],
|
|
ContextRetrievalType::OracleSnippet => vec![ContextSource::OracleSnippet],
|
|
ContextRetrievalType::All => {
|
|
let mut sources = vec![ContextSource::Lsp];
|
|
sources.extend(editable_context_sources());
|
|
sources
|
|
}
|
|
ContextRetrievalType::None => Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn context_sources_for_types(context_types: &[ContextRetrievalType]) -> Vec<ContextSource> {
|
|
let mut context_sources = Vec::new();
|
|
for context_type in context_types {
|
|
for context_source in context_type.context_sources() {
|
|
if !context_sources.contains(&context_source) {
|
|
context_sources.push(context_source);
|
|
}
|
|
}
|
|
}
|
|
context_sources
|
|
}
|
|
|
|
fn editable_context_sources() -> Vec<ContextSource> {
|
|
vec![
|
|
ContextSource::CursorExcerpt,
|
|
ContextSource::CurrentFile,
|
|
ContextSource::EditHistory,
|
|
ContextSource::EditHistoryFile,
|
|
ContextSource::GitLog,
|
|
ContextSource::Bm25,
|
|
]
|
|
}
|
|
|
|
pub async fn run_context_retrieval(
|
|
example: &mut Example,
|
|
app_state: Arc<EpAppState>,
|
|
example_progress: &ExampleProgress,
|
|
context_types: Vec<ContextRetrievalType>,
|
|
force: bool,
|
|
mut cx: AsyncApp,
|
|
) -> anyhow::Result<()> {
|
|
if (!force
|
|
&& example
|
|
.prompt_inputs
|
|
.as_ref()
|
|
.is_some_and(|inputs| inputs.related_files.is_some()))
|
|
|| example.spec.repository_url.is_empty()
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
run_load_project(example, app_state.clone(), example_progress, cx.clone()).await?;
|
|
|
|
let step_progress: Arc<StepProgress> = example_progress.start(Step::Context).into();
|
|
|
|
let state = example.state.as_ref().unwrap();
|
|
let project = state.project.clone();
|
|
|
|
let ep_store = cx
|
|
.update(|cx| EditPredictionStore::try_global(cx))
|
|
.context("EditPredictionStore not initialized")?;
|
|
|
|
let mut context_files = Vec::new();
|
|
let context_sources = context_sources_for_types(&context_types);
|
|
|
|
if context_sources.contains(&ContextSource::Lsp) {
|
|
let _lsp_handle = project.update(&mut cx, |project, cx| {
|
|
project.register_buffer_with_language_servers(&state.buffer, cx)
|
|
});
|
|
wait_for_language_servers_to_start(&project, &state.buffer, &step_progress, &mut cx)
|
|
.await?;
|
|
|
|
let mut events = ep_store.update(&mut cx, |store, cx| {
|
|
store.register_buffer(&state.buffer, &project, cx);
|
|
store.refresh_context(&project, &state.buffer, state.cursor_position, cx);
|
|
store.debug_info(&project, cx)
|
|
});
|
|
|
|
while let Some(event) = events.next().await {
|
|
match event {
|
|
DebugEvent::ContextRetrievalFinished(_) => {
|
|
break;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
context_files
|
|
.extend(ep_store.update(&mut cx, |store, cx| store.context_for_project(&project, cx)));
|
|
}
|
|
|
|
let editable_context_sources = context_sources
|
|
.into_iter()
|
|
.filter(|context_source| *context_source != ContextSource::Lsp)
|
|
.collect::<Vec<_>>();
|
|
if !editable_context_sources.is_empty() {
|
|
let oracle_targets = if editable_context_sources.contains(&ContextSource::OracleFile)
|
|
|| editable_context_sources.contains(&ContextSource::OracleSnippet)
|
|
{
|
|
let oracle_targets = oracle_targets_from_expected_patches(example);
|
|
let oracle_paths = oracle_targets
|
|
.iter()
|
|
.map(|target| target.path.clone())
|
|
.collect::<Vec<_>>();
|
|
refresh_paths(&project, &oracle_paths, &mut cx).await?;
|
|
oracle_targets
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
let editable_context = ep_store
|
|
.update(&mut cx, |store, cx| {
|
|
store.collect_editable_context(
|
|
project.clone(),
|
|
state.buffer.clone(),
|
|
state.cursor_position,
|
|
oracle_targets,
|
|
editable_context_sources,
|
|
cx,
|
|
)
|
|
})
|
|
.await?;
|
|
merge_context_files(&mut context_files, editable_context);
|
|
}
|
|
|
|
let excerpt_count: usize = context_files.iter().map(|f| f.excerpts.len()).sum();
|
|
step_progress.set_info(format!("{} excerpts", excerpt_count), InfoStyle::Normal);
|
|
|
|
if let Some(prompt_inputs) = example.prompt_inputs.as_mut() {
|
|
prompt_inputs.related_files = Some(context_files);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn merge_context_files(
|
|
context_files: &mut Vec<zeta_prompt::RelatedFile>,
|
|
new_files: Vec<zeta_prompt::RelatedFile>,
|
|
) {
|
|
for mut new_file in new_files {
|
|
if let Some(existing_file) = context_files
|
|
.iter_mut()
|
|
.find(|existing_file| existing_file.path == new_file.path)
|
|
{
|
|
existing_file.max_row = existing_file.max_row.max(new_file.max_row);
|
|
existing_file.excerpts.append(&mut new_file.excerpts);
|
|
existing_file.in_open_source_repo =
|
|
existing_file.in_open_source_repo && new_file.in_open_source_repo;
|
|
} else {
|
|
context_files.push(new_file);
|
|
}
|
|
}
|
|
for file in context_files.iter_mut() {
|
|
coalesce_touching_excerpts(&mut file.excerpts);
|
|
}
|
|
}
|
|
|
|
/// Sort a file's excerpts by position and merge those whose row ranges touch
|
|
/// or overlap. Touching excerpts render seamlessly in prompts (no `...`
|
|
/// separator), so keeping them separate splits edit addressing across a
|
|
/// boundary the model can't see and wastes adjacent marker tags (see
|
|
/// `TeacherJumpsPrompt::parse`).
|
|
///
|
|
/// A merged excerpt keeps the minimum `order` (and that excerpt's context
|
|
/// source), so the highest-priority content still survives budget-based
|
|
/// selection downstream; the tradeoff is that lower-priority touching
|
|
/// content is now selected together with it.
|
|
fn coalesce_touching_excerpts(excerpts: &mut Vec<zeta_prompt::RelatedExcerpt>) {
|
|
excerpts.sort_by_key(|excerpt| (excerpt.row_range.start, excerpt.row_range.end));
|
|
let mut coalesced: Vec<zeta_prompt::RelatedExcerpt> = Vec::with_capacity(excerpts.len());
|
|
for excerpt in excerpts.drain(..) {
|
|
let Some(last) = coalesced.last_mut() else {
|
|
coalesced.push(excerpt);
|
|
continue;
|
|
};
|
|
if excerpt.row_range.start > last.row_range.end {
|
|
coalesced.push(excerpt);
|
|
continue;
|
|
}
|
|
if excerpt.row_range.end > last.row_range.end {
|
|
// Touching or overlapping. Shared rows come from the same buffer
|
|
// snapshot, so drop the duplicated prefix of the new excerpt and
|
|
// append the rest.
|
|
let mut overlap_rows = (last.row_range.end - excerpt.row_range.start) as usize;
|
|
if !last.text.ends_with('\n') && !last.text.is_empty() {
|
|
// An unterminated final line means `last` includes the
|
|
// content of its `row_range.end` row itself.
|
|
overlap_rows += 1;
|
|
}
|
|
let Some(tail_start) = line_start_offset(&excerpt.text, overlap_rows) else {
|
|
// The excerpt has fewer lines than its row range claims;
|
|
// leave it unmerged rather than corrupt the text.
|
|
coalesced.push(excerpt);
|
|
continue;
|
|
};
|
|
let mut text =
|
|
String::with_capacity(last.text.len() + excerpt.text.len() - tail_start + 1);
|
|
text.push_str(&last.text);
|
|
if !text.is_empty() && !text.ends_with('\n') {
|
|
text.push('\n');
|
|
}
|
|
text.push_str(&excerpt.text[tail_start..]);
|
|
last.text = text.into();
|
|
last.row_range.end = excerpt.row_range.end;
|
|
}
|
|
if excerpt.order < last.order {
|
|
last.order = excerpt.order;
|
|
last.context_source = excerpt.context_source;
|
|
}
|
|
}
|
|
*excerpts = coalesced;
|
|
}
|
|
|
|
fn oracle_targets_from_expected_patches(example: &Example) -> Vec<OracleTarget> {
|
|
let mut target_indices: HashMap<PathBuf, usize> = HashMap::default();
|
|
let mut targets: Vec<(PathBuf, Vec<std::ops::Range<u32>>)> = Vec::new();
|
|
|
|
let mut target_index = |path: &str, targets: &mut Vec<(PathBuf, Vec<std::ops::Range<u32>>)>| {
|
|
let path = Path::new(path).to_path_buf();
|
|
*target_indices.entry(path.clone()).or_insert_with(|| {
|
|
targets.push((path, Vec::new()));
|
|
targets.len() - 1
|
|
})
|
|
};
|
|
|
|
for patch in &example.spec.expected_patches {
|
|
// Index of the target whose old-side rows the current hunk headers
|
|
// refer to. Hunk old rows refer to the file's current state only for
|
|
// the first expected patch; later patches drift, but the snippet
|
|
// padding absorbs small offsets.
|
|
let mut current_target: Option<usize> = None;
|
|
for line in patch.lines() {
|
|
match DiffLine::parse(line) {
|
|
DiffLine::OldPath { path } => {
|
|
current_target = (path.as_ref() != "/dev/null")
|
|
.then(|| target_index(path.as_ref(), &mut targets));
|
|
}
|
|
DiffLine::NewPath { path } => {
|
|
if path.as_ref() != "/dev/null" {
|
|
let index = target_index(path.as_ref(), &mut targets);
|
|
if current_target.is_none() {
|
|
current_target = Some(index);
|
|
}
|
|
}
|
|
}
|
|
DiffLine::HunkHeader(Some(location)) => {
|
|
if let Some(index) = current_target {
|
|
let start = location.start_line_old;
|
|
targets[index].1.push(start..start + location.count_old);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
targets
|
|
.into_iter()
|
|
.map(|(path, row_ranges)| OracleTarget {
|
|
path: path.into(),
|
|
row_ranges,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
async fn refresh_paths(
|
|
project: &Entity<Project>,
|
|
paths: &[Arc<Path>],
|
|
cx: &mut AsyncApp,
|
|
) -> anyhow::Result<()> {
|
|
if paths.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let Some(worktree) = project.read_with(cx, |project, cx| project.visible_worktrees(cx).next())
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
refresh_worktree_entries(&worktree, paths.iter().map(|path| path.as_ref()), cx).await
|
|
}
|
|
|
|
async fn wait_for_language_servers_to_start(
|
|
project: &Entity<Project>,
|
|
buffer: &Entity<Buffer>,
|
|
step_progress: &Arc<StepProgress>,
|
|
cx: &mut AsyncApp,
|
|
) -> anyhow::Result<()> {
|
|
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
|
|
|
// Determine which servers exist for this buffer, and which are still starting.
|
|
let mut servers_pending_start = HashSet::default();
|
|
let mut servers_pending_diagnostics = HashSet::default();
|
|
buffer.update(cx, |buffer, cx| {
|
|
lsp_store.update(cx, |lsp_store, cx| {
|
|
let ids = lsp_store.language_servers_for_local_buffer(buffer, cx);
|
|
for &id in &ids {
|
|
match lsp_store.language_server_statuses.get(&id) {
|
|
None => {
|
|
servers_pending_start.insert(id);
|
|
servers_pending_diagnostics.insert(id);
|
|
}
|
|
Some(status) if status.has_pending_diagnostic_updates => {
|
|
servers_pending_diagnostics.insert(id);
|
|
}
|
|
Some(_) => {}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
step_progress.set_substatus(format!(
|
|
"waiting for {} LSPs",
|
|
servers_pending_diagnostics.len()
|
|
));
|
|
|
|
let timeout_duration = if servers_pending_start.is_empty() {
|
|
Duration::from_secs(30)
|
|
} else {
|
|
Duration::from_secs(60 * 5)
|
|
};
|
|
let timeout = cx.background_executor().timer(timeout_duration).shared();
|
|
|
|
let (mut started_tx, mut started_rx) = mpsc::channel(servers_pending_start.len().max(1));
|
|
let (mut diag_tx, mut diag_rx) = mpsc::channel(servers_pending_diagnostics.len().max(1));
|
|
let subscriptions = [cx.subscribe(&lsp_store, {
|
|
let step_progress = step_progress.clone();
|
|
move |lsp_store, event, cx| match event {
|
|
project::LspStoreEvent::LanguageServerAdded(id, name, _) => {
|
|
step_progress.set_substatus(format!("LSP started: {}", name));
|
|
started_tx.try_send(*id).ok();
|
|
}
|
|
project::LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id } => {
|
|
let name = lsp_store
|
|
.read(cx)
|
|
.language_server_adapter_for_id(*language_server_id)
|
|
.unwrap()
|
|
.name();
|
|
step_progress.set_substatus(format!("LSP idle: {}", name));
|
|
diag_tx.try_send(*language_server_id).ok();
|
|
}
|
|
project::LspStoreEvent::LanguageServerUpdate {
|
|
message:
|
|
client::proto::update_language_server::Variant::WorkProgress(
|
|
client::proto::LspWorkProgress {
|
|
message: Some(message),
|
|
..
|
|
},
|
|
),
|
|
..
|
|
} => {
|
|
step_progress.set_substatus(message.clone());
|
|
}
|
|
_ => {}
|
|
}
|
|
})];
|
|
|
|
// Phase 1: wait for all servers to start.
|
|
while !servers_pending_start.is_empty() {
|
|
futures::select! {
|
|
id = started_rx.next() => {
|
|
if let Some(id) = id {
|
|
servers_pending_start.remove(&id);
|
|
}
|
|
},
|
|
_ = timeout.clone().fuse() => {
|
|
return Err(anyhow::anyhow!("LSP wait timed out after {} minutes", timeout_duration.as_secs() / 60));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Save the buffer so the server sees the current content and kicks off diagnostics.
|
|
project
|
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
|
.await?;
|
|
|
|
// Phase 2: wait for all servers to finish their diagnostic pass.
|
|
while !servers_pending_diagnostics.is_empty() {
|
|
futures::select! {
|
|
id = diag_rx.next() => {
|
|
if let Some(id) = id {
|
|
servers_pending_diagnostics.remove(&id);
|
|
}
|
|
},
|
|
_ = timeout.clone().fuse() => {
|
|
return Err(anyhow::anyhow!("LSP wait timed out after {} minutes", timeout_duration.as_secs() / 60));
|
|
}
|
|
}
|
|
}
|
|
|
|
drop(subscriptions);
|
|
step_progress.clear_substatus();
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use zeta_prompt::{ContextSource, RelatedExcerpt, RelatedFile};
|
|
|
|
fn excerpt(
|
|
row_range: std::ops::Range<u32>,
|
|
text: &str,
|
|
order: usize,
|
|
context_source: ContextSource,
|
|
) -> RelatedExcerpt {
|
|
RelatedExcerpt {
|
|
row_range,
|
|
text: Arc::from(text),
|
|
order,
|
|
context_source,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_coalesce_touching_excerpts() {
|
|
let mut excerpts = vec![
|
|
excerpt(2..4, "line2\nline3\n", 1, ContextSource::EditHistory),
|
|
excerpt(0..2, "line0\nline1\n", 3, ContextSource::Bm25),
|
|
excerpt(4..6, "line4\nline5\n", 2, ContextSource::OracleSnippet),
|
|
excerpt(10..12, "line10\nline11\n", 0, ContextSource::Bm25),
|
|
];
|
|
coalesce_touching_excerpts(&mut excerpts);
|
|
|
|
assert_eq!(excerpts.len(), 2);
|
|
assert_eq!(excerpts[0].row_range, 0..6);
|
|
assert_eq!(
|
|
excerpts[0].text.as_ref(),
|
|
"line0\nline1\nline2\nline3\nline4\nline5\n"
|
|
);
|
|
// The merged excerpt keeps the highest priority (minimum order) and
|
|
// its context source.
|
|
assert_eq!(excerpts[0].order, 1);
|
|
assert_eq!(excerpts[0].context_source, ContextSource::EditHistory);
|
|
// The gapped excerpt stays separate.
|
|
assert_eq!(excerpts[1].row_range, 10..12);
|
|
}
|
|
|
|
#[test]
|
|
fn test_coalesce_overlapping_excerpts_drops_duplicated_rows() {
|
|
let mut excerpts = vec![
|
|
excerpt(0..3, "line0\nline1\nline2\n", 0, ContextSource::Bm25),
|
|
excerpt(
|
|
2..5,
|
|
"line2\nline3\nline4\n",
|
|
1,
|
|
ContextSource::OracleSnippet,
|
|
),
|
|
];
|
|
coalesce_touching_excerpts(&mut excerpts);
|
|
|
|
assert_eq!(excerpts.len(), 1);
|
|
assert_eq!(excerpts[0].row_range, 0..5);
|
|
assert_eq!(
|
|
excerpts[0].text.as_ref(),
|
|
"line0\nline1\nline2\nline3\nline4\n"
|
|
);
|
|
assert_eq!(excerpts[0].order, 0);
|
|
assert_eq!(excerpts[0].context_source, ContextSource::Bm25);
|
|
}
|
|
|
|
#[test]
|
|
fn test_coalesce_contained_excerpt_upgrades_order() {
|
|
let mut excerpts = vec![
|
|
excerpt(0..4, "line0\nline1\nline2\nline3\n", 5, ContextSource::Bm25),
|
|
excerpt(1..3, "line1\nline2\n", 2, ContextSource::OracleSnippet),
|
|
];
|
|
coalesce_touching_excerpts(&mut excerpts);
|
|
|
|
assert_eq!(excerpts.len(), 1);
|
|
assert_eq!(excerpts[0].row_range, 0..4);
|
|
assert_eq!(excerpts[0].text.as_ref(), "line0\nline1\nline2\nline3\n");
|
|
assert_eq!(excerpts[0].order, 2);
|
|
assert_eq!(excerpts[0].context_source, ContextSource::OracleSnippet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_coalesce_handles_unterminated_final_line() {
|
|
// An excerpt ending without a newline includes the content of its
|
|
// `row_range.end` row (e.g. git-log excerpts ending at EOF), so a
|
|
// touching excerpt's first row duplicates it.
|
|
let mut excerpts = vec![
|
|
excerpt(0..2, "line0\nline1\nline2", 0, ContextSource::GitLog),
|
|
excerpt(2..4, "line2\nline3\n", 1, ContextSource::Bm25),
|
|
];
|
|
coalesce_touching_excerpts(&mut excerpts);
|
|
|
|
assert_eq!(excerpts.len(), 1);
|
|
assert_eq!(excerpts[0].row_range, 0..4);
|
|
assert_eq!(excerpts[0].text.as_ref(), "line0\nline1\nline2\nline3\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_merge_context_files_coalesces_across_sources() {
|
|
let path: Arc<Path> = Path::new("root/src/lib.rs").into();
|
|
let mut context_files = vec![RelatedFile {
|
|
path: path.clone(),
|
|
max_row: 100,
|
|
excerpts: vec![excerpt(
|
|
0..2,
|
|
"line0\nline1\n",
|
|
0,
|
|
ContextSource::CurrentFile,
|
|
)],
|
|
in_open_source_repo: false,
|
|
}];
|
|
let new_files = vec![RelatedFile {
|
|
path,
|
|
max_row: 100,
|
|
excerpts: vec![excerpt(2..4, "line2\nline3\n", 1, ContextSource::Bm25)],
|
|
in_open_source_repo: false,
|
|
}];
|
|
merge_context_files(&mut context_files, new_files);
|
|
|
|
assert_eq!(context_files.len(), 1);
|
|
assert_eq!(context_files[0].excerpts.len(), 1);
|
|
assert_eq!(context_files[0].excerpts[0].row_range, 0..4);
|
|
assert_eq!(
|
|
context_files[0].excerpts[0].text.as_ref(),
|
|
"line0\nline1\nline2\nline3\n"
|
|
);
|
|
}
|
|
}
|