use crate::{EditPredictionStore, StoredEvent, zeta}; use anyhow::{Context as _, Result};use buffer_diff::BufferDiffSnapshot;use collections::HashMap;use gpui::{Context, Entity, Task};use language::{Buffer, BufferSnapshot, ToPoint as _};use project::{Project, ProjectPath, WorktreeId};use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};use text::{OffsetRangeExt as _, Point};use util::rel_path::RelPath; const MAX_UNCOMMITTED_DIFF_SIZE: usize = 64 * 1024; pub type UncommittedDiffSnapshot = Vec<(Arc<Path>, BufferSnapshot, BufferDiffSnapshot)>;pub type UncommittedDiffResult = std::result::Result<UncommittedDiffSnapshot, Arc<anyhow::Error>>; pub use zeta_prompt::udiff::CURSOR_POSITION_MARKER; pub(crate) struct CapturedPredictionContext { pub(crate) repository_url: Option<String>, pub(crate) revision: Option<String>, pub(crate) uncommitted_diff: Option<String>, pub(crate) buffer_diagnostics: Vec<zeta_prompt::ActiveBufferDiagnostic>, pub(crate) editable_context: Vec<zeta_prompt::RelatedFile>,} pub(crate) fn capture_prediction_context( project: Entity<Project>, buffer: Entity<Buffer>, cursor_anchor: language::Anchor, stored_events: Vec<StoredEvent>, repository_url: Option<String>, revision: Option<String>, editable_context_task: Task<Result<Vec<zeta_prompt::RelatedFile>>>, cx: &mut Context<EditPredictionStore>,) -> Option<Task<Result<CapturedPredictionContext>>> { let snapshot = buffer.read(cx).snapshot(); let worktree_id = snapshot.file()?.worktree_id(cx); let uncommitted_diff_task = uncommitted_diffs_for_events(project, worktree_id, stored_events, cx); Some(cx.spawn(async move |_this, cx| { let uncommitted_diff_snapshot = match uncommitted_diff_task.await { Ok(snapshot) => Some(snapshot), Err(error) => { log::debug!("failed to capture uncommitted diff: {error:?}"); None } }; let uncommitted_diff = if let Some(uncommitted_diff_snapshot) = uncommitted_diff_snapshot { let estimated_uncommitted_diff_size = uncommitted_diff_snapshot .iter() .map(|(_, buffer_snapshot, diff_snapshot)| { diff_snapshot .hunks(buffer_snapshot) .map(|hunk| { hunk.diff_base_byte_range.len() + hunk.range.to_offset(buffer_snapshot).len() }) .sum::<usize>() }) .sum::<usize>(); if estimated_uncommitted_diff_size <= MAX_UNCOMMITTED_DIFF_SIZE { let uncommitted_diff = cx .background_executor() .spawn(async move { compute_uncommitted_diff(uncommitted_diff_snapshot) }) .await; (uncommitted_diff.len() <= MAX_UNCOMMITTED_DIFF_SIZE).then_some(uncommitted_diff) } else { None } } else { None }; let buffer_diagnostics = zeta::active_buffer_diagnostics( &snapshot, Point::new(0, 0)..snapshot.max_point(), cursor_anchor.to_point(&snapshot).row, 100, ); let editable_context = match editable_context_task.await { Ok(editable_context) => editable_context, Err(error) => { log::debug!("failed to capture editable context: {error:?}"); Vec::new() } }; Ok(CapturedPredictionContext { repository_url, revision, uncommitted_diff, buffer_diagnostics, editable_context, }) }))} pub fn uncommitted_diffs_for_events( project: Entity<Project>, worktree_id: WorktreeId, events: Vec<StoredEvent>, cx: &Context<'_, EditPredictionStore>,) -> Task<UncommittedDiffResult> { let git_store = project.read_with(cx, |project, _| project.git_store().clone()); cx.spawn(async move |_store, cx| { let (worktree_root_name, worktree_abs_path, path_style) = project .read_with(cx, |project, cx| { let worktree = project.worktree_for_id(worktree_id, cx)?; let worktree = worktree.read(cx); let path_style = worktree.path_style(); let root_name = RelPath::new(Path::new(worktree.root_name_str()), path_style) .ok()? .into_owned(); Some((root_name, worktree.abs_path(), path_style)) }) .context("failed to find worktree for uncommitted diff capture") .map_err(Arc::new)?; let events_with_paths = events .into_iter() .filter_map(|stored_event| { let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref(); let path = if let Ok(path) = RelPath::new(path, path_style) { path.strip_prefix(&worktree_root_name).ok()?.into_arc() } else { let path = path.strip_prefix(worktree_abs_path.as_ref()).ok()?; RelPath::new(path, path_style).ok()?.into_arc() }; let project_path = ProjectPath { worktree_id, path }; let relative_path: Arc<Path> = project_path.path.as_std_path().into(); Some((stored_event, project_path, relative_path)) }) .collect::<Vec<_>>(); let mut snapshots_by_path: HashMap<Arc<Path>, (BufferSnapshot, BufferDiffSnapshot)> = HashMap::default(); for (stored_event, project_path, relative_path) in events_with_paths.iter().rev() { if snapshots_by_path.contains_key(relative_path) { continue; } let buffer = project .update(cx, |project, cx| { project.open_buffer(project_path.clone(), cx) }) .await .context("failed to open buffer for uncommitted diff capture") .map_err(Arc::new)?; let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id()); let file_context = stored_event.file_context.clone(); let cached_diff = file_context .as_ref() .and_then(|file_context| { file_context .read_with(cx, |file_context, _| file_context.uncommitted_diff.clone()) }) // The cached diff is keyed by path, but its hunk anchors are pinned to a // specific buffer. If that buffer was closed and reopened, `open_buffer` // hands back a buffer with a new `BufferId`; reusing the stale diff against // it would mix anchors from different buffers and panic. Drop the cache in // that case so the diff is recomputed for the current buffer. .filter(|diff| diff.read_with(cx, |diff, _| diff.buffer_id) == buffer_id); let diff = match cached_diff { Some(diff) => diff, None => { let diff = git_store .update(cx, |git_store, cx| { git_store.open_uncommitted_diff(buffer.clone(), cx) }) .await .context("failed to open uncommitted diff for capture") .map_err(Arc::new)?; if let Some(file_context) = file_context { file_context.update(cx, |file_context, _| { file_context.uncommitted_diff = Some(diff.clone()); }); } diff } }; let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx)); snapshots_by_path.insert(relative_path.clone(), (buffer_snapshot, diff_snapshot)); } let uncommitted_diff_snapshots = snapshots_by_path .into_iter() .map(|(relative_path, (snapshot, diff_snapshot))| { (relative_path, snapshot, diff_snapshot) }) .collect(); Ok(uncommitted_diff_snapshots) })} pub fn compute_cursor_excerpt( snapshot: &language::BufferSnapshot, cursor_anchor: language::Anchor,) -> (String, usize, Range<Point>) { use text::ToOffset as _; use text::ToPoint as _; let cursor_offset = cursor_anchor.to_offset(snapshot); let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) = crate::cursor_excerpt::compute_cursor_excerpt(snapshot, cursor_offset); let syntax_ranges = crate::cursor_excerpt::compute_syntax_ranges( snapshot, cursor_offset, &excerpt_offset_range, ); let excerpt_text: String = snapshot.text_for_range(excerpt_point_range).collect(); let (_, context_range) = zeta_prompt::compute_editable_and_context_ranges( &excerpt_text, cursor_offset_in_excerpt, &syntax_ranges, 100, 50, ); let context_text = excerpt_text[context_range.clone()].to_string(); let cursor_in_context = cursor_offset_in_excerpt.saturating_sub(context_range.start); let context_buffer_start = (excerpt_offset_range.start + context_range.start).to_point(snapshot); let context_buffer_end = (excerpt_offset_range.start + context_range.end).to_point(snapshot); ( context_text, cursor_in_context, context_buffer_start..context_buffer_end, )} pub(crate) fn compute_uncommitted_diff(snapshot: UncommittedDiffSnapshot) -> String { let mut uncommitted_diff = String::new(); let mut snapshots_by_path = snapshot; snapshots_by_path.sort_by(|(left_path, _, _), (right_path, _, _)| left_path.cmp(right_path)); for (relative_path, buffer_snapshot, diff_snapshot) in snapshots_by_path { let base_snapshot = diff_snapshot.base_text(); let is_existing_file = diff_snapshot.base_text_exists(); let new_path_str = relative_path.to_string_lossy(); let old_path_str = if is_existing_file { new_path_str.as_ref() } else { "/dev/null" }; writeln!( uncommitted_diff, "--- {}{old_path_str}", if is_existing_file { "a/" } else { "" } ) .ok(); writeln!(uncommitted_diff, "+++ b/{new_path_str}").ok(); if !is_existing_file { let new_text = buffer_snapshot.text(); writeln!( uncommitted_diff, "@@ -0,0 +1,{} @@", new_text.lines().count() ) .ok(); for line in new_text.lines() { writeln!(uncommitted_diff, "+{line}").ok(); } continue; } let mut ranges: Vec<(Range<u32>, Range<u32>)> = Vec::new(); for hunk in (&diff_snapshot).hunks(&buffer_snapshot) { let old_start = base_snapshot .offset_to_point(hunk.diff_base_byte_range.start) .row; let old_end = exclusive_end_row(base_snapshot.offset_to_point(hunk.diff_base_byte_range.end)); let new_start = hunk.range.start.row; let new_end = exclusive_end_row(hunk.range.end); let old_range = old_start.saturating_sub(3)..old_end + 3; let new_range = new_start.saturating_sub(3)..new_end + 3; if let Some((last_old_range, last_new_range)) = ranges.last_mut() && (old_range.start <= last_old_range.end || new_range.start <= last_new_range.end) { last_old_range.end = last_old_range.end.max(old_range.end); last_new_range.end = last_new_range.end.max(new_range.end); continue; } ranges.push((old_range, new_range)); } for (old_range, new_range) in ranges { uncommitted_diff.push_str(&language::unified_diff_with_offsets( &base_snapshot .text_for_range( Point::new(old_range.start, 0) ..row_start_or_max(base_snapshot, old_range.end), ) .collect::<String>(), &buffer_snapshot .text_for_range( Point::new(new_range.start, 0) ..row_start_or_max(&buffer_snapshot, new_range.end), ) .collect::<String>(), old_range.start, new_range.start, )); } if !uncommitted_diff.ends_with('\n') { uncommitted_diff.push('\n'); } } uncommitted_diff} fn row_start_or_max(snapshot: &language::BufferSnapshot, row: u32) -> Point { if row >= snapshot.max_point().row { snapshot.max_point() } else { Point::new(row, 0) }} fn exclusive_end_row(point: Point) -> u32 { if point.column == 0 { point.row } else { point.row + 1 }} pub fn format_cursor_excerpt( excerpt: &str, cursor_offset: usize, line_comment_prefix: &str,) -> String { let cursor_line_start = excerpt[..cursor_offset] .rfind('\n') .map(|pos| pos + 1) .unwrap_or(0); let cursor_line_end = excerpt[cursor_line_start..] .find('\n') .map(|pos| cursor_line_start + pos + 1) .unwrap_or(excerpt.len()); let cursor_line = &excerpt[cursor_line_start..cursor_line_end]; let cursor_line_indent = &cursor_line[..cursor_line.len() - cursor_line.trim_start().len()]; let cursor_column = cursor_offset - cursor_line_start; let mut marker_line = String::new(); if cursor_column < line_comment_prefix.len() { for _ in 0..cursor_column { marker_line.push(' '); } marker_line.push_str(line_comment_prefix); write!(marker_line, " <{}", CURSOR_POSITION_MARKER).unwrap(); } else { if cursor_column >= cursor_line_indent.len() + line_comment_prefix.len() { marker_line.push_str(cursor_line_indent); } marker_line.push_str(line_comment_prefix); while marker_line.len() < cursor_column { marker_line.push(' '); } write!(marker_line, "^{}", CURSOR_POSITION_MARKER).unwrap(); } let mut result = String::with_capacity(excerpt.len() + marker_line.len() + 2); result.push_str(&excerpt[..cursor_line_end]); if !result.ends_with('\n') { result.push('\n'); } result.push_str(&marker_line); if cursor_line_end < excerpt.len() { result.push('\n'); result.push_str(&excerpt[cursor_line_end..]); } result}
use crate::{EditPredictionStore, StoredEvent, zeta};
use anyhow::{Context as _, Result};
use buffer_diff::BufferDiffSnapshot;
use collections::HashMap;
use gpui::{Context, Entity, Task};
use language::{Buffer, BufferSnapshot, ToPoint as _};
use project::{Project, ProjectPath, WorktreeId};
use std::{fmt::Write as _, ops::Range, path::Path, sync::Arc};
use text::{OffsetRangeExt as _, Point};
use util::rel_path::RelPath;
const MAX_UNCOMMITTED_DIFF_SIZE: usize = 64 * 1024;
pub type UncommittedDiffSnapshot = Vec<(Arc<Path>, BufferSnapshot, BufferDiffSnapshot)>;
pub type UncommittedDiffResult = std::result::Result<UncommittedDiffSnapshot, Arc<anyhow::Error>>;
pub use zeta_prompt::udiff::CURSOR_POSITION_MARKER;
pub(crate) struct CapturedPredictionContext {
pub(crate) repository_url: Option<String>,
pub(crate) revision: Option<String>,
pub(crate) uncommitted_diff: Option<String>,
pub(crate) buffer_diagnostics: Vec<zeta_prompt::ActiveBufferDiagnostic>,
pub(crate) editable_context: Vec<zeta_prompt::RelatedFile>,
}
pub(crate) fn capture_prediction_context(
project: Entity<Project>,
buffer: Entity<Buffer>,
cursor_anchor: language::Anchor,
stored_events: Vec<StoredEvent>,
repository_url: Option<String>,
revision: Option<String>,
editable_context_task: Task<Result<Vec<zeta_prompt::RelatedFile>>>,
cx: &mut Context<EditPredictionStore>,
) -> Option<Task<Result<CapturedPredictionContext>>> {
let snapshot = buffer.read(cx).snapshot();
let worktree_id = snapshot.file()?.worktree_id(cx);
let uncommitted_diff_task =
uncommitted_diffs_for_events(project, worktree_id, stored_events, cx);
Some(cx.spawn(async move |_this, cx| {
let uncommitted_diff_snapshot = match uncommitted_diff_task.await {
Ok(snapshot) => Some(snapshot),
Err(error) => {
log::debug!("failed to capture uncommitted diff: {error:?}");
None
}
};
let uncommitted_diff = if let Some(uncommitted_diff_snapshot) = uncommitted_diff_snapshot {
let estimated_uncommitted_diff_size = uncommitted_diff_snapshot
.iter()
.map(|(_, buffer_snapshot, diff_snapshot)| {
diff_snapshot
.hunks(buffer_snapshot)
.map(|hunk| {
hunk.diff_base_byte_range.len()
+ hunk.range.to_offset(buffer_snapshot).len()
})
.sum::<usize>()
})
.sum::<usize>();
if estimated_uncommitted_diff_size <= MAX_UNCOMMITTED_DIFF_SIZE {
let uncommitted_diff = cx
.background_executor()
.spawn(async move { compute_uncommitted_diff(uncommitted_diff_snapshot) })
.await;
(uncommitted_diff.len() <= MAX_UNCOMMITTED_DIFF_SIZE).then_some(uncommitted_diff)
} else {
None
}
} else {
None
};
let buffer_diagnostics = zeta::active_buffer_diagnostics(
&snapshot,
Point::new(0, 0)..snapshot.max_point(),
cursor_anchor.to_point(&snapshot).row,
100,
);
let editable_context = match editable_context_task.await {
Ok(editable_context) => editable_context,
Err(error) => {
log::debug!("failed to capture editable context: {error:?}");
Vec::new()
}
};
Ok(CapturedPredictionContext {
repository_url,
revision,
uncommitted_diff,
buffer_diagnostics,
editable_context,
})
}))
}
pub fn uncommitted_diffs_for_events(
project: Entity<Project>,
worktree_id: WorktreeId,
events: Vec<StoredEvent>,
cx: &Context<'_, EditPredictionStore>,
) -> Task<UncommittedDiffResult> {
let git_store = project.read_with(cx, |project, _| project.git_store().clone());
cx.spawn(async move |_store, cx| {
let (worktree_root_name, worktree_abs_path, path_style) = project
.read_with(cx, |project, cx| {
let worktree = project.worktree_for_id(worktree_id, cx)?;
let worktree = worktree.read(cx);
let path_style = worktree.path_style();
let root_name = RelPath::new(Path::new(worktree.root_name_str()), path_style)
.ok()?
.into_owned();
Some((root_name, worktree.abs_path(), path_style))
})
.context("failed to find worktree for uncommitted diff capture")
.map_err(Arc::new)?;
let events_with_paths = events
.into_iter()
.filter_map(|stored_event| {
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
let path = if let Ok(path) = RelPath::new(path, path_style) {
path.strip_prefix(&worktree_root_name).ok()?.into_arc()
} else {
let path = path.strip_prefix(worktree_abs_path.as_ref()).ok()?;
RelPath::new(path, path_style).ok()?.into_arc()
};
let project_path = ProjectPath { worktree_id, path };
let relative_path: Arc<Path> = project_path.path.as_std_path().into();
Some((stored_event, project_path, relative_path))
})
.collect::<Vec<_>>();
let mut snapshots_by_path: HashMap<Arc<Path>, (BufferSnapshot, BufferDiffSnapshot)> =
HashMap::default();
for (stored_event, project_path, relative_path) in events_with_paths.iter().rev() {
if snapshots_by_path.contains_key(relative_path) {
continue;
}
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.context("failed to open buffer for uncommitted diff capture")
.map_err(Arc::new)?;
let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id());
let file_context = stored_event.file_context.clone();
let cached_diff = file_context
.as_ref()
.and_then(|file_context| {
file_context
.read_with(cx, |file_context, _| file_context.uncommitted_diff.clone())
})
// The cached diff is keyed by path, but its hunk anchors are pinned to a
// specific buffer. If that buffer was closed and reopened, `open_buffer`
// hands back a buffer with a new `BufferId`; reusing the stale diff against
// it would mix anchors from different buffers and panic. Drop the cache in
// that case so the diff is recomputed for the current buffer.
.filter(|diff| diff.read_with(cx, |diff, _| diff.buffer_id) == buffer_id);
let diff = match cached_diff {
Some(diff) => diff,
None => {
let diff = git_store
.update(cx, |git_store, cx| {
git_store.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.context("failed to open uncommitted diff for capture")
.map_err(Arc::new)?;
if let Some(file_context) = file_context {
file_context.update(cx, |file_context, _| {
file_context.uncommitted_diff = Some(diff.clone());
});
}
diff
}
};
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx));
snapshots_by_path.insert(relative_path.clone(), (buffer_snapshot, diff_snapshot));
}
let uncommitted_diff_snapshots = snapshots_by_path
.into_iter()
.map(|(relative_path, (snapshot, diff_snapshot))| {
(relative_path, snapshot, diff_snapshot)
})
.collect();
Ok(uncommitted_diff_snapshots)
})
}
pub fn compute_cursor_excerpt(
snapshot: &language::BufferSnapshot,
cursor_anchor: language::Anchor,
) -> (String, usize, Range<Point>) {
use text::ToOffset as _;
use text::ToPoint as _;
let cursor_offset = cursor_anchor.to_offset(snapshot);
let (excerpt_point_range, excerpt_offset_range, cursor_offset_in_excerpt) =
crate::cursor_excerpt::compute_cursor_excerpt(snapshot, cursor_offset);
let syntax_ranges = crate::cursor_excerpt::compute_syntax_ranges(
snapshot,
cursor_offset,
&excerpt_offset_range,
);
let excerpt_text: String = snapshot.text_for_range(excerpt_point_range).collect();
let (_, context_range) = zeta_prompt::compute_editable_and_context_ranges(
&excerpt_text,
cursor_offset_in_excerpt,
&syntax_ranges,
100,
50,
);
let context_text = excerpt_text[context_range.clone()].to_string();
let cursor_in_context = cursor_offset_in_excerpt.saturating_sub(context_range.start);
let context_buffer_start =
(excerpt_offset_range.start + context_range.start).to_point(snapshot);
let context_buffer_end = (excerpt_offset_range.start + context_range.end).to_point(snapshot);
(
context_text,
cursor_in_context,
context_buffer_start..context_buffer_end,
)
}
pub(crate) fn compute_uncommitted_diff(snapshot: UncommittedDiffSnapshot) -> String {
let mut uncommitted_diff = String::new();
let mut snapshots_by_path = snapshot;
snapshots_by_path.sort_by(|(left_path, _, _), (right_path, _, _)| left_path.cmp(right_path));
for (relative_path, buffer_snapshot, diff_snapshot) in snapshots_by_path {
let base_snapshot = diff_snapshot.base_text();
let is_existing_file = diff_snapshot.base_text_exists();
let new_path_str = relative_path.to_string_lossy();
let old_path_str = if is_existing_file {
new_path_str.as_ref()
} else {
"/dev/null"
};
writeln!(
uncommitted_diff,
"--- {}{old_path_str}",
if is_existing_file { "a/" } else { "" }
)
.ok();
writeln!(uncommitted_diff, "+++ b/{new_path_str}").ok();
if !is_existing_file {
let new_text = buffer_snapshot.text();
writeln!(
uncommitted_diff,
"@@ -0,0 +1,{} @@",
new_text.lines().count()
)
.ok();
for line in new_text.lines() {
writeln!(uncommitted_diff, "+{line}").ok();
}
continue;
}
let mut ranges: Vec<(Range<u32>, Range<u32>)> = Vec::new();
for hunk in (&diff_snapshot).hunks(&buffer_snapshot) {
let old_start = base_snapshot
.offset_to_point(hunk.diff_base_byte_range.start)
.row;
let old_end =
exclusive_end_row(base_snapshot.offset_to_point(hunk.diff_base_byte_range.end));
let new_start = hunk.range.start.row;
let new_end = exclusive_end_row(hunk.range.end);
let old_range = old_start.saturating_sub(3)..old_end + 3;
let new_range = new_start.saturating_sub(3)..new_end + 3;
if let Some((last_old_range, last_new_range)) = ranges.last_mut()
&& (old_range.start <= last_old_range.end || new_range.start <= last_new_range.end)
{
last_old_range.end = last_old_range.end.max(old_range.end);
last_new_range.end = last_new_range.end.max(new_range.end);
continue;
}
ranges.push((old_range, new_range));
}
for (old_range, new_range) in ranges {
uncommitted_diff.push_str(&language::unified_diff_with_offsets(
&base_snapshot
.text_for_range(
Point::new(old_range.start, 0)
..row_start_or_max(base_snapshot, old_range.end),
)
.collect::<String>(),
&buffer_snapshot
.text_for_range(
Point::new(new_range.start, 0)
..row_start_or_max(&buffer_snapshot, new_range.end),
)
.collect::<String>(),
old_range.start,
new_range.start,
));
}
if !uncommitted_diff.ends_with('\n') {
uncommitted_diff.push('\n');
}
}
uncommitted_diff
}
fn row_start_or_max(snapshot: &language::BufferSnapshot, row: u32) -> Point {
if row >= snapshot.max_point().row {
snapshot.max_point()
} else {
Point::new(row, 0)
}
}
fn exclusive_end_row(point: Point) -> u32 {
if point.column == 0 {
point.row
} else {
point.row + 1
}
}
pub fn format_cursor_excerpt(
excerpt: &str,
cursor_offset: usize,
line_comment_prefix: &str,
) -> String {
let cursor_line_start = excerpt[..cursor_offset]
.rfind('\n')
.map(|pos| pos + 1)
.unwrap_or(0);
let cursor_line_end = excerpt[cursor_line_start..]
.find('\n')
.map(|pos| cursor_line_start + pos + 1)
.unwrap_or(excerpt.len());
let cursor_line = &excerpt[cursor_line_start..cursor_line_end];
let cursor_line_indent = &cursor_line[..cursor_line.len() - cursor_line.trim_start().len()];
let cursor_column = cursor_offset - cursor_line_start;
let mut marker_line = String::new();
if cursor_column < line_comment_prefix.len() {
for _ in 0..cursor_column {
marker_line.push(' ');
}
marker_line.push_str(line_comment_prefix);
write!(marker_line, " <{}", CURSOR_POSITION_MARKER).unwrap();
} else {
if cursor_column >= cursor_line_indent.len() + line_comment_prefix.len() {
marker_line.push_str(cursor_line_indent);
}
marker_line.push_str(line_comment_prefix);
while marker_line.len() < cursor_column {
marker_line.push(' ');
}
write!(marker_line, "^{}", CURSOR_POSITION_MARKER).unwrap();
}
let mut result = String::with_capacity(excerpt.len() + marker_line.len() + 2);
result.push_str(&excerpt[..cursor_line_end]);
if !result.ends_with('\n') {
result.push('\n');
}
result.push_str(&marker_line);
if cursor_line_end < excerpt.len() {
result.push('\n');
result.push_str(&excerpt[cursor_line_end..]);
}
result
}