use crate::parser;
use tower_lsp::lsp_types::*;
#[derive(Debug, Clone)]
pub struct VariableLocation {
pub name: String,
pub line: u32,
pub character: u32,
pub uri: String,
}
pub fn find_variable_definition(
content: &str,
position: Position,
uri: &str,
) -> Option<VariableLocation> {
let lines: Vec<&str> = content.lines().collect();
let line_idx = position.line as usize;
if line_idx >= lines.len() {
return None;
}
let line = lines[line_idx];
let char_idx = position.character as usize;
if char_idx >= line.len() {
return None;
}
if let Some(var_name) = extract_variable_at_position(line, char_idx) {
return find_variable_in_extract(content, &var_name, uri);
}
None
}
pub fn extract_variable_at_position(line: &str, char_idx: usize) -> Option<String> {
let before = &line[..char_idx];
let after = &line[char_idx..];
let open_start = before.rfind("{{")?;
let close_end = after.find("}}")?;
let var_content = &line[open_start + 2..char_idx + close_end];
let var_name = var_content.trim();
if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') && !var_name.is_empty() {
Some(var_name.to_string())
} else {
None
}
}
fn find_variable_in_extract(content: &str, var_name: &str, uri: &str) -> Option<VariableLocation> {
let doc = parser::parse_gctf_from_str(content, "lsp-document.gctf").ok()?;
for section in &doc.sections {
if section.section_type != parser::ast::SectionType::Extract {
continue;
}
for (idx, line) in section.raw_content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
if let Some(extract_var) = parser::ExtractVar::parse(trimmed)
&& extract_var.name == var_name
{
let line_num = section.start_line as u32 + idx as u32 + 1;
let character = line
.find(&extract_var.name)
.map(|pos| pos as u32)
.unwrap_or(0);
return Some(VariableLocation {
name: var_name.to_string(),
line: line_num,
character,
uri: uri.to_string(),
});
}
}
}
None
}
pub fn variable_location_to_lsp(loc: &VariableLocation) -> Location {
Location {
uri: Url::parse(&loc.uri).unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range {
start: Position {
line: loc.line,
character: loc.character,
},
end: Position {
line: loc.line,
character: loc.character + loc.name.len() as u32,
},
},
}
}
pub fn find_variable_references(content: &str, var_name: &str, uri: &str) -> Vec<Location> {
let mut references = Vec::new();
if let Ok(doc) = parser::parse_gctf_from_str(content, "lsp-document.gctf") {
for section in &doc.sections {
for (idx, line) in section.raw_content.lines().enumerate() {
let mut search_start = 0;
while let Some(var_start) = line[search_start..].find("{{") {
let abs_start = search_start + var_start;
if let Some(var_end) = line[abs_start..].find("}}") {
let var_content = line[abs_start + 2..abs_start + var_end].trim();
if var_content == var_name {
references.push(Location {
uri: Url::parse(uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range {
start: Position {
line: section.start_line as u32 + idx as u32 + 1,
character: abs_start as u32,
},
end: Position {
line: section.start_line as u32 + idx as u32 + 1,
character: (abs_start + var_end + 2) as u32,
},
},
});
}
search_start = abs_start + var_end + 2;
} else {
break;
}
}
}
}
return references;
}
for (line_idx, line) in content.lines().enumerate() {
let mut search_start = 0;
while let Some(var_start) = line[search_start..].find("{{") {
let abs_start = search_start + var_start;
if let Some(var_end) = line[abs_start..].find("}}") {
let var_content = line[abs_start + 2..abs_start + var_end].trim();
if var_content == var_name {
references.push(Location {
uri: Url::parse(uri)
.unwrap_or_else(|_| Url::parse("file:///unknown").unwrap()),
range: Range {
start: Position {
line: line_idx as u32,
character: abs_start as u32,
},
end: Position {
line: line_idx as u32,
character: (abs_start + var_end + 2) as u32,
},
},
});
}
search_start = abs_start + var_end + 2;
} else {
break;
}
}
}
references
}
pub fn get_all_variables(content: &str) -> Vec<(String, u32)> {
let mut variables = Vec::new();
if let Ok(doc) = parser::parse_gctf_from_str(content, "lsp-document.gctf") {
for section in &doc.sections {
if section.section_type != parser::ast::SectionType::Extract {
continue;
}
for (idx, line) in section.raw_content.lines().enumerate() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
if let Some(extract_var) = parser::ExtractVar::parse(trimmed) {
variables.push((extract_var.name, section.start_line as u32 + idx as u32 + 1));
}
}
}
}
variables
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_variable_at_position() {
let line = "Authorization: Bearer {{ token }}";
let result = extract_variable_at_position(line, 25);
assert_eq!(result, Some("token".to_string()));
}
#[test]
fn test_extract_variable_at_position_not_on_variable() {
let line = "Authorization: Bearer {{ token }}";
let result = extract_variable_at_position(line, 0);
assert!(result.is_none());
}
#[test]
fn test_find_variable_in_extract() {
let content = r#"
--- ENDPOINT ---
test.Service/Method
--- RESPONSE ---
{"token": "abc"}
--- EXTRACT ---
auth_token = .token
user_id = .user_id
--- ASSERTS ---
@len({{ auth_token }}) > 0
"#;
let loc = find_variable_in_extract(content, "auth_token", "file:///test.gctf");
assert!(loc.is_some());
assert_eq!(loc.unwrap().name, "auth_token");
}
#[test]
fn test_find_variable_not_found() {
let content = r#"
--- ENDPOINT ---
test.Service/Method
--- RESPONSE ---
{"token": "abc"}
--- EXTRACT ---
auth_token = .token
--- ASSERTS ---
@len({{ auth_token }}) > 0
"#;
let loc = find_variable_in_extract(content, "nonexistent", "file:///test.gctf");
assert!(loc.is_none());
}
#[test]
fn test_get_all_variables() {
let content = r#"
--- ENDPOINT ---
test.Service/Method
--- RESPONSE ---
{"token": "abc", "id": 123}
--- EXTRACT ---
auth_token = .token
user_id = .user_id
user_name = .name
--- ASSERTS ---
@len({{ auth_token }}) > 0
"#;
let vars = get_all_variables(content);
assert_eq!(vars.len(), 3);
assert_eq!(vars[0].0, "auth_token");
assert_eq!(vars[1].0, "user_id");
assert_eq!(vars[2].0, "user_name");
}
#[test]
fn test_find_variable_references() {
let content = r#"
--- ENDPOINT ---
test.Service/Method
--- RESPONSE ---
{"token": "abc"}
--- EXTRACT ---
token = .token
--- ASSERTS ---
@len({{ token }}) > 0
{{ token }} != null
"#;
let refs = find_variable_references(content, "token", "file:///test.gctf");
assert_eq!(refs.len(), 2);
}
#[test]
fn test_variable_location_to_lsp() {
let var_loc = VariableLocation {
name: "test_var".to_string(),
line: 10,
character: 5,
uri: "file:///test.gctf".to_string(),
};
let lsp_loc = variable_location_to_lsp(&var_loc);
assert_eq!(lsp_loc.range.start.line, 10);
assert_eq!(lsp_loc.range.start.character, 5);
}
#[test]
fn test_find_variable_definition_full() {
let content = r#"
--- ENDPOINT ---
test.Service/Method
--- REQUEST ---
{"id": 123}
--- RESPONSE ---
{"token": "abc"}
--- EXTRACT ---
auth_token = .token
--- ASSERTS ---
@len({{ auth_token }}) > 0
"#;
let position = Position {
line: 14,
character: 15,
};
let result = find_variable_definition(content, position, "file:///test.gctf");
assert!(result.is_some());
assert_eq!(result.unwrap().name, "auth_token");
}
}