use crate::lsp::{DocumentUri, LspPosition, LspRange};
use anyhow::Context;
use serde::Deserialize;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ReferenceLocation {
pub(crate) path: PathBuf,
pub(crate) range: LspRange,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct LspLocation {
uri: DocumentUri,
range: LspRange,
}
impl LspLocation {
pub(crate) fn into_reference_location(self) -> anyhow::Result<ReferenceLocation> {
Ok(ReferenceLocation {
path: self.uri.to_file_path()?,
range: self.range,
})
}
}
pub(crate) fn parse_reference_locations(
value: serde_json::Value,
) -> anyhow::Result<Vec<ReferenceLocation>> {
if value.is_null() {
return Ok(Vec::new());
}
let locations: Vec<LspLocation> =
serde_json::from_value(value).context("parsing LSP locations")?;
locations
.into_iter()
.map(LspLocation::into_reference_location)
.collect()
}
pub(crate) fn format_references(
locations: &[ReferenceLocation],
limit: usize,
workspace_root: &Path,
) -> String {
let mut lines = Vec::new();
let shown = locations.len().min(limit);
for location in locations.iter().take(shown) {
let path = location
.path
.strip_prefix(workspace_root)
.unwrap_or(&location.path);
lines.push(format!(
"{}:{}:{}",
path.display(),
location.range.start.line + 1,
location.range.start.character + 1
));
}
if locations.len() > shown {
lines.push(format!(
"... truncated {} references",
locations.len() - shown
));
}
crate::output::redact_sensitive_text(&lines.join("\n"))
}
pub(crate) fn one_based_to_lsp(line: u32, column: u32, text: &str) -> anyhow::Result<LspPosition> {
if line == 0 || column == 0 {
anyhow::bail!("line and column must be 1-based positive integers");
}
let zero_based_line = line - 1;
let Some(line_text) = text.lines().nth(zero_based_line as usize) else {
anyhow::bail!("line {line} is outside file");
};
let utf16_character = utf16_units_for_one_based_column(line_text, column)?;
Ok(LspPosition {
line: zero_based_line,
character: utf16_character,
})
}
#[cfg(test)]
pub(crate) fn lsp_to_one_based(position: LspPosition, text: &str) -> anyhow::Result<(u32, u32)> {
let line = position.line + 1;
let Some(line_text) = text.lines().nth(position.line as usize) else {
anyhow::bail!("line {line} is outside file");
};
let column = one_based_column_for_utf16_units(line_text, position.character)?;
Ok((line, column))
}
fn utf16_units_for_one_based_column(line: &str, column: u32) -> anyhow::Result<u32> {
let target_chars = (column - 1) as usize;
if target_chars > line.chars().count() {
anyhow::bail!("column {column} is outside line");
}
Ok(line
.chars()
.take(target_chars)
.map(char::len_utf16)
.sum::<usize>() as u32)
}
#[cfg(test)]
fn one_based_column_for_utf16_units(line: &str, character: u32) -> anyhow::Result<u32> {
let mut utf16_units = 0u32;
let mut column = 1u32;
for ch in line.chars() {
if utf16_units == character {
return Ok(column);
}
utf16_units += ch.len_utf16() as u32;
column += 1;
}
if utf16_units == character {
Ok(column)
} else {
anyhow::bail!("UTF-16 character offset {character} is outside line")
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn references_response_maps_uris_to_paths() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("lib.rs");
std::fs::write(&path, "fn x() {}\n").unwrap();
let uri = DocumentUri::from_path(&path).unwrap();
let locations = parse_reference_locations(json!([{
"uri": uri.as_str(),
"range": {"start": {"line": 2, "character": 3}, "end": {"line": 2, "character": 4}}
}]))
.unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(locations[0].path, path.canonicalize().unwrap());
assert_eq!(locations[0].range.start.character, 3);
}
#[test]
fn format_references_uses_one_based_output_and_limit() {
let root = PathBuf::from("/tmp/root");
let locations = vec![
ReferenceLocation {
path: root.join("a.rs"),
range: LspRange {
start: LspPosition {
line: 0,
character: 4,
},
end: LspPosition {
line: 0,
character: 5,
},
},
},
ReferenceLocation {
path: root.join("b.rs"),
range: LspRange {
start: LspPosition {
line: 1,
character: 0,
},
end: LspPosition {
line: 1,
character: 1,
},
},
},
];
assert_eq!(
format_references(&locations, 1, &root),
"a.rs:1:5\n... truncated 1 references"
);
}
#[test]
fn utf16_coordinate_conversion_handles_non_ascii() {
let text = "a🦀b\nnext";
let pos = one_based_to_lsp(1, 3, text).unwrap();
assert_eq!(pos.line, 0);
assert_eq!(pos.character, 3);
assert_eq!(lsp_to_one_based(pos, text).unwrap(), (1, 3));
assert!(one_based_to_lsp(0, 1, text).is_err());
assert!(one_based_to_lsp(1, 99, text).is_err());
}
}