use serde_json::{Map, Value};
pub(super) fn apply_lines_alias(args: &Map<String, Value>) -> Option<Map<String, Value>> {
let mut rewritten = args.clone();
if !rewritten.contains_key("lines")
&& let Some(key) = rewritten
.keys()
.find(|k| k.len() > "lines:".len() && k.starts_with("lines:"))
.cloned()
{
rewritten.remove(&key);
rewritten.insert("lines".into(), Value::String(key));
}
let base_mode = match rewritten.get("mode") {
None => None,
Some(Value::String(m)) if matches!(m.trim(), "anchored" | "full" | "auto") => {
Some(m.trim().to_string())
}
Some(_) => return None,
};
let range_mode = |payload: String| {
let prefix = if base_mode.as_deref() == Some("anchored") {
"anchored"
} else {
"lines"
};
Value::String(format!("{prefix}:{payload}"))
};
match rewritten.remove("lines")? {
Value::String(text) => {
let payload = text.trim();
let payload = payload.strip_prefix("lines:").unwrap_or(payload);
let payload = payload.trim_start_matches(':');
rewritten.insert("mode".into(), range_mode(payload.to_string()));
}
Value::Array(pair) => match pair.as_slice() {
[start, end] => {
let (start, end) = (start.as_i64()?, end.as_i64()?);
rewritten.insert("mode".into(), range_mode(format!("{start}-{end}")));
}
_ => return None,
},
Value::Number(_) if base_mode.is_some() => return None,
Value::Number(start) => {
if !(rewritten.contains_key("start_line") || rewritten.contains_key("offset")) {
rewritten.insert("start_line".into(), Value::from(start.as_i64()?));
}
}
_ => return None,
}
Some(rewritten)
}
pub(super) fn resolve_line_window(
start_line: Option<i64>,
offset: Option<i64>,
limit: Option<i64>,
) -> Option<(i64, Option<i64>)> {
let start = start_line.or(offset).map(|v| v.max(1));
let limit = limit.filter(|&l| l > 0);
match (start, limit) {
(Some(s), l) => Some((s, l)),
(None, Some(_)) => Some((1, limit)),
(None, None) => None,
}
}
pub(super) fn lines_mode(start: i64, limit: Option<i64>) -> String {
match limit {
Some(l) => format!("lines:{start}-{}", start + l - 1),
None => format!("lines:{start}-999999"),
}
}
pub(super) fn anchored_lines_mode(start: i64, limit: Option<i64>) -> String {
match limit {
Some(l) => format!("anchored:{start}-{}", start + l - 1),
None => format!("anchored:{start}-999999"),
}
}
pub(super) fn resolve_instruction_file_mode(path: &str, mode: &str) -> (String, Option<String>) {
if !crate::tools::ctx_read::instruction_file_forces_full(path, mode) {
return (mode.to_string(), None);
}
(
"full".to_string(),
Some(format!(
"[mode overridden: {mode} -> full, reason=instruction file requires complete content]"
)),
)
}
pub(super) fn scoped_read_ranges(
mode: &str,
) -> Option<Vec<crate::tools::ctx_read::mode::LineRange>> {
use crate::tools::ctx_read::{ReadMode, mode::LineRange};
match mode.parse::<ReadMode>().ok()? {
ReadMode::Lines(range) | ReadMode::Anchored(Some(range)) => Some(vec![range]),
ReadMode::LinesMulti(payload) => Some(
payload
.split(',')
.filter_map(|part| {
let (start, end) = part.split_once('-').unwrap_or((part, part));
Some(LineRange::new(start.parse().ok()?, end.parse().ok()?))
})
.collect(),
),
_ => None,
}
}
pub(super) fn hint_intersects_ranges(
hint: &lean_ctx::core::cross_source_hints::CrossSourceHint,
ranges: &[crate::tools::ctx_read::mode::LineRange],
graph: &lean_ctx::core::property_graph::CodeGraph,
relative_path: &str,
) -> bool {
if hint.relation != "health_hotspot" {
return false;
}
let Some((_, symbol)) = hint.source_uri.rsplit_once('#') else {
return false;
};
let Ok(Some(node)) = graph.get_node_by_symbol(symbol, relative_path) else {
return false;
};
let (Some(start), Some(end)) = (node.line_start, node.line_end) else {
return false;
};
ranges
.iter()
.any(|range| start <= range.end as usize && end >= range.start as usize)
}
pub(super) fn apply_line_window(
mode: &mut String,
fresh: &mut bool,
explicit_mode: bool,
start_line: Option<i64>,
offset: Option<i64>,
limit: Option<i64>,
) {
let preserve_explicit_window = explicit_mode
&& start_line.is_none()
&& offset.is_none()
&& limit.is_some_and(|value| value > 0)
&& matches!(
mode.parse::<crate::tools::ctx_read::ReadMode>(),
Ok(crate::tools::ctx_read::ReadMode::Lines(_)
| crate::tools::ctx_read::ReadMode::LinesTail(_)
| crate::tools::ctx_read::ReadMode::Anchored(Some(_)))
);
if preserve_explicit_window {
return;
}
let Some((start, limit)) = resolve_line_window(start_line, offset, limit) else {
return;
};
if start <= 1 && limit.is_none() {
return;
}
*fresh = true;
if mode == "anchored" {
*mode = anchored_lines_mode(start, limit);
} else {
*mode = lines_mode(start, limit);
}
}
pub(super) fn resolve_raw_alias(arg_raw: bool, mode_arg: Option<String>) -> Option<String> {
if arg_raw {
if let Some(ref m) = mode_arg {
if m.starts_with("lines:") || m.starts_with("anchored:") {
return mode_arg;
}
}
Some("raw".to_string())
} else {
mode_arg
}
}
pub(super) fn canonicalize_tail_mode(mode_arg: Option<String>) -> Option<String> {
let mode = mode_arg?;
Some(crate::tools::ctx_read::canonicalize_tail_mode(&mode).unwrap_or(mode))
}
pub(in crate::tools::registered) fn resolve_explicit_mode(
arg_raw: bool,
mode_arg: Option<String>,
) -> Option<String> {
resolve_raw_alias(arg_raw, canonicalize_tail_mode(mode_arg))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bare_tail_mode_canonicalizes_to_lines_form() {
for (input, expected) in [
("-3", "lines:-3"),
("-1", "lines:-1"),
("-250", "lines:-250"),
] {
assert_eq!(
canonicalize_tail_mode(Some(input.to_string())),
Some(expected.to_string()),
"{input} is the documented tail spelling"
);
}
}
#[test]
fn bare_multi_select_canonicalizes_to_lines_form() {
for (input, expected) in [
("3,7", "lines:3,7"),
("3,7-9", "lines:3,7-9"),
("105,125,784-835", "lines:105,125,784-835"),
("5, 10-20", "lines:5, 10-20"),
("1-3,-5", "lines:1-3,-5"),
] {
assert_eq!(
canonicalize_tail_mode(Some(input.to_string())),
Some(expected.to_string()),
"{input} is the documented multi-select spelling"
);
}
let mode: crate::tools::ctx_read::ReadMode = "lines:3,7-9".parse().unwrap();
assert_eq!(mode.to_string(), "lines:3,7-9");
}
#[test]
fn comma_shapes_that_are_not_line_selections_are_left_alone() {
for mode in ["lines:3,7", "3,x", ",", "3,,7", "full,signatures", "3-,7"] {
assert_eq!(
canonicalize_tail_mode(Some(mode.to_string())),
Some(mode.to_string()),
"{mode} must not be reinterpreted as a line selection"
);
}
}
#[test]
fn canonical_and_other_modes_pass_through_untouched() {
for mode in [
"lines:-3", "full",
"signatures",
"lines:5-10",
"anchored:1-2",
] {
assert_eq!(
canonicalize_tail_mode(Some(mode.to_string())),
Some(mode.to_string())
);
}
assert_eq!(canonicalize_tail_mode(None), None);
}
#[test]
fn dash_shapes_that_are_not_tails_are_left_alone() {
for mode in ["-", "-x", "-3-5", "--3", "-3px", "- 3"] {
assert_eq!(
canonicalize_tail_mode(Some(mode.to_string())),
Some(mode.to_string()),
"{mode} must not be reinterpreted as a tail"
);
}
}
#[test]
fn gh1490_raw_preserves_lines_mode() {
assert_eq!(
resolve_raw_alias(true, Some("lines:90-100".into())),
Some("lines:90-100".into()),
);
assert_eq!(
resolve_raw_alias(true, Some("anchored:5-10".into())),
Some("anchored:5-10".into()),
);
}
#[test]
fn gh1965_raw_keeps_bare_tail_window() {
for (mode, expected) in [
("-3", "lines:-3"),
("lines:-3", "lines:-3"),
("lines:195-201", "lines:195-201"),
("full", "raw"),
] {
assert_eq!(
resolve_explicit_mode(true, Some(mode.into())),
Some(expected.into()),
"raw=true with mode={mode}"
);
}
}
#[test]
fn gh1490_raw_still_works_without_range() {
assert_eq!(resolve_raw_alias(true, None), Some("raw".into()));
assert_eq!(
resolve_raw_alias(true, Some("full".into())),
Some("raw".into()),
);
assert_eq!(
resolve_raw_alias(true, Some("map".into())),
Some("raw".into()),
);
}
#[test]
fn raw_false_passes_through_mode() {
assert_eq!(resolve_raw_alias(false, None), None);
assert_eq!(
resolve_raw_alias(false, Some("lines:5-10".into())),
Some("lines:5-10".into()),
);
}
#[test]
fn instruction_file_rule_preserves_diff_mode() {
let (mode, note) = resolve_instruction_file_mode("AGENTS.md", "diff");
assert_eq!(mode, "diff", "diff must survive the instruction-file rule");
assert!(
note.is_none(),
"no override note when nothing was overridden"
);
let (mode, note) = resolve_instruction_file_mode("AGENTS.md", "signatures");
assert_eq!(mode, "full");
assert!(note.is_some());
}
}