use std::collections::HashMap;
use std::path::Path;
use anyhow::Result;
use crate::commands::lsp_engine::LspEngine;
use crate::data::buffer::Buffer;
use crate::data::chord_types::{Action, Component, Positional, Scope};
use crate::data::lsp::types::{DocumentSymbol, SymbolKind};
use super::errors::ChordError;
use super::text::{char_to_byte, extract_range_text, line_char_count};
use super::types::{
BufferResolution, ChordQuery, CursorPosition, EditorMode, ListItem, ResolvedChord, TextRange,
};
pub fn resolve(
query: &ChordQuery,
buffers: &HashMap<String, Buffer>,
lsp: &mut LspEngine,
) -> Result<ResolvedChord> {
let mut resolutions = HashMap::new();
for (name, buffer) in buffers {
let resolution = resolve_buffer(query, name, buffer, lsp)?;
resolutions.insert(name.clone(), resolution);
}
Ok(ResolvedChord {
query: query.clone(),
resolutions,
})
}
fn resolve_buffer(
query: &ChordQuery,
buffer_name: &str,
buffer: &Buffer,
lsp: &mut LspEngine,
) -> Result<BufferResolution> {
if query.action == Action::List {
let (listed_items, warnings) = resolve_list(query, buffer, lsp, buffer_name)?;
return Ok(BufferResolution {
target_ranges: vec![TextRange::point(0, 0)],
scope_range: TextRange::point(0, 0),
component_range: TextRange::point(0, 0),
replacement: None,
cursor_destination: None,
mode_after: None,
listed_items,
warnings,
});
}
if let Positional::Count(n) = query.positional {
return resolve_count_positional(query, buffer_name, buffer, lsp, n);
}
let scope_range = resolve_scope(query, buffer_name, buffer, lsp)?;
let component_range = resolve_component(query, buffer, &scope_range, buffer_name, lsp)?;
let mut target_ranges =
apply_positional(query, buffer, &scope_range, &component_range, buffer_name)?;
if query.action == Action::Jump && query.positional == Positional::Outside {
target_ranges = match query.component {
Component::Beginning => vec![position_before_scope(buffer, &scope_range)],
Component::End => vec![position_after_scope(buffer, &scope_range)],
_ => target_ranges,
};
}
let mut replacement = query.args.value.clone();
let is_interactive_cifc = query.action == Action::Change
&& query.positional == Positional::Inside
&& query.scope == Scope::Function
&& query.component == Component::Contents
&& replacement.is_none();
if is_interactive_cifc && let Some(primary) = target_ranges.first() {
let body_indent = buffer
.lines
.get(primary.start_line)
.map(|l| {
let ws: String = l.chars().take_while(|c| c.is_whitespace()).collect();
ws
})
.filter(|ws| !ws.is_empty());
let indent = body_indent.unwrap_or_else(|| {
let brace_indent: String = buffer
.lines
.get(primary.end_line + 1)
.map(|l| l.chars().take_while(|c| c.is_whitespace()).collect())
.unwrap_or_default();
format!("{brace_indent} ")
});
replacement = Some(indent);
}
let is_interactive_newline = replacement.is_none()
&& query.scope == Scope::Line
&& ((query.action == Action::Append && query.positional == Positional::After)
|| (query.action == Action::Prepend && query.positional == Positional::Before));
if is_interactive_newline {
let indent: String = buffer
.lines
.get(scope_range.start_line)
.map(|l| l.chars().take_while(|c| c.is_whitespace()).collect())
.unwrap_or_default();
if !indent.is_empty() {
replacement = Some(indent);
}
}
let primary = target_ranges.first().copied().unwrap_or(TextRange::point(
scope_range.start_line,
scope_range.start_col,
));
let (mut cursor_destination, mode_after) = resolve_cursor_and_mode(query, &primary);
if is_interactive_cifc && let Some(ref repl) = replacement {
cursor_destination = Some(CursorPosition {
line: primary.start_line,
col: repl.len(),
});
}
if is_interactive_newline
&& let Some(ref repl) = replacement
&& let Some(ref cursor) = cursor_destination
{
cursor_destination = Some(CursorPosition {
line: cursor.line,
col: repl.len(),
});
}
Ok(BufferResolution {
target_ranges,
scope_range,
component_range,
replacement,
cursor_destination,
mode_after,
listed_items: vec![],
warnings: vec![],
})
}
fn resolve_count_positional(
query: &ChordQuery,
buffer_name: &str,
buffer: &Buffer,
lsp: &mut LspEngine,
n: u8,
) -> Result<BufferResolution> {
let (cursor_line, cursor_col) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"numeric positional requires a cursor position; pass cursor:\"line,col\"",
)
})?;
let mut warnings: Vec<String> = Vec::new();
let (target_ranges, scope_range) = match query.scope {
Scope::Line => resolve_count_line(
query,
buffer,
buffer_name,
lsp,
n,
cursor_line,
cursor_col,
&mut warnings,
)?,
Scope::Function | Scope::Variable | Scope::Struct | Scope::Member => resolve_count_lsp(
query,
buffer_name,
lsp,
n,
cursor_line,
cursor_col,
&mut warnings,
)?,
Scope::Buffer | Scope::Delimiter => {
unreachable!("Count positional with Buffer/Delimiter rejected at parse time")
}
};
let replacement = query.args.value.clone();
let primary = target_ranges
.first()
.copied()
.unwrap_or(TextRange::point(cursor_line, cursor_col));
let (cursor_destination, mode_after) = resolve_cursor_and_mode(query, &primary);
Ok(BufferResolution {
target_ranges: target_ranges.clone(),
scope_range,
component_range: target_ranges
.first()
.copied()
.unwrap_or(TextRange::point(cursor_line, cursor_col)),
replacement,
cursor_destination,
mode_after,
listed_items: vec![],
warnings,
})
}
#[allow(clippy::too_many_arguments)]
fn resolve_count_line(
query: &ChordQuery,
buffer: &Buffer,
buffer_name: &str,
lsp: &mut LspEngine,
n: u8,
cursor_line: usize,
cursor_col: usize,
warnings: &mut Vec<String>,
) -> Result<(Vec<TextRange>, TextRange)> {
match query.component {
Component::Self_ => {
let available = buffer.line_count().saturating_sub(cursor_line);
let actual = (n as usize).min(available);
if actual < n as usize {
warnings.push(format!(
"only {} of {} requested occurrences found",
actual, n
));
}
let end_line = cursor_line + actual.saturating_sub(1);
let end_col = buffer
.lines
.get(end_line)
.map(|l| line_char_count(l))
.unwrap_or(0);
let range = TextRange {
start_line: cursor_line,
start_col: 0,
end_line,
end_col,
};
Ok((vec![range], range))
}
Component::Word => {
let line_idx = cursor_line.min(buffer.line_count().saturating_sub(1));
let line = buffer.lines.get(line_idx).map(|l| l.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
let words = collect_words_in_range(&chars, 0, chars.len());
let current_word_end = find_current_word_end(&words, cursor_col);
let search_after = current_word_end.unwrap_or(cursor_col);
let next_words: Vec<(usize, usize)> = words
.iter()
.filter(|(s, _)| *s >= search_after && *s > cursor_col)
.copied()
.collect();
let actual = next_words.len().min(n as usize);
if actual < n as usize {
warnings.push(format!(
"only {} of {} requested occurrences found",
actual, n
));
}
let target = if actual == 0 {
let end_col = chars.len();
TextRange {
start_line: line_idx,
start_col: end_col,
end_line: line_idx,
end_col,
}
} else {
let (_, end) = next_words[actual - 1];
TextRange {
start_line: line_idx,
start_col: cursor_col,
end_line: line_idx,
end_col: end,
}
};
let scope_range = TextRange {
start_line: line_idx,
start_col: 0,
end_line: line_idx,
end_col: chars.len(),
};
Ok((vec![target], scope_range))
}
_ => {
let scope_range = resolve_line_scope_at(buffer, cursor_line, buffer_name)?;
let component_range = resolve_component(query, buffer, &scope_range, buffer_name, lsp)?;
Ok((vec![component_range], scope_range))
}
}
}
fn resolve_line_scope_at(buffer: &Buffer, line: usize, buffer_name: &str) -> Result<TextRange> {
if buffer.line_count() == 0 {
return Err(
ChordError::resolve(buffer_name, "cannot resolve line scope on empty buffer").into(),
);
}
let line = line.min(buffer.line_count() - 1);
let line_len = line_char_count(&buffer.lines[line]);
Ok(TextRange {
start_line: line,
start_col: 0,
end_line: line,
end_col: line_len,
})
}
fn resolve_count_lsp(
query: &ChordQuery,
buffer_name: &str,
lsp: &mut LspEngine,
n: u8,
cursor_line: usize,
cursor_col: usize,
warnings: &mut Vec<String>,
) -> Result<(Vec<TextRange>, TextRange)> {
let path = Path::new(buffer_name);
let symbols = lsp.document_symbols(path).map_err(|e| {
ChordError::resolve(
buffer_name,
format!("LSP not ready: {e}; LSP-scoped chords need an active language server"),
)
})?;
let target_kinds = scope_to_symbol_kinds(query.scope);
let mut flat: Vec<&DocumentSymbol> = Vec::new();
if target_kinds.is_empty() {
flatten_all_symbols(&symbols, &mut flat);
} else {
flatten_by_kind(&symbols, &target_kinds, &mut flat);
}
flat.sort_by(|a, b| {
a.range
.start_line
.cmp(&b.range.start_line)
.then(a.range.start_col.cmp(&b.range.start_col))
});
let after_cursor: Vec<&DocumentSymbol> = flat
.into_iter()
.filter(|s| {
s.range.start_line > cursor_line
|| (s.range.start_line == cursor_line && s.range.start_col > cursor_col)
})
.collect();
let actual = after_cursor.len().min(n as usize);
if actual == 0 {
return Err(ChordError::resolve(
buffer_name,
format!(
"no {} symbol found after cursor ({cursor_line}, {cursor_col})",
query.scope.to_string().to_lowercase()
),
)
.into());
}
if actual < n as usize {
warnings.push(format!(
"only {} of {} requested occurrences found",
actual, n
));
}
let selected: Vec<&DocumentSymbol> = after_cursor.into_iter().take(actual).collect();
let first = &selected[0];
let last = &selected[actual - 1];
let scope_range = TextRange {
start_line: first.range.start_line,
start_col: first.range.start_col,
end_line: last.range.end_line,
end_col: last.range.end_col,
};
let target_ranges = vec![scope_range];
Ok((target_ranges, scope_range))
}
fn resolve_scope(
query: &ChordQuery,
buffer_name: &str,
buffer: &Buffer,
lsp: &mut LspEngine,
) -> Result<TextRange> {
match query.scope {
Scope::Line => resolve_line_scope(query, buffer, buffer_name),
Scope::Buffer => resolve_buffer_scope(buffer),
Scope::Function => resolve_lsp_scope(
query,
buffer_name,
buffer,
lsp,
&[SymbolKind::Function, SymbolKind::Method],
),
Scope::Variable => resolve_variable_scope(query, buffer_name, buffer, lsp),
Scope::Struct => resolve_lsp_scope(
query,
buffer_name,
buffer,
lsp,
&[SymbolKind::Struct, SymbolKind::Enum],
),
Scope::Member => resolve_member_scope(query, buffer_name, buffer, lsp),
Scope::Delimiter => resolve_delimiter_scope(query, buffer, buffer_name),
}
}
fn resolve_line_scope(query: &ChordQuery, buffer: &Buffer, buffer_name: &str) -> Result<TextRange> {
let base_line = match query
.args
.target_line
.or(query.args.cursor_pos.map(|(l, _)| l))
{
Some(l) => l,
None => {
return Err(ChordError::resolve(
buffer_name,
"line scope requires either 'target:' arg or a cursor position",
)
.into());
}
};
let line = match query.positional {
Positional::Next => {
let next = base_line + 1;
if next >= buffer.line_count() {
return Err(ChordError::resolve(
buffer_name,
format!(
"no next line: cursor is on line {base_line} (file has {} lines)",
buffer.line_count()
),
)
.into());
}
next
}
Positional::Previous => {
if base_line == 0 {
return Err(ChordError::resolve(
buffer_name,
"no previous line: cursor is already on line 0",
)
.into());
}
base_line - 1
}
_ => base_line,
};
if buffer.line_count() == 0 {
return Err(
ChordError::resolve(buffer_name, "cannot resolve line scope on empty buffer").into(),
);
}
if line >= buffer.line_count() {
return Err(ChordError::resolve(
buffer_name,
format!(
"line {line} out of range (file has {} lines)",
buffer.line_count()
),
)
.into());
}
let line_len = line_char_count(&buffer.lines[line]);
Ok(TextRange {
start_line: line,
start_col: 0,
end_line: line,
end_col: line_len,
})
}
fn resolve_buffer_scope(buffer: &Buffer) -> Result<TextRange> {
let last_line = buffer.line_count().saturating_sub(1);
let last_col = buffer
.lines
.get(last_line)
.map(|l| line_char_count(l))
.unwrap_or(0);
Ok(TextRange {
start_line: 0,
start_col: 0,
end_line: last_line,
end_col: last_col,
})
}
fn resolve_lsp_scope(
query: &ChordQuery,
buffer_name: &str,
_buffer: &Buffer,
lsp: &mut LspEngine,
target_kinds: &[SymbolKind],
) -> Result<TextRange> {
let path = Path::new(buffer_name);
let symbols = lsp.document_symbols(path).map_err(|e| {
ChordError::resolve(
buffer_name,
format!("LSP not ready: {e}; LSP-scoped chords need an active language server"),
)
})?;
if let Some(ref name) = query.args.target_name {
if let Some(sym) = find_symbol_by_name_and_kind(&symbols, name, target_kinds) {
return Ok(symbol_to_range(&sym.range));
}
let available: Vec<String> = collect_symbols_by_kind(&symbols, target_kinds);
return Err(ChordError::resolve_with_symbols(
buffer_name,
format!("symbol '{name}' not found"),
available,
)
.into());
}
if matches!(query.positional, Positional::First | Positional::Last) {
let mut flat: Vec<&DocumentSymbol> = Vec::new();
flatten_by_kind(&symbols, target_kinds, &mut flat);
flat.sort_by(|a, b| {
a.range
.start_line
.cmp(&b.range.start_line)
.then(a.range.start_col.cmp(&b.range.start_col))
});
let sym = if query.positional == Positional::First {
flat.first()
} else {
flat.last()
};
if let Some(s) = sym {
return Ok(symbol_to_range(&s.range));
}
return Err(ChordError::resolve(
buffer_name,
format!(
"no {} found in buffer",
query.scope.to_string().to_lowercase()
),
)
.into());
}
if let Some((line, col)) = query.args.cursor_pos {
if matches!(query.positional, Positional::Next | Positional::Previous) {
if let Some(sym) =
find_neighbor_symbol(&symbols, line, col, target_kinds, query.positional)
{
return Ok(symbol_to_range(&sym.range));
}
return Err(ChordError::resolve(
buffer_name,
format!(
"no {} symbol found from cursor ({line}, {col})",
if query.positional == Positional::Next {
"next"
} else {
"previous"
}
),
)
.into());
}
if let Some(sym) = find_symbol_at_position_by_kind(&symbols, line, col, target_kinds) {
return Ok(symbol_to_range(&sym.range));
}
if matches!(query.scope, Scope::Variable)
&& let Some(sym) = find_symbol_on_line_by_kind(&symbols, line, target_kinds)
{
return Ok(symbol_to_range(&sym.range));
}
return Err(ChordError::resolve(
buffer_name,
format!("no matching symbol at cursor position ({line}, {col})"),
)
.into());
}
Err(ChordError::resolve(
buffer_name,
"LSP scope requires either a target name or cursor position",
)
.into())
}
fn resolve_member_scope(
query: &ChordQuery,
buffer_name: &str,
_buffer: &Buffer,
lsp: &mut LspEngine,
) -> Result<TextRange> {
let path = Path::new(buffer_name);
let symbols = lsp.document_symbols(path).map_err(|e| {
ChordError::resolve(
buffer_name,
format!("LSP not ready: {e}; member-scoped chords need an active language server"),
)
})?;
let parent_kinds = &[SymbolKind::Struct, SymbolKind::Enum];
if let Some(ref name) = query.args.target_name {
if let Some(parent_name) = query.args.parent_name.as_deref() {
if let Some(parent) = find_symbol_by_name_and_kind(&symbols, parent_name, parent_kinds)
{
if let Some(child) = parent.children.iter().find(|c| c.name == *name) {
return Ok(symbol_to_range(&child.range));
}
let available: Vec<String> =
parent.children.iter().map(|c| c.name.clone()).collect();
return Err(ChordError::resolve_with_symbols(
buffer_name,
format!("member '{name}' not found in '{parent_name}'"),
available,
)
.into());
}
return Err(ChordError::resolve(
buffer_name,
format!("parent struct/enum '{parent_name}' not found"),
)
.into());
}
let mut matches: Vec<(&DocumentSymbol, &DocumentSymbol)> = Vec::new();
collect_member_matches(&symbols, name, parent_kinds, &mut matches);
match matches.len() {
0 => {
return Err(ChordError::resolve(
buffer_name,
format!("member '{name}' not found in any struct or enum"),
)
.into());
}
1 => return Ok(symbol_to_range(&matches[0].1.range)),
_ => {
let parents: Vec<String> = matches.iter().map(|(p, _)| p.name.clone()).collect();
return Err(ChordError::resolve_with_symbols(
buffer_name,
format!(
"member '{name}' is ambiguous (defined in {}); pass parent:<name> to disambiguate",
parents.join(", ")
),
parents,
)
.into());
}
}
}
if matches!(query.positional, Positional::First | Positional::Last) {
let (line, col) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"First/Last member requires a cursor position to identify the parent struct",
)
})?;
if let Some(parent) = find_parent_struct_at_cursor(&symbols, line, col, parent_kinds) {
if parent.children.is_empty() {
return Err(ChordError::resolve(
buffer_name,
"no members found in enclosing struct/enum",
)
.into());
}
let member = if query.positional == Positional::First {
&parent.children[0]
} else {
&parent.children[parent.children.len() - 1]
};
return Ok(symbol_to_range(&member.range));
}
return Err(ChordError::resolve(
buffer_name,
"no enclosing struct/enum found at cursor position",
)
.into());
}
if let Some((line, col)) = query.args.cursor_pos {
if let Some(member) = find_member_at_cursor(&symbols, line, col, parent_kinds) {
return Ok(symbol_to_range(&member.range));
}
return Err(ChordError::resolve(
buffer_name,
format!("no member found at cursor position ({line}, {col})"),
)
.into());
}
Err(ChordError::resolve(
buffer_name,
"member scope requires either a target name or cursor position",
)
.into())
}
fn resolve_variable_scope(
query: &ChordQuery,
buffer_name: &str,
buffer: &Buffer,
lsp: &mut LspEngine,
) -> Result<TextRange> {
let target_kinds = &[SymbolKind::Variable, SymbolKind::Const];
let path = Path::new(buffer_name);
if query.args.target_name.is_some()
|| matches!(query.positional, Positional::First | Positional::Last)
{
return resolve_lsp_scope(query, buffer_name, buffer, lsp, target_kinds);
}
let (line, col) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"variable scope requires a cursor position or target name",
)
})?;
if let Ok(symbols) = lsp.document_symbols(path) {
if matches!(query.positional, Positional::Next | Positional::Previous) {
if let Some(sym) =
find_neighbor_symbol(&symbols, line, col, target_kinds, query.positional)
{
return Ok(symbol_to_range(&sym.range));
}
return Err(ChordError::resolve(
buffer_name,
format!(
"no {} variable found from cursor ({line}, {col})",
if query.positional == Positional::Next {
"next"
} else {
"previous"
}
),
)
.into());
}
let var_pos = find_symbol_at_position_by_kind(&symbols, line, col, target_kinds)
.or_else(|| find_symbol_on_line_by_kind(&symbols, line, target_kinds))
.map(|sym| (sym.range.start_line, sym.range.start_col));
if let Some((var_line, var_col)) = var_pos
&& let Ok(sel) = lsp.selection_range(path, var_line, var_col)
&& let Some(range) = find_enclosing_declaration(&sel)
{
return Ok(range);
}
}
resolve_variable_scope_via_selection_range(query, buffer_name, buffer, lsp)
}
fn find_enclosing_declaration(sel: &crate::data::lsp::types::SelectionRange) -> Option<TextRange> {
let reference =
if sel.range.start_line == sel.range.end_line && sel.range.start_col == sel.range.end_col {
sel.parent.as_ref()?
} else {
sel
};
let inner = &reference.range;
let mut current = reference;
while let Some(ref parent) = current.parent {
let r = &parent.range;
let wider = (r.start_line < inner.start_line
|| (r.start_line == inner.start_line && r.start_col < inner.start_col))
|| (r.end_line > inner.end_line
|| (r.end_line == inner.end_line && r.end_col > inner.end_col));
if wider {
return Some(symbol_to_range(r));
}
current = parent;
}
None
}
fn resolve_variable_scope_via_selection_range(
query: &ChordQuery,
buffer_name: &str,
buffer: &Buffer,
lsp: &mut LspEngine,
) -> Result<TextRange> {
let (line, col) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"variable scope requires a cursor position or target name",
)
})?;
let path = Path::new(buffer_name);
let sel = lsp
.selection_range(path, line, col)
.map_err(|e| ChordError::resolve(buffer_name, format!("LSP selectionRange failed: {e}")))?;
let mut current = &sel;
loop {
let range = symbol_to_range(¤t.range);
if has_interior_assignment(buffer, &range) {
return Ok(range);
}
match current.parent {
Some(ref parent) => current = parent,
None => break,
}
}
Err(ChordError::resolve(
buffer_name,
format!("no enclosing variable declaration found at cursor ({line}, {col})"),
)
.into())
}
fn has_interior_assignment(buffer: &Buffer, range: &TextRange) -> bool {
let text = extract_range_text(buffer, range);
let chars: Vec<char> = text.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c == '=' && i > 0 && i < chars.len() - 1 {
let prev = chars[i - 1];
let next = chars[i + 1];
let is_compound = matches!(
prev,
'!' | '<' | '>' | '=' | '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^'
) || next == '='
|| next == '>';
if !is_compound {
return true;
}
}
}
false
}
fn resolve_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
lsp: &mut LspEngine,
) -> Result<TextRange> {
match query.component {
Component::Beginning => Ok(TextRange::point(
scope_range.start_line,
scope_range.start_col,
)),
Component::Contents => resolve_contents_component(query, buffer, scope_range, buffer_name),
Component::End => Ok(TextRange::point(scope_range.end_line, scope_range.end_col)),
Component::Self_ => Ok(*scope_range),
Component::Name => resolve_name_component(query, buffer, buffer_name, lsp, scope_range),
Component::Value => resolve_value_component(query, buffer, scope_range, buffer_name),
Component::Parameters => resolve_parameters_component(buffer, scope_range, buffer_name),
Component::Arguments => {
resolve_arguments_component(query, buffer, scope_range, buffer_name)
}
Component::Word => resolve_word_component(query, buffer, scope_range, buffer_name),
Component::Definition => {
resolve_definition_component(query, buffer, scope_range, buffer_name)
}
}
}
fn resolve_name_component(
query: &ChordQuery,
buffer: &Buffer,
buffer_name: &str,
lsp: &mut LspEngine,
scope_range: &TextRange,
) -> Result<TextRange> {
if query.scope == Scope::Line || query.scope == Scope::Buffer {
return Ok(TextRange::point(
scope_range.start_line,
scope_range.start_col,
));
}
if query.scope == Scope::Delimiter {
return Ok(TextRange {
start_line: scope_range.start_line,
start_col: scope_range.start_col,
end_line: scope_range.start_line,
end_col: scope_range.start_col + 1,
});
}
let path = Path::new(buffer_name);
let symbols = lsp.document_symbols(path).map_err(|e| {
ChordError::resolve(
buffer_name,
format!("LSP not ready: {e}; cannot resolve Name component"),
)
})?;
if let Some(ref name) = query.args.target_name {
if let Some(sym) = find_symbol_by_name_recursive(&symbols, name) {
return Ok(symbol_name_range(sym));
}
return Err(ChordError::resolve(
buffer_name,
format!("symbol '{name}' not found for Name component"),
)
.into());
}
if matches!(query.positional, Positional::First | Positional::Last) {
let target_kinds = scope_to_symbol_kinds(query.scope);
if !target_kinds.is_empty()
&& let Some(sym) = find_symbol_in_range(&symbols, scope_range, &target_kinds)
{
return Ok(symbol_name_range(sym));
}
return Err(ChordError::resolve(
buffer_name,
format!(
"no {} found in scope range",
query.scope.to_string().to_lowercase()
),
)
.into());
}
if let Some((line, col)) = query.args.cursor_pos {
let target_kinds = scope_to_symbol_kinds(query.scope);
if !target_kinds.is_empty()
&& let Some(sym) = find_symbol_in_range(&symbols, scope_range, &target_kinds)
{
return Ok(symbol_name_range(sym));
}
if let Some(sym) = find_innermost_symbol(&symbols, line, col)
&& (target_kinds.is_empty() || matches_kind(&sym.kind, &target_kinds))
{
return Ok(symbol_name_range(sym));
}
if query.scope == Scope::Variable
&& let Some(range) = extract_variable_name_from_text(buffer, scope_range)
{
return Ok(range);
}
return Err(ChordError::resolve(
buffer_name,
format!("no matching symbol at cursor ({line}, {col}) for Name component"),
)
.into());
}
Err(ChordError::resolve(
buffer_name,
"Name component requires either a target name or cursor position",
)
.into())
}
fn scope_to_symbol_kinds(scope: Scope) -> Vec<SymbolKind> {
match scope {
Scope::Function => vec![SymbolKind::Function, SymbolKind::Method],
Scope::Variable => vec![SymbolKind::Variable, SymbolKind::Const],
Scope::Struct => vec![SymbolKind::Struct, SymbolKind::Enum],
Scope::Member | Scope::Line | Scope::Buffer | Scope::Delimiter => vec![],
}
}
fn find_symbol_in_range<'a>(
symbols: &'a [DocumentSymbol],
range: &TextRange,
kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if matches_kind(&sym.kind, kinds) && symbol_within_range(&sym.range, range) {
return Some(sym);
}
if let Some(found) = find_symbol_in_range(&sym.children, range, kinds) {
return Some(found);
}
}
None
}
fn symbol_within_range(
sym_range: &crate::data::lsp::types::SymbolRange,
outer: &TextRange,
) -> bool {
let after_start = sym_range.start_line > outer.start_line
|| (sym_range.start_line == outer.start_line && sym_range.start_col >= outer.start_col);
let before_end = sym_range.end_line < outer.end_line
|| (sym_range.end_line == outer.end_line && sym_range.end_col <= outer.end_col);
after_start && before_end
}
fn extract_variable_name_from_text(buffer: &Buffer, scope_range: &TextRange) -> Option<TextRange> {
let text = extract_range_text(buffer, scope_range);
let keywords = ["let", "const", "static", "mut"];
let mut pos = 0;
let chars: Vec<char> = text.chars().collect();
loop {
while pos < chars.len() && !chars[pos].is_alphanumeric() && chars[pos] != '_' {
pos += 1;
}
if pos >= chars.len() {
return None;
}
let start = pos;
while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
pos += 1;
}
let word: String = chars[start..pos].iter().collect();
if !keywords.contains(&word.as_str()) {
let abs_col = if scope_range.start_line == scope_range.end_line {
scope_range.start_col + start
} else {
let newlines_before = text
[..chars[..start].iter().map(|c| c.len_utf8()).sum::<usize>()]
.matches('\n')
.count();
if newlines_before == 0 {
scope_range.start_col + start
} else {
start
- text[..chars[..start].iter().map(|c| c.len_utf8()).sum::<usize>()]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0)
}
};
return Some(TextRange {
start_line: scope_range.start_line,
start_col: abs_col,
end_line: scope_range.start_line,
end_col: abs_col + (pos - start),
});
}
}
}
fn resolve_value_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
match query.scope {
Scope::Variable => find_assignment_rhs(buffer, scope_range, buffer_name),
Scope::Member => find_member_value(buffer, scope_range, buffer_name),
_ => Err(ChordError::resolve(
buffer_name,
format!("Value component is not valid for {} scope", query.scope),
)
.into()),
}
}
fn resolve_parameters_component(
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
find_paren_range(buffer, scope_range, buffer_name)
}
fn resolve_arguments_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let name = query.args.target_name.clone().ok_or_else(|| {
ChordError::resolve(
buffer_name,
"Arguments component requires a target name to locate a call expression",
)
})?;
let content = buffer.content();
let needle = format!("{name}(");
let mut search_from = 0;
let last_line = buffer.line_count().saturating_sub(1);
let buffer_end = TextRange {
start_line: 0,
start_col: 0,
end_line: last_line,
end_col: buffer
.lines
.get(last_line)
.map(|l| line_char_count(l))
.unwrap_or(0),
};
while let Some(rel) = content[search_from..].find(&needle) {
let byte_pos = search_from + rel;
let paren_byte = byte_pos + needle.len() - 1;
let prefix = &content[..paren_byte];
let line = prefix.matches('\n').count();
let line_start = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = content[line_start..paren_byte].chars().count();
let in_scope = (line > scope_range.start_line
|| (line == scope_range.start_line && col >= scope_range.start_col))
&& (line < scope_range.end_line
|| (line == scope_range.end_line && col <= scope_range.end_col));
if in_scope {
search_from = byte_pos + needle.len();
continue;
}
let signature_scope = TextRange {
start_line: line,
start_col: col,
end_line: buffer_end.end_line,
end_col: buffer_end.end_col,
};
return find_paren_range(buffer, &signature_scope, buffer_name);
}
Err(ChordError::resolve(
buffer_name,
format!("no call site for '{name}' found in buffer"),
)
.into())
}
fn apply_positional(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
component_range: &TextRange,
buffer_name: &str,
) -> Result<Vec<TextRange>> {
match query.positional {
Positional::Inside => {
if component_range.is_empty() || query.component == Component::Contents {
Ok(vec![*component_range])
} else {
Ok(vec![shrink_range(buffer, component_range)])
}
}
Positional::Entire => Ok(vec![*component_range]),
Positional::After => {
if query.component == Component::Self_ {
return Ok(vec![TextRange::point(
component_range.end_line,
component_range.end_col,
)]);
}
Ok(vec![TextRange {
start_line: component_range.end_line,
start_col: component_range.end_col,
end_line: scope_range.end_line,
end_col: scope_range.end_col,
}])
}
Positional::Before => {
if query.component == Component::Self_ {
return Ok(vec![TextRange::point(
component_range.start_line,
component_range.start_col,
)]);
}
Ok(vec![TextRange {
start_line: scope_range.start_line,
start_col: scope_range.start_col,
end_line: component_range.start_line,
end_col: component_range.start_col,
}])
}
Positional::Until => {
let cursor = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(buffer_name, "'Until' positional requires a cursor position")
})?;
Ok(vec![TextRange {
start_line: cursor.0,
start_col: cursor.1,
end_line: component_range.start_line,
end_col: component_range.start_col,
}])
}
Positional::To => {
let cursor = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(buffer_name, "'To' positional requires a cursor position")
})?;
Ok(vec![TextRange {
start_line: cursor.0,
start_col: cursor.1,
end_line: component_range.end_line,
end_col: component_range.end_col,
}])
}
Positional::Outside => Ok(outside_ranges(scope_range, component_range)),
Positional::Next | Positional::Previous => {
if matches!(query.scope, Scope::Line) && query.args.cursor_pos.is_none() {
return Err(ChordError::resolve(
buffer_name,
format!(
"'{}' positional on Line scope requires a cursor position",
if query.positional == Positional::Next {
"Next"
} else {
"Previous"
}
),
)
.into());
}
Ok(vec![*component_range])
}
Positional::Last | Positional::First => Ok(vec![*component_range]),
Positional::Count(_) => Ok(vec![*component_range]),
}
}
fn position_before_scope(buffer: &Buffer, scope: &TextRange) -> TextRange {
if scope.start_line == 0 {
return TextRange::point(0, 0);
}
let prev = scope.start_line - 1;
let col = buffer
.lines
.get(prev)
.map(|l| line_char_count(l))
.unwrap_or(0);
TextRange::point(prev, col)
}
fn position_after_scope(buffer: &Buffer, scope: &TextRange) -> TextRange {
let last = buffer.line_count().saturating_sub(1);
if scope.end_line >= last {
return TextRange::point(scope.end_line, scope.end_col);
}
TextRange::point(scope.end_line + 1, 0)
}
fn outside_ranges(scope: &TextRange, component: &TextRange) -> Vec<TextRange> {
let mut out = Vec::new();
let head = TextRange {
start_line: scope.start_line,
start_col: scope.start_col,
end_line: component.start_line,
end_col: component.start_col,
};
if !head.is_empty() {
out.push(head);
}
let tail = TextRange {
start_line: component.end_line,
start_col: component.end_col,
end_line: scope.end_line,
end_col: scope.end_col,
};
if !tail.is_empty() {
out.push(tail);
}
if out.is_empty() {
out.push(TextRange::point(scope.start_line, scope.start_col));
}
out
}
fn resolve_cursor_and_mode(
query: &ChordQuery,
target_range: &TextRange,
) -> (Option<CursorPosition>, Option<EditorMode>) {
match query.action {
Action::Change => {
let cursor = CursorPosition {
line: target_range.start_line,
col: target_range.start_col,
};
if query.args.value.is_some() {
(Some(cursor), Some(EditorMode::Chord))
} else {
(Some(cursor), Some(EditorMode::Edit))
}
}
Action::Delete => {
let cursor = CursorPosition {
line: target_range.start_line,
col: target_range.start_col,
};
(Some(cursor), Some(EditorMode::Chord))
}
Action::Append | Action::Prepend | Action::Insert => {
let new_line_insert = query.action == Action::Append
&& query.positional == Positional::After
&& query.scope == Scope::Line;
let cursor = if new_line_insert {
CursorPosition {
line: target_range.start_line + 1,
col: 0,
}
} else if query.action == Action::Append {
CursorPosition {
line: target_range.end_line,
col: target_range.end_col,
}
} else {
CursorPosition {
line: target_range.start_line,
col: target_range.start_col,
}
};
if query.args.value.is_some() {
(Some(cursor), Some(EditorMode::Chord))
} else {
(Some(cursor), Some(EditorMode::Edit))
}
}
Action::Replace => {
let cursor = CursorPosition {
line: target_range.start_line,
col: target_range.start_col,
};
(Some(cursor), Some(EditorMode::Chord))
}
Action::Yank | Action::List => (None, None),
Action::Jump => {
let cursor = match query.positional {
Positional::To | Positional::Until | Positional::Before | Positional::Count(_) => {
CursorPosition {
line: target_range.end_line,
col: target_range.end_col,
}
}
_ => CursorPosition {
line: target_range.start_line,
col: target_range.start_col,
},
};
(Some(cursor), Some(EditorMode::Chord))
}
}
}
fn resolve_contents_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
match query.scope {
Scope::Function | Scope::Struct => find_brace_range(buffer, scope_range, buffer_name),
Scope::Delimiter => {
let end_col = if scope_range.end_col > 0 {
scope_range.end_col - 1
} else {
0
};
Ok(TextRange {
start_line: scope_range.start_line,
start_col: scope_range.start_col + 1,
end_line: scope_range.end_line,
end_col,
})
}
_ => Err(ChordError::resolve(
buffer_name,
format!("Contents component is not valid for {} scope", query.scope),
)
.into()),
}
}
fn symbol_name_range(sym: &DocumentSymbol) -> TextRange {
if let Some(ref sr) = sym.selection_range {
symbol_to_range(sr)
} else {
symbol_to_range(&sym.range)
}
}
fn symbol_to_range(sr: &crate::data::lsp::types::SymbolRange) -> TextRange {
TextRange {
start_line: sr.start_line,
start_col: sr.start_col,
end_line: sr.end_line,
end_col: sr.end_col,
}
}
fn contains_position(
range: &crate::data::lsp::types::SymbolRange,
line: usize,
col: usize,
) -> bool {
if line < range.start_line || line > range.end_line {
return false;
}
if line == range.start_line && col < range.start_col {
return false;
}
if line == range.end_line && col > range.end_col {
return false;
}
true
}
fn matches_kind(kind: &SymbolKind, targets: &[SymbolKind]) -> bool {
targets.contains(kind)
}
fn find_symbol_by_name_and_kind<'a>(
symbols: &'a [DocumentSymbol],
name: &str,
kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if sym.name == name && matches_kind(&sym.kind, kinds) {
return Some(sym);
}
if let Some(found) = find_symbol_by_name_and_kind(&sym.children, name, kinds) {
return Some(found);
}
}
None
}
fn find_symbol_by_name_recursive<'a>(
symbols: &'a [DocumentSymbol],
name: &str,
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if sym.name == name {
return Some(sym);
}
if let Some(found) = find_symbol_by_name_recursive(&sym.children, name) {
return Some(found);
}
}
None
}
fn find_symbol_at_position_by_kind<'a>(
symbols: &'a [DocumentSymbol],
line: usize,
col: usize,
kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
let mut best: Option<&'a DocumentSymbol> = None;
for sym in symbols {
if contains_position(&sym.range, line, col) {
if matches_kind(&sym.kind, kinds) {
best = Some(sym);
}
if let Some(child) = find_symbol_at_position_by_kind(&sym.children, line, col, kinds) {
best = Some(child);
}
}
}
best
}
fn find_symbol_on_line_by_kind<'a>(
symbols: &'a [DocumentSymbol],
line: usize,
kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if matches_kind(&sym.kind, kinds) && sym.range.start_line == line {
return Some(sym);
}
if let Some(found) = find_symbol_on_line_by_kind(&sym.children, line, kinds) {
return Some(found);
}
}
None
}
fn find_innermost_symbol(
symbols: &[DocumentSymbol],
line: usize,
col: usize,
) -> Option<&DocumentSymbol> {
for sym in symbols {
if contains_position(&sym.range, line, col) {
if let Some(child) = find_innermost_symbol(&sym.children, line, col) {
return Some(child);
}
return Some(sym);
}
}
None
}
fn find_neighbor_symbol<'a>(
symbols: &'a [DocumentSymbol],
line: usize,
col: usize,
kinds: &[SymbolKind],
positional: Positional,
) -> Option<&'a DocumentSymbol> {
let mut flat: Vec<&'a DocumentSymbol> = Vec::new();
flatten_by_kind(symbols, kinds, &mut flat);
flat.sort_by(|a, b| {
a.range
.start_line
.cmp(&b.range.start_line)
.then(a.range.start_col.cmp(&b.range.start_col))
});
match positional {
Positional::Next => flat.into_iter().find(|s| {
s.range.start_line > line || (s.range.start_line == line && s.range.start_col > col)
}),
Positional::Previous => flat.into_iter().rev().find(|s| {
s.range.end_line < line || (s.range.end_line == line && s.range.end_col < col)
}),
_ => None,
}
}
fn flatten_by_kind<'a>(
symbols: &'a [DocumentSymbol],
kinds: &[SymbolKind],
out: &mut Vec<&'a DocumentSymbol>,
) {
for sym in symbols {
if matches_kind(&sym.kind, kinds) {
out.push(sym);
}
flatten_by_kind(&sym.children, kinds, out);
}
}
fn collect_member_matches<'a>(
symbols: &'a [DocumentSymbol],
name: &str,
parent_kinds: &[SymbolKind],
out: &mut Vec<(&'a DocumentSymbol, &'a DocumentSymbol)>,
) {
for sym in symbols {
if matches_kind(&sym.kind, parent_kinds) {
for child in &sym.children {
if child.name == name {
out.push((sym, child));
}
}
}
collect_member_matches(&sym.children, name, parent_kinds, out);
}
}
fn find_member_at_cursor<'a>(
symbols: &'a [DocumentSymbol],
line: usize,
col: usize,
parent_kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if matches_kind(&sym.kind, parent_kinds) && contains_position(&sym.range, line, col) {
for child in &sym.children {
if contains_position(&child.range, line, col) {
return Some(child);
}
}
}
if let Some(deeper) = find_member_at_cursor(&sym.children, line, col, parent_kinds) {
return Some(deeper);
}
}
None
}
fn find_parent_struct_at_cursor<'a>(
symbols: &'a [DocumentSymbol],
line: usize,
col: usize,
parent_kinds: &[SymbolKind],
) -> Option<&'a DocumentSymbol> {
for sym in symbols {
if matches_kind(&sym.kind, parent_kinds) && contains_position(&sym.range, line, col) {
if let Some(deeper) =
find_parent_struct_at_cursor(&sym.children, line, col, parent_kinds)
{
return Some(deeper);
}
return Some(sym);
}
if let Some(deeper) = find_parent_struct_at_cursor(&sym.children, line, col, parent_kinds) {
return Some(deeper);
}
}
None
}
fn collect_symbols_by_kind(symbols: &[DocumentSymbol], kinds: &[SymbolKind]) -> Vec<String> {
let mut result = Vec::new();
for sym in symbols {
if matches_kind(&sym.kind, kinds) {
result.push(sym.name.clone());
}
result.extend(collect_symbols_by_kind(&sym.children, kinds));
}
result
}
fn find_brace_range(
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let mut range = scan_balanced(buffer, scope_range, '{', '}')
.ok_or_else(|| ChordError::resolve(buffer_name, "no brace block found in scope"))?;
range.start_col += 1;
range.end_col = range.end_col.saturating_sub(1);
let start_line_rest_is_blank = buffer
.lines
.get(range.start_line)
.map(|l| l.chars().skip(range.start_col).all(|c| c.is_whitespace()))
.unwrap_or(true);
if start_line_rest_is_blank && range.start_line < range.end_line {
range.start_line += 1;
range.start_col = 0;
}
let end_line_prefix_is_blank = buffer
.lines
.get(range.end_line)
.map(|l| l.chars().take(range.end_col).all(|c| c.is_whitespace()))
.unwrap_or(true);
if end_line_prefix_is_blank && range.end_line > range.start_line {
range.end_line -= 1;
range.end_col = buffer
.lines
.get(range.end_line)
.map(|l| line_char_count(l))
.unwrap_or(0);
}
Ok(range)
}
fn find_paren_range(
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
if let Some(range) = scan_balanced(buffer, scope_range, '(', ')') {
Ok(range)
} else {
Err(ChordError::resolve(buffer_name, "no parenthesized list found in scope").into())
}
}
fn scan_balanced(
buffer: &Buffer,
scope_range: &TextRange,
open: char,
close: char,
) -> Option<TextRange> {
let last = buffer.line_count().saturating_sub(1);
let start_line = scope_range.start_line.min(last);
let end_line = scope_range.end_line.min(last);
let mut depth = 0i32;
let mut start: Option<(usize, usize)> = None;
for line_idx in start_line..=end_line {
let line = &buffer.lines[line_idx];
let line_chars: Vec<char> = line.chars().collect();
let from = if line_idx == start_line {
scope_range.start_col.min(line_chars.len())
} else {
0
};
let to = if line_idx == end_line {
scope_range.end_col.min(line_chars.len())
} else {
line_chars.len()
};
for (col, ch) in line_chars.iter().enumerate().take(to).skip(from) {
let ch = *ch;
if ch == open {
if depth == 0 {
start = Some((line_idx, col));
}
depth += 1;
} else if ch == close {
depth -= 1;
if depth == 0
&& let Some((sl, sc)) = start
{
return Some(TextRange {
start_line: sl,
start_col: sc,
end_line: line_idx,
end_col: col + 1,
});
}
}
}
}
None
}
fn find_assignment_in_text(
text: &str,
base_line: usize,
base_col: usize,
end_line: usize,
end_col: usize,
) -> Option<TextRange> {
let chars: Vec<char> = text.chars().collect();
let mut byte_offset = 0usize;
let mut char_idx = 0usize;
while char_idx < chars.len() {
let c = chars[char_idx];
if c == '=' {
let prev = if char_idx > 0 {
Some(chars[char_idx - 1])
} else {
None
};
let next = chars.get(char_idx + 1).copied();
let is_compound = matches!(
prev,
Some('!' | '<' | '>' | '=' | '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^')
) || next == Some('=');
if !is_compound {
let lines_before = text[..byte_offset].matches('\n').count();
let line_start = text[..byte_offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
let col_in_line = text[line_start..byte_offset].chars().count();
let abs_line = base_line + lines_before;
let abs_col = if lines_before == 0 {
base_col + col_in_line
} else {
col_in_line
};
return Some(TextRange {
start_line: abs_line,
start_col: abs_col + 1,
end_line,
end_col,
});
}
}
byte_offset += c.len_utf8();
char_idx += 1;
}
None
}
fn find_assignment_rhs(
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let content = extract_range_text(buffer, scope_range);
if let Some(range) = find_assignment_in_text(
&content,
scope_range.start_line,
scope_range.start_col,
scope_range.end_line,
scope_range.end_col,
) {
return Ok(range);
}
if scope_range.start_line == scope_range.end_line {
let line_idx = scope_range.start_line;
if let Some(line) = buffer.lines.get(line_idx) {
let line_end = line_char_count(line);
if let Some(mut range) = find_assignment_in_text(line, line_idx, 0, line_idx, line_end)
{
range.end_line = line_idx;
range.end_col = line_end;
return Ok(range);
}
}
}
Err(ChordError::resolve(buffer_name, "variable has no value (no assignment found)").into())
}
fn find_member_value(
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let content = extract_range_text(buffer, scope_range);
let mut depth = 0i32;
let chars: Vec<char> = content.chars().collect();
let mut byte_offset = 0usize;
let mut field_colon: Option<usize> = None;
let mut variant_open: Option<(char, usize)> = None;
for (idx, c) in chars.iter().enumerate() {
if depth == 0 {
if *c == ':' {
field_colon = Some(idx);
break;
}
if (*c == '(' || *c == '{') && variant_open.is_none() {
variant_open = Some((*c, byte_offset));
}
}
if *c == '(' || *c == '{' {
depth += 1;
} else if *c == ')' || *c == '}' {
depth -= 1;
}
byte_offset += c.len_utf8();
}
if let Some(colon_char_idx) = field_colon {
let _ = colon_char_idx;
let colon_byte = char_to_byte(&content, colon_char_idx);
let lines_before = content[..colon_byte].matches('\n').count();
let line_start = content[..colon_byte]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0);
let col_in_line = content[line_start..colon_byte].chars().count();
let abs_line = scope_range.start_line + lines_before;
let abs_col = if lines_before == 0 {
scope_range.start_col + col_in_line
} else {
col_in_line
};
return Ok(TextRange {
start_line: abs_line,
start_col: abs_col + 1,
end_line: scope_range.end_line,
end_col: scope_range.end_col,
});
}
if let Some((_open_ch, open_byte)) = variant_open {
let lines_before = content[..open_byte].matches('\n').count();
let line_start = content[..open_byte].rfind('\n').map(|i| i + 1).unwrap_or(0);
let col_in_line = content[line_start..open_byte].chars().count();
let abs_line = scope_range.start_line + lines_before;
let abs_col = if lines_before == 0 {
scope_range.start_col + col_in_line
} else {
col_in_line
};
return Ok(TextRange {
start_line: abs_line,
start_col: abs_col,
end_line: scope_range.end_line,
end_col: scope_range.end_col,
});
}
Err(ChordError::resolve(buffer_name, "member has no value").into())
}
fn resolve_delimiter_scope(
query: &ChordQuery,
buffer: &Buffer,
buffer_name: &str,
) -> Result<TextRange> {
let (line, col) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(buffer_name, "Delimiter scope requires a cursor position")
})?;
find_innermost_delimiter(buffer, line, col, buffer_name)
}
fn find_innermost_delimiter(
buffer: &Buffer,
cursor_line: usize,
cursor_col: usize,
buffer_name: &str,
) -> Result<TextRange> {
let paired = [('(', ')'), ('{', '}'), ('[', ']')];
let self_paired = ['"', '\'', '`'];
let mut candidates: Vec<TextRange> = Vec::new();
for &(open, close) in &paired {
if let Some(range) = find_paired_delimiter(buffer, cursor_line, cursor_col, open, close) {
candidates.push(range);
}
}
for &delim in &self_paired {
if let Some(range) = find_self_paired_delimiter(buffer, cursor_line, cursor_col, delim) {
candidates.push(range);
}
}
candidates
.into_iter()
.min_by_key(delimiter_span_size)
.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"no enclosing delimiter found at cursor position",
)
.into()
})
}
fn delimiter_span_size(range: &TextRange) -> (usize, usize) {
if range.start_line == range.end_line {
(0, range.end_col - range.start_col)
} else {
(range.end_line - range.start_line, range.end_col)
}
}
fn find_paired_delimiter(
buffer: &Buffer,
cursor_line: usize,
cursor_col: usize,
open: char,
close: char,
) -> Option<TextRange> {
let mut best: Option<TextRange> = None;
let mut depth: i32 = 0;
let mut candidates: Vec<(usize, usize)> = Vec::new();
let lines = &buffer.lines;
for line_idx in (0..=cursor_line.min(lines.len().saturating_sub(1))).rev() {
let line_chars: Vec<char> = lines[line_idx].chars().collect();
let start_col = if line_idx == cursor_line {
cursor_col.min(line_chars.len())
} else {
line_chars.len()
};
for col in (0..start_col).rev() {
let ch = line_chars[col];
if ch == close {
depth += 1;
} else if ch == open {
if depth > 0 {
depth -= 1;
} else {
candidates.push((line_idx, col));
}
}
}
}
for (open_line, open_col) in candidates {
let mut d: i32 = 0;
let mut found = false;
'outer: for (line_idx, line) in lines.iter().enumerate().skip(open_line) {
let line_chars: Vec<char> = line.chars().collect();
let from = if line_idx == open_line { open_col } else { 0 };
for (col, &ch) in line_chars.iter().enumerate().skip(from) {
if ch == open {
d += 1;
} else if ch == close {
d -= 1;
if d == 0 {
let encloses = (line_idx > cursor_line)
|| (line_idx == cursor_line && col >= cursor_col);
if encloses {
let range = TextRange {
start_line: open_line,
start_col: open_col,
end_line: line_idx,
end_col: col + 1,
};
if best.as_ref().is_none_or(|b| {
delimiter_span_size(&range) < delimiter_span_size(b)
}) {
best = Some(range);
}
}
found = true;
break 'outer;
}
}
}
}
if !found {
continue;
}
if best.is_some() {
break;
}
}
best
}
fn is_escaped_at(line_chars: &[char], col: usize) -> bool {
let mut count = 0usize;
let mut i = col;
while i > 0 && line_chars[i - 1] == '\\' {
count += 1;
i -= 1;
}
count % 2 == 1
}
fn find_self_paired_delimiter(
buffer: &Buffer,
cursor_line: usize,
cursor_col: usize,
delim: char,
) -> Option<TextRange> {
let lines = &buffer.lines;
let mut count = 0usize;
for (line_idx, line) in lines
.iter()
.enumerate()
.take(cursor_line.min(lines.len().saturating_sub(1)) + 1)
{
let line_chars: Vec<char> = line.chars().collect();
let end = if line_idx == cursor_line {
cursor_col.min(line_chars.len())
} else {
line_chars.len()
};
let mut last_was_backslash = false;
for &ch in line_chars.iter().take(end) {
if ch == delim && !last_was_backslash {
count += 1;
}
last_was_backslash = ch == '\\' && !last_was_backslash;
}
}
if count.is_multiple_of(2) {
return None;
}
let mut open_pos: Option<(usize, usize)> = None;
'backward: for line_idx in (0..=cursor_line.min(lines.len().saturating_sub(1))).rev() {
let line_chars: Vec<char> = lines[line_idx].chars().collect();
let start = if line_idx == cursor_line {
cursor_col.min(line_chars.len())
} else {
line_chars.len()
};
for col in (0..start).rev() {
let ch = line_chars[col];
if ch == delim && !is_escaped_at(&line_chars, col) {
open_pos = Some((line_idx, col));
break 'backward;
}
}
}
let (open_line, open_col) = open_pos?;
let mut close_pos: Option<(usize, usize)> = None;
'forward: for (line_idx, line) in lines.iter().enumerate().skip(cursor_line) {
let line_chars: Vec<char> = line.chars().collect();
let from = if line_idx == cursor_line {
cursor_col.min(line_chars.len())
} else {
0
};
let mut prev_backslash = if from > 0 {
line_chars[from - 1] == '\\'
} else {
false
};
for (col, &ch) in line_chars.iter().enumerate().skip(from) {
if ch == delim && !prev_backslash {
close_pos = Some((line_idx, col));
break 'forward;
}
prev_backslash = ch == '\\' && !prev_backslash;
}
}
let (close_line, close_col) = close_pos?;
Some(TextRange {
start_line: open_line,
start_col: open_col,
end_line: close_line,
end_col: close_col + 1,
})
}
fn shrink_range(buffer: &Buffer, range: &TextRange) -> TextRange {
let content = extract_range_text(buffer, range);
let first_char = content.chars().next();
let last_char = content.chars().last();
let is_delimited = matches!(
(first_char, last_char),
(Some('('), Some(')'))
| (Some('{'), Some('}'))
| (Some('['), Some(']'))
| (Some('"'), Some('"'))
| (Some('\''), Some('\''))
| (Some('`'), Some('`'))
);
if is_delimited {
let total_chars = content.chars().count();
if total_chars >= 2 {
let inner_lines = content.matches('\n').count();
let start_line = range.start_line;
let start_col = range.start_col + 1;
let end_line;
let end_col;
if inner_lines == 0 {
end_line = start_line;
end_col = range.end_col.saturating_sub(1);
} else {
end_line = range.end_line;
end_col = range.end_col.saturating_sub(1);
}
return TextRange {
start_line,
start_col,
end_line,
end_col,
};
}
}
*range
}
fn resolve_word_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let cursor = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(buffer_name, "Word component requires a cursor position")
})?;
let cursor_line = cursor.0;
let cursor_col = cursor.1;
let line_idx = if query.scope == Scope::Line {
query
.args
.target_line
.unwrap_or(cursor_line)
.min(buffer.line_count().saturating_sub(1))
} else {
cursor_line.min(buffer.line_count().saturating_sub(1))
};
let line = buffer.lines.get(line_idx).map(|l| l.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
if chars.is_empty() || chars.iter().all(|c| c.is_whitespace()) {
return Err(ChordError::resolve(buffer_name, "no word found on current line").into());
}
let scope_start_col = if scope_range.start_line == line_idx {
scope_range.start_col
} else {
0
};
let scope_end_col = if scope_range.end_line == line_idx {
scope_range.end_col
} else {
chars.len()
};
let words = collect_words_in_range(&chars, scope_start_col, scope_end_col);
if words.is_empty() {
return Err(ChordError::resolve(buffer_name, "no word found on current line").into());
}
match query.positional {
Positional::First => {
let (start, end) = words[0];
Ok(TextRange {
start_line: line_idx,
start_col: start,
end_line: line_idx,
end_col: end,
})
}
Positional::Last => {
let (start, end) = words[words.len() - 1];
Ok(TextRange {
start_line: line_idx,
start_col: start,
end_line: line_idx,
end_col: end,
})
}
Positional::Next => {
let current_word_end = find_current_word_end(&words, cursor_col);
if let Some((start, end)) = words
.iter()
.find(|(s, _)| *s > current_word_end.unwrap_or(cursor_col))
{
Ok(TextRange {
start_line: line_idx,
start_col: *start,
end_line: line_idx,
end_col: *end,
})
} else {
Err(ChordError::resolve(buffer_name, "no next word on this line").into())
}
}
Positional::Previous => {
let current_word_start = find_current_word_start(&words, cursor_col);
if let Some((start, end)) = words
.iter()
.rev()
.find(|(_, e)| *e <= current_word_start.unwrap_or(cursor_col))
{
Ok(TextRange {
start_line: line_idx,
start_col: *start,
end_line: line_idx,
end_col: *end,
})
} else {
Err(ChordError::resolve(buffer_name, "no previous word on this line").into())
}
}
_ => {
if let Some((start, end)) = find_word_at_cursor(&words, cursor_col) {
Ok(TextRange {
start_line: line_idx,
start_col: start,
end_line: line_idx,
end_col: end,
})
} else {
if let Some((start, end)) = words.iter().find(|(s, _)| *s > cursor_col) {
Ok(TextRange {
start_line: line_idx,
start_col: *start,
end_line: line_idx,
end_col: *end,
})
} else if let Some((start, end)) =
words.iter().rev().find(|(_, e)| *e <= cursor_col)
{
Ok(TextRange {
start_line: line_idx,
start_col: *start,
end_line: line_idx,
end_col: *end,
})
} else {
Err(ChordError::resolve(buffer_name, "no word found at cursor position").into())
}
}
}
}
}
fn collect_words_in_range(chars: &[char], start: usize, end: usize) -> Vec<(usize, usize)> {
let mut words = Vec::new();
let mut i = start;
while i < end {
if !chars[i].is_whitespace() {
let word_start = i;
while i < end && !chars[i].is_whitespace() {
i += 1;
}
words.push((word_start, i));
} else {
i += 1;
}
}
words
}
fn find_word_at_cursor(words: &[(usize, usize)], col: usize) -> Option<(usize, usize)> {
words
.iter()
.find(|(start, end)| col >= *start && col < *end)
.copied()
}
fn find_current_word_end(words: &[(usize, usize)], col: usize) -> Option<usize> {
words
.iter()
.find(|(start, end)| col >= *start && col < *end)
.map(|(_, end)| *end)
}
fn find_current_word_start(words: &[(usize, usize)], col: usize) -> Option<usize> {
words
.iter()
.find(|(start, end)| col >= *start && col < *end)
.map(|(start, _)| *start)
}
fn resolve_definition_component(
query: &ChordQuery,
buffer: &Buffer,
scope_range: &TextRange,
buffer_name: &str,
) -> Result<TextRange> {
let text = extract_range_text(buffer, scope_range);
match query.scope {
Scope::Variable => {
let chars: Vec<char> = text.chars().collect();
let eq_pos = find_standalone_equals(&chars);
let def_end_char = if let Some(pos) = eq_pos {
let mut end = pos;
while end > 0 && chars[end - 1].is_whitespace() {
end -= 1;
}
end
} else {
let mut end = chars.len();
while end > 0 && (chars[end - 1] == ';' || chars[end - 1].is_whitespace()) {
end -= 1;
}
end
};
let def_range = char_offset_to_range(scope_range, &text, 0, def_end_char);
Ok(def_range)
}
Scope::Function => {
let chars: Vec<char> = text.chars().collect();
let brace_pos = find_first_open_brace(&chars);
let def_end_char = if let Some(pos) = brace_pos {
let mut end = pos;
while end > 0 && chars[end - 1].is_whitespace() {
end -= 1;
}
end
} else {
let mut end = chars.len();
while end > 0 && (chars[end - 1] == ';' || chars[end - 1].is_whitespace()) {
end -= 1;
}
end
};
let def_range = char_offset_to_range(scope_range, &text, 0, def_end_char);
Ok(def_range)
}
Scope::Struct => {
let chars: Vec<char> = text.chars().collect();
let brace_pos = find_first_open_brace(&chars);
let paren_pos = find_first_open_paren(&chars);
let def_end_char = if let Some(pos) = brace_pos {
let mut end = pos;
while end > 0 && chars[end - 1].is_whitespace() {
end -= 1;
}
end
} else if let Some(pos) = paren_pos {
let mut end = pos;
while end > 0 && chars[end - 1].is_whitespace() {
end -= 1;
}
end
} else {
let mut end = chars.len();
while end > 0 && (chars[end - 1] == ';' || chars[end - 1].is_whitespace()) {
end -= 1;
}
end
};
let def_range = char_offset_to_range(scope_range, &text, 0, def_end_char);
Ok(def_range)
}
_ => Err(ChordError::resolve(
buffer_name,
format!(
"Definition component is not valid for {} scope",
query.scope
),
)
.into()),
}
}
fn find_standalone_equals(chars: &[char]) -> Option<usize> {
for (i, &c) in chars.iter().enumerate() {
if c == '=' {
let prev = if i > 0 { Some(chars[i - 1]) } else { None };
let next = chars.get(i + 1).copied();
let is_compound = matches!(
prev,
Some('!' | '<' | '>' | '=' | '+' | '-' | '*' | '/' | '%' | '&' | '|' | '^')
) || next == Some('=')
|| next == Some('>');
if !is_compound {
return Some(i);
}
}
}
None
}
fn find_first_open_brace(chars: &[char]) -> Option<usize> {
chars.iter().position(|&c| c == '{')
}
fn find_first_open_paren(chars: &[char]) -> Option<usize> {
let mut angle_depth = 0i32;
for (i, &c) in chars.iter().enumerate() {
match c {
'<' => angle_depth += 1,
'>' => angle_depth -= 1,
'(' if angle_depth <= 0 => return Some(i),
_ => {}
}
}
None
}
fn char_offset_to_range(
scope_range: &TextRange,
text: &str,
start_char: usize,
end_char: usize,
) -> TextRange {
let chars: Vec<char> = text.chars().collect();
let mut line = scope_range.start_line;
let mut col = scope_range.start_col;
let mut start_line = line;
let mut start_col = col;
let mut end_line = line;
let mut end_col = col;
for (i, &ch) in chars.iter().enumerate() {
if i == start_char {
start_line = line;
start_col = col;
}
if i == end_char {
end_line = line;
end_col = col;
break;
}
if ch == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
if end_char >= chars.len() {
end_line = line;
end_col = col;
}
TextRange {
start_line,
start_col,
end_line,
end_col,
}
}
fn resolve_list(
query: &ChordQuery,
buffer: &Buffer,
lsp: &mut LspEngine,
buffer_name: &str,
) -> Result<(Vec<ListItem>, Vec<String>)> {
let mut items = collect_list_candidates(query, buffer, lsp, buffer_name)?;
if let Positional::Count(n) = query.positional {
let (cl, cc) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"numeric positional requires a cursor position; pass cursor:\"line,col\"",
)
})?;
let after: Vec<ListItem> = items
.into_iter()
.filter(|item| item.line > cl || (item.line == cl && item.col > cc))
.collect();
let available = after.len();
let actual = available.min(n as usize);
let mut warnings = Vec::new();
if actual < n as usize {
warnings.push(format!(
"only {} of {} requested occurrences found",
actual, n
));
}
let result: Vec<ListItem> = after.into_iter().take(actual).collect();
return Ok((result, warnings));
}
if query.positional == Positional::Inside {
let (cl, cc) = query.args.cursor_pos.ok_or_else(|| {
ChordError::resolve(
buffer_name,
"List with Inside positional requires a cursor position",
)
})?;
if let Ok(symbols) = lsp.document_symbols(Path::new(buffer_name))
&& let Some(enclosing) = find_innermost_symbol(&symbols, cl, cc)
{
let range = &enclosing.range;
items.retain(|item| {
(item.line > range.start_line
|| (item.line == range.start_line && item.col >= range.start_col))
&& (item.line < range.end_line
|| (item.line == range.end_line && item.col <= range.end_col))
});
}
return Ok((items, vec![]));
}
let cursor = query.args.cursor_pos;
items = filter_list_by_positional(items, query.positional, cursor);
Ok((items, vec![]))
}
fn collect_list_candidates(
query: &ChordQuery,
buffer: &Buffer,
lsp: &mut LspEngine,
buffer_name: &str,
) -> Result<Vec<ListItem>> {
match query.component {
Component::Word => collect_word_items(query, buffer, buffer_name),
Component::Name | Component::Definition | Component::End | Component::Self_ => {
collect_lsp_items(query, buffer, lsp, buffer_name)
}
_ => Err(ChordError::resolve(
buffer_name,
"List action only supports Name, Definition, End, and Self components for LSP scopes",
)
.into()),
}
}
fn collect_word_items(
query: &ChordQuery,
buffer: &Buffer,
buffer_name: &str,
) -> Result<Vec<ListItem>> {
let mut items = Vec::new();
match query.scope {
Scope::Buffer => {
for (line_idx, line) in buffer.lines.iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
let words = collect_words_in_range(&chars, 0, chars.len());
for (start, end) in words {
let val: String = chars[start..end].iter().collect();
items.push(ListItem {
val,
line: line_idx,
col: start,
});
}
}
}
Scope::Line => {
let line_idx = query
.args
.target_line
.or(query.args.cursor_pos.map(|(l, _)| l))
.unwrap_or(0)
.min(buffer.line_count().saturating_sub(1));
let line = buffer.lines.get(line_idx).map(|l| l.as_str()).unwrap_or("");
let chars: Vec<char> = line.chars().collect();
let words = collect_words_in_range(&chars, 0, chars.len());
for (start, end) in words {
let val: String = chars[start..end].iter().collect();
items.push(ListItem {
val,
line: line_idx,
col: start,
});
}
}
_ => {
return Err(ChordError::resolve(
buffer_name,
"Word component with List action only works with Line or Buffer scope",
)
.into());
}
}
Ok(items)
}
fn collect_lsp_items(
query: &ChordQuery,
buffer: &Buffer,
lsp: &mut LspEngine,
buffer_name: &str,
) -> Result<Vec<ListItem>> {
let path = Path::new(buffer_name);
let symbols = lsp.document_symbols(path).map_err(|e| {
ChordError::resolve(
buffer_name,
format!("LSP not ready: {e}; LSP-scoped chords need an active language server"),
)
})?;
let target_kinds = scope_to_symbol_kinds(query.scope);
let mut flat: Vec<&DocumentSymbol> = Vec::new();
if target_kinds.is_empty() {
flatten_all_symbols(&symbols, &mut flat);
} else {
flatten_by_kind(&symbols, &target_kinds, &mut flat);
}
flat.sort_by(|a, b| {
a.range
.start_line
.cmp(&b.range.start_line)
.then(a.range.start_col.cmp(&b.range.start_col))
});
let mut items = Vec::new();
for sym in flat {
match query.component {
Component::Name => {
let name_range = symbol_name_range(sym);
items.push(ListItem {
val: sym.name.clone(),
line: name_range.start_line,
col: name_range.start_col,
});
}
Component::Definition => {
let sym_range = symbol_to_range(&sym.range);
let scope_range = sym_range;
let mut def_query = query.clone();
def_query.scope = match sym.kind {
SymbolKind::Function | SymbolKind::Method => Scope::Function,
SymbolKind::Struct | SymbolKind::Enum => Scope::Struct,
SymbolKind::Variable | SymbolKind::Const => Scope::Variable,
_ => Scope::Function,
};
if let Ok(def_range) =
resolve_definition_component(&def_query, buffer, &scope_range, buffer_name)
{
let val = extract_range_text(buffer, &def_range);
items.push(ListItem {
val,
line: def_range.start_line,
col: def_range.start_col,
});
}
}
Component::End => {
let sym_range = symbol_to_range(&sym.range);
items.push(ListItem {
val: sym.name.clone(),
line: sym_range.end_line,
col: sym_range.end_col,
});
}
Component::Self_ => {
let sym_range = symbol_to_range(&sym.range);
let val = extract_range_text(buffer, &sym_range);
items.push(ListItem {
val,
line: sym_range.start_line,
col: sym_range.start_col,
});
}
_ => {}
}
}
Ok(items)
}
fn flatten_all_symbols<'a>(symbols: &'a [DocumentSymbol], out: &mut Vec<&'a DocumentSymbol>) {
for sym in symbols {
out.push(sym);
flatten_all_symbols(&sym.children, out);
}
}
fn filter_list_by_positional(
items: Vec<ListItem>,
positional: Positional,
cursor: Option<(usize, usize)>,
) -> Vec<ListItem> {
match positional {
Positional::Entire => items,
Positional::After => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.filter(|item| item.line > cl || (item.line == cl && item.col > cc))
.collect()
}
Positional::Before => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.filter(|item| item.line < cl || (item.line == cl && item.col < cc))
.collect()
}
Positional::Next => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.find(|item| item.line > cl || (item.line == cl && item.col > cc))
.into_iter()
.collect()
}
Positional::Previous => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.rev()
.find(|item| item.line < cl || (item.line == cl && item.col < cc))
.into_iter()
.collect()
}
Positional::Last => items.into_iter().last().into_iter().collect(),
Positional::First => items.into_iter().next().into_iter().collect(),
Positional::Inside => items,
Positional::Until => {
let (cl, cc) = cursor.unwrap_or((usize::MAX, usize::MAX));
items
.into_iter()
.filter(|item| item.line < cl || (item.line == cl && item.col <= cc))
.collect()
}
Positional::To => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.filter(|item| item.line > cl || (item.line == cl && item.col >= cc))
.collect()
}
Positional::Outside => items, Positional::Count(n) => {
let (cl, cc) = cursor.unwrap_or((0, 0));
items
.into_iter()
.filter(|item| item.line > cl || (item.line == cl && item.col > cc))
.take(n as usize)
.collect()
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::path::PathBuf;
use crate::commands::chord_engine::types::{
ChordArgs, ChordQuery, CursorPosition, EditorMode, TextRange,
};
use crate::commands::lsp_engine::{LspEngine, LspEngineConfig};
use crate::data::buffer::Buffer;
use crate::data::chord_types::{Action, Component, Positional, Scope};
use crate::data::lsp::types::{DocumentSymbol, SelectionRange, SymbolKind, SymbolRange};
use super::*;
fn buf(lines: &[&str]) -> Buffer {
Buffer {
path: PathBuf::from("/test/file.rs"),
lines: lines.iter().map(|s| s.to_string()).collect(),
dirty: false,
trailing_newline: false,
last_disk_mtime: None,
disk_changed: false,
disk_deleted: false,
}
}
fn named_buf(path: &str, lines: &[&str]) -> Buffer {
Buffer {
path: PathBuf::from(path),
lines: lines.iter().map(|s| s.to_string()).collect(),
dirty: false,
trailing_newline: false,
last_disk_mtime: None,
disk_changed: false,
disk_deleted: false,
}
}
fn query(
action: Action,
pos: Positional,
scope: Scope,
comp: Component,
target_name: Option<&str>,
target_line: Option<usize>,
cursor_pos: Option<(usize, usize)>,
) -> ChordQuery {
ChordQuery {
action,
positional: pos,
scope,
component: comp,
args: ChordArgs {
target_name: target_name.map(String::from),
parent_name: None,
target_line,
cursor_pos,
value: None,
find: None,
replace: None,
},
requires_lsp: scope.requires_lsp(),
}
}
fn sym(
name: &str,
kind: SymbolKind,
sl: usize,
sc: usize,
el: usize,
ec: usize,
) -> DocumentSymbol {
DocumentSymbol {
name: name.to_string(),
kind,
range: SymbolRange {
start_line: sl,
start_col: sc,
end_line: el,
end_col: ec,
},
selection_range: None,
children: vec![],
}
}
#[allow(clippy::too_many_arguments)]
fn sym_sel(
name: &str,
kind: SymbolKind,
sl: usize,
sc: usize,
el: usize,
ec: usize,
sel_sl: usize,
sel_sc: usize,
sel_el: usize,
sel_ec: usize,
) -> DocumentSymbol {
DocumentSymbol {
name: name.to_string(),
kind,
range: SymbolRange {
start_line: sl,
start_col: sc,
end_line: el,
end_col: ec,
},
selection_range: Some(SymbolRange {
start_line: sel_sl,
start_col: sel_sc,
end_line: sel_el,
end_col: sel_ec,
}),
children: vec![],
}
}
fn sym_with_children(
name: &str,
kind: SymbolKind,
sl: usize,
sc: usize,
el: usize,
ec: usize,
children: Vec<DocumentSymbol>,
) -> DocumentSymbol {
DocumentSymbol {
name: name.to_string(),
kind,
range: SymbolRange {
start_line: sl,
start_col: sc,
end_line: el,
end_col: ec,
},
selection_range: None,
children,
}
}
fn mock_lsp(path: &str, symbols: Vec<DocumentSymbol>) -> LspEngine {
let mut lsp = LspEngine::new(LspEngineConfig::default());
lsp.inject_test_symbols(PathBuf::from(path), symbols);
lsp
}
fn no_lsp() -> LspEngine {
LspEngine::new(LspEngineConfig::default())
}
#[test]
fn line_scope_by_explicit_line_number() {
let buffer = buf(&["first", "second", "third"]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
Some(1),
None,
);
let range = resolve_line_scope(&q, &buffer, "/buf").unwrap();
assert_eq!(range.start_line, 1);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 1);
assert_eq!(range.end_col, 6);
}
#[test]
fn line_scope_by_cursor_position() {
let buffer = buf(&["alpha", "beta", "gamma"]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
None,
Some((2, 3)),
);
let range = resolve_line_scope(&q, &buffer, "/buf").unwrap();
assert_eq!(range.start_line, 2);
assert_eq!(range.end_line, 2);
assert_eq!(range.end_col, 5);
}
#[test]
fn line_scope_without_input_errors() {
let buffer = buf(&["only", "line"]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let err = resolve_line_scope(&q, &buffer, "/buf").unwrap_err();
assert!(format!("{err}").contains("requires"));
}
#[test]
fn line_scope_out_of_bounds_errors() {
let buffer = buf(&["one", "two"]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
Some(5),
None,
);
assert!(resolve_line_scope(&q, &buffer, "/buf").is_err());
}
#[test]
fn line_scope_unicode_char_count() {
let buffer = buf(&["héllo wörld"]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
Some(0),
None,
);
let range = resolve_line_scope(&q, &buffer, "/buf").unwrap();
assert_eq!(range.end_col, 11);
}
#[test]
fn line_scope_next_positional_resolves_next_line() {
let buffer = buf(&["first", "second", "third"]);
let q = query(
Action::Jump,
Positional::Next,
Scope::Line,
Component::End,
None,
None,
Some((0, 0)),
);
let range = resolve_line_scope(&q, &buffer, "/buf").unwrap();
assert_eq!(range.start_line, 1);
assert_eq!(range.end_line, 1);
assert_eq!(range.end_col, 6);
}
#[test]
fn line_scope_previous_positional_resolves_previous_line() {
let buffer = buf(&["first", "second", "third"]);
let q = query(
Action::Jump,
Positional::Previous,
Scope::Line,
Component::Beginning,
None,
None,
Some((2, 3)),
);
let range = resolve_line_scope(&q, &buffer, "/buf").unwrap();
assert_eq!(range.start_line, 1);
assert_eq!(range.end_line, 1);
}
#[test]
fn line_scope_next_at_last_line_errors() {
let buffer = buf(&["first", "second"]);
let q = query(
Action::Jump,
Positional::Next,
Scope::Line,
Component::End,
None,
None,
Some((1, 0)),
);
assert!(resolve_line_scope(&q, &buffer, "/buf").is_err());
}
#[test]
fn line_scope_previous_at_first_line_errors() {
let buffer = buf(&["first", "second"]);
let q = query(
Action::Jump,
Positional::Previous,
Scope::Line,
Component::Beginning,
None,
None,
Some((0, 0)),
);
assert!(resolve_line_scope(&q, &buffer, "/buf").is_err());
}
#[test]
fn buffer_scope_entire_file() {
let buffer = buf(&["line one", "line two", "line three"]);
let range = resolve_buffer_scope(&buffer).unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 2);
assert_eq!(range.end_col, 10);
}
#[test]
fn buffer_scope_empty() {
let buffer = Buffer {
path: PathBuf::from("/test"),
lines: vec![],
dirty: false,
trailing_newline: false,
last_disk_mtime: None,
disk_changed: false,
disk_deleted: false,
};
let range = resolve_buffer_scope(&buffer).unwrap();
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 0);
}
#[test]
fn find_brace_range_single_line() {
let buffer = buf(&["fn foo() { 42 }"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 15,
};
let range = find_brace_range(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 10);
assert_eq!(range.end_col, 14);
}
#[test]
fn find_brace_range_multi_line() {
let buffer = buf(&["fn foo() {", " 42", "}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 2,
end_col: 1,
};
let range = find_brace_range(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 1);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 1);
assert_eq!(range.end_col, 6);
}
#[test]
fn find_brace_range_no_braces_errors() {
let buffer = buf(&["no braces here"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
assert!(find_brace_range(&buffer, &scope, "/buf").is_err());
}
#[test]
fn find_paren_range_simple() {
let buffer = buf(&["fn foo(x: i32, y: i32) -> i32 {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 33,
};
let range = find_paren_range(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 6);
assert_eq!(range.end_col, 22);
}
#[test]
fn find_paren_range_no_params_empty() {
let buffer = buf(&["fn foo() {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 11,
};
let range = find_paren_range(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 6);
assert_eq!(range.end_col, 8);
let inside = shrink_range(&buffer, &range);
assert!(inside.is_empty());
}
#[test]
fn find_paren_range_multi_line() {
let buffer = buf(&["fn foo(", " x: i32,", " y: i32,", ") {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 3,
end_col: 4,
};
let range = find_paren_range(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 6);
assert_eq!(range.end_line, 3);
assert_eq!(range.end_col, 1);
}
#[test]
fn find_paren_range_no_parens_errors() {
let buffer = buf(&["struct Foo { x: i32 }"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 21,
};
assert!(find_paren_range(&buffer, &scope, "/buf").is_err());
}
#[test]
fn find_assignment_rhs_simple() {
let buffer = buf(&["let x = 42;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 11,
};
let range = find_assignment_rhs(&buffer, &scope, "/buf").unwrap();
assert!(range.start_col > 6);
}
#[test]
fn find_assignment_rhs_no_value_errors() {
let buffer = buf(&["let x: i32;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 11,
};
assert!(find_assignment_rhs(&buffer, &scope, "/buf").is_err());
}
#[test]
fn find_assignment_rhs_skips_double_eq() {
let buffer = buf(&["let x = if a == b { 1 } else { 2 };"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 35,
};
let range = find_assignment_rhs(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 7); }
#[test]
fn find_assignment_rhs_expands_to_line_when_scope_is_name_only() {
let buffer = buf(&[" let asdf = dude();"]);
let scope = TextRange {
start_line: 0,
start_col: 8,
end_line: 0,
end_col: 12,
};
let range = find_assignment_rhs(&buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 14); assert_eq!(range.end_col, 22); }
#[test]
fn find_member_value_struct_field() {
let buffer = buf(&[" field: i32,"]);
let scope = TextRange {
start_line: 0,
start_col: 4,
end_line: 0,
end_col: 15,
};
let range = find_member_value(&buffer, &scope, "/buf").unwrap();
assert!(range.start_col > scope.start_col);
}
#[test]
fn find_member_value_unit_variant_errors() {
let buffer = buf(&[" None,"]);
let scope = TextRange {
start_line: 0,
start_col: 4,
end_line: 0,
end_col: 9,
};
assert!(find_member_value(&buffer, &scope, "/buf").is_err());
}
#[test]
fn find_member_value_tuple_variant() {
let buffer = buf(&[" Some(T),"]);
let scope = TextRange {
start_line: 0,
start_col: 4,
end_line: 0,
end_col: 11,
};
let range = find_member_value(&buffer, &scope, "/buf").unwrap();
assert!(range.start_col >= 8);
}
#[test]
fn find_member_value_struct_variant() {
let buffer = buf(&[" Variant { field: T },"]);
let scope = TextRange {
start_line: 0,
start_col: 4,
end_line: 0,
end_col: 24,
};
let range = find_member_value(&buffer, &scope, "/buf").unwrap();
assert!(range.start_col >= 12);
}
#[test]
fn shrink_range_removes_braces() {
let buffer = buf(&["{hello}"]);
let range = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 7,
};
let shrunk = shrink_range(&buffer, &range);
assert_eq!(shrunk.start_col, 1);
assert_eq!(shrunk.end_col, 6);
}
#[test]
fn shrink_range_removes_parens() {
let buffer = buf(&["(abc)"]);
let range = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 5,
};
let shrunk = shrink_range(&buffer, &range);
assert_eq!(shrunk.start_col, 1);
assert_eq!(shrunk.end_col, 4);
}
#[test]
fn shrink_range_no_delimiters_unchanged() {
let buffer = buf(&["hello"]);
let range = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 5,
};
let shrunk = shrink_range(&buffer, &range);
assert_eq!(shrunk, range);
}
#[test]
fn outside_returns_two_ranges_when_component_in_middle() {
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 20,
};
let comp = TextRange {
start_line: 0,
start_col: 5,
end_line: 0,
end_col: 10,
};
let ranges = outside_ranges(&scope, &comp);
assert_eq!(ranges.len(), 2);
assert_eq!(ranges[0].end_col, 5);
assert_eq!(ranges[1].start_col, 10);
}
#[test]
fn outside_returns_single_range_when_component_at_start() {
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 20,
};
let comp = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 10,
};
let ranges = outside_ranges(&scope, &comp);
assert_eq!(ranges.len(), 1);
assert_eq!(ranges[0].start_col, 10);
}
#[test]
fn outside_returns_empty_point_when_component_equals_scope() {
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 20,
};
let ranges = outside_ranges(&scope, &scope);
assert_eq!(ranges.len(), 1);
assert!(ranges[0].is_empty());
}
#[test]
fn positional_entire_returns_component() {
let buffer = buf(&["fn foo(x: i32) {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 18,
};
let comp = TextRange {
start_line: 0,
start_col: 6,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Yank,
Positional::Entire,
Scope::Function,
Component::Parameters,
None,
None,
None,
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result, vec![comp]);
}
#[test]
fn positional_outside_excludes_component() {
let buffer = buf(&["fn foo(x: i32) {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 17,
};
let comp = TextRange {
start_line: 0,
start_col: 6,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::Outside,
Scope::Function,
Component::Parameters,
None,
None,
None,
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].end_col, 6);
assert_eq!(result[1].start_col, 14);
}
#[test]
fn positional_until_no_cursor_errors() {
let buffer = buf(&["abc"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 3,
};
let comp = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 3,
};
let q = query(
Action::Change,
Positional::Until,
Scope::Line,
Component::Self_,
None,
None,
None,
);
assert!(apply_positional(&q, &buffer, &scope, &comp, "/buf").is_err());
}
#[test]
fn positional_inside_shrinks_delimited_range() {
let buffer = buf(&["(hello)"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 7,
};
let comp = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 7,
};
let q = query(
Action::Change,
Positional::Inside,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result[0].start_col, 1);
assert_eq!(result[0].end_col, 6);
}
#[test]
fn positional_until_from_cursor_to_component() {
let buffer = buf(&["fn foo(x: i32) {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 18,
};
let comp = TextRange {
start_line: 0,
start_col: 6,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::Until,
Scope::Function,
Component::Parameters,
None,
None,
Some((0, 2)),
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result[0].start_col, 2);
assert_eq!(result[0].end_col, 6);
}
#[test]
fn cursor_mode_change_no_value_goes_to_edit() {
let range = TextRange {
start_line: 2,
start_col: 5,
end_line: 2,
end_col: 10,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let (cursor, mode) = resolve_cursor_and_mode(&q, &range);
assert_eq!(cursor, Some(CursorPosition { line: 2, col: 5 }));
assert_eq!(mode, Some(EditorMode::Edit));
}
#[test]
fn cursor_mode_yank_is_none() {
let range = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 5,
};
let q = query(
Action::Yank,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let (cursor, mode) = resolve_cursor_and_mode(&q, &range);
assert!(cursor.is_none());
assert!(mode.is_none());
}
#[test]
fn cursor_aals_lands_on_next_line() {
let range = TextRange {
start_line: 3,
start_col: 10,
end_line: 3,
end_col: 10,
};
let q = query(
Action::Append,
Positional::After,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let (cursor, _) = resolve_cursor_and_mode(&q, &range);
assert_eq!(cursor, Some(CursorPosition { line: 4, col: 0 }));
}
#[test]
fn cursor_aels_lands_at_end_of_line() {
let range = TextRange {
start_line: 3,
start_col: 0,
end_line: 3,
end_col: 15,
};
let q = query(
Action::Append,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let (cursor, _) = resolve_cursor_and_mode(&q, &range);
assert_eq!(cursor, Some(CursorPosition { line: 3, col: 15 }));
}
#[test]
fn cursor_pbls_stays_on_inserted_line() {
let range = TextRange {
start_line: 5,
start_col: 0,
end_line: 5,
end_col: 0,
};
let q = query(
Action::Prepend,
Positional::Before,
Scope::Line,
Component::Self_,
None,
None,
None,
);
let (cursor, _) = resolve_cursor_and_mode(&q, &range);
assert_eq!(cursor, Some(CursorPosition { line: 5, col: 0 }));
}
#[test]
fn function_scope_by_name_resolves_range() {
let path = "/test/file.rs";
let lines = &["fn foo() {}", "fn bar(x: i32) {", " x + 1", "}"];
let buffer = named_buf(path, lines);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("foo", SymbolKind::Function, 0, 0, 0, 11),
sym("bar", SymbolKind::Function, 1, 0, 3, 1),
],
);
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Self_,
Some("bar"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.end_line, 3);
}
#[test]
fn function_scope_not_found_lists_available_symbols() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 0, 11)]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Self_,
Some("missing"),
None,
None,
);
let err = resolve(&q, &buffers, &mut lsp).unwrap_err();
assert!(format!("{err}").contains("foo"));
}
#[test]
fn function_scope_innermost_at_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn outer() { fn inner() {} }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let inner = sym("inner", SymbolKind::Function, 0, 13, 0, 26);
let outer = sym_with_children(
"outer",
SymbolKind::Function,
0,
0,
0,
28,
vec![inner.clone()],
);
let mut lsp = mock_lsp(path, vec![outer]);
let q = query(
Action::Yank,
Positional::Entire,
Scope::Function,
Component::Self_,
None,
None,
Some((0, 18)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 13);
}
#[test]
fn function_scope_without_input_errors() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 0, 11)]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Self_,
None,
None,
None,
);
assert!(resolve(&q, &buffers, &mut lsp).is_err());
}
#[test]
fn member_scope_field_by_name() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["struct Foo {", " x: i32,", " y: f64,", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let field_x = sym("x", SymbolKind::Field, 1, 4, 1, 10);
let field_y = sym("y", SymbolKind::Field, 2, 4, 2, 10);
let struct_sym = sym_with_children(
"Foo",
SymbolKind::Struct,
0,
0,
3,
1,
vec![field_x, field_y],
);
let mut lsp = mock_lsp(path, vec![struct_sym]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Member,
Component::Self_,
Some("y"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 2);
}
#[test]
fn member_scope_ambiguous_without_parent() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["struct A { x: i32 }", "struct B { x: i32 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let a_x = sym("x", SymbolKind::Field, 0, 11, 0, 18);
let b_x = sym("x", SymbolKind::Field, 1, 11, 1, 18);
let a = sym_with_children("A", SymbolKind::Struct, 0, 0, 0, 19, vec![a_x]);
let b = sym_with_children("B", SymbolKind::Struct, 1, 0, 1, 19, vec![b_x]);
let mut lsp = mock_lsp(path, vec![a, b]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Member,
Component::Self_,
Some("x"),
None,
None,
);
let err = resolve(&q, &buffers, &mut lsp).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("ambiguous"), "expected ambiguous error: {msg}");
}
#[test]
fn member_scope_disambiguates_with_parent() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["struct A { x: i32 }", "struct B { x: i32 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let a_x = sym("x", SymbolKind::Field, 0, 11, 0, 18);
let b_x = sym("x", SymbolKind::Field, 1, 11, 1, 18);
let a = sym_with_children("A", SymbolKind::Struct, 0, 0, 0, 19, vec![a_x]);
let b = sym_with_children("B", SymbolKind::Struct, 1, 0, 1, 19, vec![b_x]);
let mut lsp = mock_lsp(path, vec![a, b]);
let mut q = query(
Action::Change,
Positional::Entire,
Scope::Member,
Component::Self_,
Some("x"),
None,
None,
);
q.args.parent_name = Some("B".to_string());
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
}
#[test]
fn member_scope_enum_variant_by_name() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&["enum Color {", " Red,", " Green,", " Blue,", "}"],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let red = sym("Red", SymbolKind::Field, 1, 4, 1, 7);
let green = sym("Green", SymbolKind::Field, 2, 4, 2, 9);
let blue = sym("Blue", SymbolKind::Field, 3, 4, 3, 8);
let enum_sym = sym_with_children(
"Color",
SymbolKind::Enum,
0,
0,
4,
1,
vec![red, green, blue],
);
let mut lsp = mock_lsp(path, vec![enum_sym]);
let q = query(
Action::Yank,
Positional::Entire,
Scope::Member,
Component::Self_,
Some("Green"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 2);
}
#[test]
fn member_scope_recurses_into_nested_modules() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["mod inner {", " struct Foo { x: i32 }", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let field_x = sym("x", SymbolKind::Field, 1, 17, 1, 24);
let foo = sym_with_children("Foo", SymbolKind::Struct, 1, 4, 1, 27, vec![field_x]);
let inner = sym_with_children("inner", SymbolKind::Module, 0, 0, 2, 1, vec![foo]);
let mut lsp = mock_lsp(path, vec![inner]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Member,
Component::Self_,
Some("x"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
}
#[test]
fn arguments_component_finds_call_site() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&["fn foo(x: i32) -> i32 { x }", "", "fn main() { foo(42); }"],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("foo", SymbolKind::Function, 0, 0, 0, 27),
sym("main", SymbolKind::Function, 2, 0, 2, 22),
],
);
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Arguments,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let primary = res.target_ranges.first().unwrap();
assert_eq!(primary.start_line, 2);
}
#[test]
fn arguments_component_no_call_errors() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 0, 11)]);
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Arguments,
Some("foo"),
None,
None,
);
assert!(resolve(&q, &buffers, &mut lsp).is_err());
}
#[test]
fn next_lsp_scope_finds_following_symbol() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn alpha() {}", "fn beta() {}", "fn gamma() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 0, 0, 0, 13),
sym("beta", SymbolKind::Function, 1, 0, 1, 12),
sym("gamma", SymbolKind::Function, 2, 0, 2, 13),
],
);
let q = query(
Action::Yank,
Positional::Next,
Scope::Function,
Component::Self_,
None,
None,
Some((0, 5)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
}
#[test]
fn previous_lsp_scope_finds_preceding_symbol() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn alpha() {}", "fn beta() {}", "fn gamma() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 0, 0, 0, 13),
sym("beta", SymbolKind::Function, 1, 0, 1, 12),
sym("gamma", SymbolKind::Function, 2, 0, 2, 13),
],
);
let q = query(
Action::Yank,
Positional::Previous,
Scope::Function,
Component::Self_,
None,
None,
Some((2, 5)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
}
#[test]
fn multi_buffer_chord_applies_to_each_independently() {
let path_a = "/test/a.txt";
let path_b = "/test/b.txt";
let buf_a = named_buf(path_a, &["aaa", "bbb", "ccc"]);
let buf_b = named_buf(path_b, &["xxx", "yyy", "zzz"]);
let mut buffers = HashMap::new();
buffers.insert(path_a.to_string(), buf_a);
buffers.insert(path_b.to_string(), buf_b);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Self_,
None,
Some(1),
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
assert_eq!(resolved.resolutions.len(), 2);
let res_a = resolved.resolutions.get(path_a).unwrap();
let res_b = resolved.resolutions.get(path_b).unwrap();
assert_eq!(res_a.scope_range.start_line, 1);
assert_eq!(res_b.scope_range.start_line, 1);
}
#[test]
fn cifn_with_selection_range_targets_identifier() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() { 42 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![sym_sel(
"foo",
SymbolKind::Function,
0,
0,
0,
15,
0,
3,
0,
6,
)],
);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Name,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 0);
assert_eq!(target.start_col, 3);
assert_eq!(target.end_col, 6);
}
#[test]
fn cifc_resolves_to_brace_contents() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() { 42 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 0, 15)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 0);
assert_eq!(target.start_col, 10);
assert_eq!(target.end_col, 14);
}
#[test]
fn cifc_multiline_resolves_contents() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {", " 42", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 2, 1)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 1);
assert_eq!(target.start_col, 0);
assert_eq!(target.end_line, 1);
assert_eq!(target.end_col, 6);
}
#[test]
fn cbfs_targets_text_before_function() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["use std::io;", "", "fn foo() { 42 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 2, 0, 2, 15)]);
let q = query(
Action::Change,
Positional::Before,
Scope::Function,
Component::Self_,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 2);
assert_eq!(target.start_col, 0);
assert_eq!(target.end_line, 2);
assert_eq!(target.end_col, 0);
}
#[test]
fn after_function_self_targets_text_after_function() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}", "", "fn bar() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("foo", SymbolKind::Function, 0, 0, 0, 11),
sym("bar", SymbolKind::Function, 2, 0, 2, 11),
],
);
let q = query(
Action::Change,
Positional::After,
Scope::Function,
Component::Self_,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 0);
assert_eq!(target.start_col, 11);
assert_eq!(target.end_line, 0);
assert_eq!(target.end_col, 11);
}
fn sel_range(
sl: usize,
sc: usize,
el: usize,
ec: usize,
parent: Option<SelectionRange>,
) -> SelectionRange {
SelectionRange {
range: SymbolRange {
start_line: sl,
start_col: sc,
end_line: el,
end_col: ec,
},
parent: parent.map(Box::new),
}
}
#[test]
fn variable_scope_selection_range_simple_let() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let x = 42;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 15, Some(block));
let ident = sel_range(1, 8, 1, 9, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 8, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 8)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_line, 1);
assert_eq!(res.scope_range.end_col, 15);
}
#[test]
fn variable_scope_selection_range_value_component() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let name = \"hello\";", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 22, Some(block));
let ident = sel_range(1, 8, 1, 12, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 8, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Value,
None,
None,
Some((1, 8)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 14);
}
#[test]
fn variable_scope_selection_range_const() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["const MAX: usize = 100;"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![]);
let file = sel_range(0, 0, 0, 23, None);
let const_stmt = sel_range(0, 0, 0, 23, Some(file));
let ident = sel_range(0, 6, 0, 9, Some(const_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 0, 6, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((0, 6)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 0);
assert_eq!(res.scope_range.start_col, 0);
assert_eq!(res.scope_range.end_col, 23);
}
#[test]
fn variable_scope_selection_range_multiline() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"fn main() {",
" let v = vec![",
" 1, 2, 3,",
" ];",
"}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 4, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 4, 1, None);
let let_stmt = sel_range(1, 4, 3, 6, Some(block));
let ident = sel_range(1, 8, 1, 9, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 8, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 8)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_line, 3);
assert_eq!(res.scope_range.end_col, 6);
}
#[test]
fn variable_scope_selection_range_cursor_on_keyword() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let params = some_value;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 27, Some(block));
let keyword = sel_range(1, 4, 1, 7, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 5, keyword);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 5)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_line, 1);
assert_eq!(res.scope_range.end_col, 27);
}
#[test]
fn variable_scope_prefers_selection_range_over_narrow_document_symbol() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let asdf = dude();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let var_sym = sym("asdf", SymbolKind::Variable, 1, 8, 1, 12);
let main_fn = sym_with_children("main", SymbolKind::Function, 0, 0, 2, 1, vec![var_sym]);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 21, Some(block));
let ident = sel_range(1, 8, 1, 12, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 9, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Value,
None,
None,
Some((1, 9)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_col, 21);
assert_eq!(res.component_range.start_col, 14);
}
#[test]
fn variable_scope_selection_range_cursor_on_value() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let x = 42;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 15, Some(block));
let literal = sel_range(1, 12, 1, 14, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 12, literal);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Value,
None,
None,
Some((1, 12)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_line, 1);
assert_eq!(res.scope_range.end_col, 15);
}
#[test]
fn variable_name_component_finds_variable_not_function() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let asdf = dude();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let var_sym = sym("asdf", SymbolKind::Variable, 1, 8, 1, 12);
let main_fn = sym_with_children("main", SymbolKind::Function, 0, 0, 2, 1, vec![var_sym]);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 21, Some(block));
let ident = sel_range(1, 8, 1, 12, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 9, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 9)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_line, 1);
assert_eq!(res.component_range.start_col, 8);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn variable_name_component_cursor_on_value_side() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let asdf = dude();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let var_sym = sym("asdf", SymbolKind::Variable, 1, 8, 1, 12);
let main_fn = sym_with_children("main", SymbolKind::Function, 0, 0, 2, 1, vec![var_sym]);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 21, Some(block));
let call = sel_range(1, 15, 1, 21, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 15, call);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 15)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_col, 21);
assert_eq!(res.component_range.start_col, 8);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn variable_name_text_fallback_when_no_symbol() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let asdf = dude();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("main", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 10, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 21, Some(block));
let ident = sel_range(1, 8, 1, 12, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 9, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 9)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 8);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn variable_name_text_fallback_const() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["const MAX: usize = 100;"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![]);
let file = sel_range(0, 0, 0, 23, None);
let const_stmt = sel_range(0, 0, 0, 23, Some(file));
let ident = sel_range(0, 6, 0, 9, Some(const_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 0, 7, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((0, 7)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 6);
assert_eq!(res.component_range.end_col, 9);
}
#[test]
fn variable_name_text_fallback_let_mut() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn f() {", " let mut count = 0;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("f", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
let block = sel_range(0, 7, 2, 1, None);
let let_stmt = sel_range(1, 4, 1, 21, Some(block));
let ident = sel_range(1, 12, 1, 17, Some(let_stmt));
lsp.inject_test_selection_range(PathBuf::from(path), 1, 13, ident);
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 13)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 12);
assert_eq!(res.component_range.end_col, 17);
}
fn real_sel_hierarchy_at_let(line: usize) -> SelectionRange {
let file = sel_range(0, 0, 3, 0, None);
let func = sel_range(0, 0, 2, 1, Some(file));
let block = sel_range(0, 12, 2, 1, Some(func));
let stmt = sel_range(line, 4, line, 23, Some(block));
let keyword = sel_range(line, 4, line, 7, Some(stmt));
sel_range(line, 4, line, 4, Some(keyword))
}
fn real_sel_hierarchy_at_name(line: usize) -> SelectionRange {
let file = sel_range(0, 0, 3, 0, None);
let func = sel_range(0, 0, 2, 1, Some(file));
let block = sel_range(0, 12, 2, 1, Some(func));
let stmt = sel_range(line, 4, line, 23, Some(block));
let ident = sel_range(line, 8, line, 12, Some(stmt));
sel_range(line, 8, line, 8, Some(ident))
}
fn real_sel_hierarchy_at_rhs(line: usize) -> SelectionRange {
let file = sel_range(0, 0, 3, 0, None);
let func = sel_range(0, 0, 2, 1, Some(file));
let block = sel_range(0, 12, 2, 1, Some(func));
let stmt = sel_range(line, 4, line, 23, Some(block));
let call = sel_range(line, 15, line, 22, Some(stmt));
let ident = sel_range(line, 15, line, 20, Some(call));
sel_range(line, 15, line, 15, Some(ident))
}
#[test]
fn real_lsp_variable_scope_cursor_on_let() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 4, real_sel_hierarchy_at_let(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_line, 1);
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_col, 23);
}
#[test]
fn real_lsp_variable_scope_cursor_on_name() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 8, real_sel_hierarchy_at_name(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 8)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_col, 23);
}
#[test]
fn real_lsp_variable_scope_cursor_on_rhs() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 15, real_sel_hierarchy_at_rhs(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Self_,
None,
None,
Some((1, 15)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 4);
assert_eq!(res.scope_range.end_col, 23);
}
#[test]
fn real_lsp_variable_name_cursor_on_let() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 4, real_sel_hierarchy_at_let(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 8);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn real_lsp_variable_name_cursor_on_rhs() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 15, real_sel_hierarchy_at_rhs(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Name,
None,
None,
Some((1, 15)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 8);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn real_lsp_variable_value_cursor_on_let() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 4, real_sel_hierarchy_at_let(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Value,
None,
None,
Some((1, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 14);
assert_eq!(res.component_range.end_col, 23);
}
#[test]
fn real_lsp_variable_value_cursor_on_rhs() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn on_stderr() {", " let cmon = hello();", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let main_fn = sym("on_stderr", SymbolKind::Function, 0, 0, 2, 1);
let mut lsp = mock_lsp(path, vec![main_fn]);
lsp.inject_test_selection_range(PathBuf::from(path), 1, 15, real_sel_hierarchy_at_rhs(1));
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Value,
None,
None,
Some((1, 15)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 14);
assert_eq!(res.component_range.end_col, 23);
}
#[test]
fn find_innermost_delimiter_parens_basic() {
let buffer = buf(&["foo(bar, baz)"]);
let result = find_innermost_delimiter(&buffer, 0, 4, "/buf").unwrap();
assert_eq!(result.start_line, 0);
assert_eq!(result.start_col, 3);
assert_eq!(result.end_line, 0);
assert_eq!(result.end_col, 13);
}
#[test]
fn find_innermost_delimiter_braces() {
let buffer = buf(&["if true { x + 1 }"]);
let result = find_innermost_delimiter(&buffer, 0, 10, "/buf").unwrap();
assert_eq!(result.start_col, 8);
assert_eq!(result.end_col, 17);
}
#[test]
fn find_innermost_delimiter_double_quotes() {
let buffer = buf(&[r#"let s = "hello";"#]);
let result = find_innermost_delimiter(&buffer, 0, 10, "/buf").unwrap();
assert_eq!(result.start_col, 8);
assert_eq!(result.end_col, 15);
}
#[test]
fn find_innermost_delimiter_nested_picks_innermost() {
let buffer = buf(&["foo({ bar })"]);
let result = find_innermost_delimiter(&buffer, 0, 6, "/buf").unwrap();
assert_eq!(result.start_col, 4);
assert_eq!(result.end_col, 11);
}
#[test]
fn find_innermost_delimiter_empty_parens() {
let buffer = buf(&["f()"]);
let result = find_innermost_delimiter(&buffer, 0, 2, "/buf").unwrap();
assert_eq!(result.start_col, 1);
assert_eq!(result.end_col, 3);
}
#[test]
fn find_innermost_delimiter_multi_line_braces() {
let buffer = buf(&["fn f() {", " x", "}"]);
let result = find_innermost_delimiter(&buffer, 1, 4, "/buf").unwrap();
assert_eq!(result.start_line, 0);
assert_eq!(result.start_col, 7);
assert_eq!(result.end_line, 2);
assert_eq!(result.end_col, 1);
}
#[test]
fn find_innermost_delimiter_no_delimiter_errors() {
let buffer = buf(&["abc"]);
let result = find_innermost_delimiter(&buffer, 0, 1, "/buf");
assert!(result.is_err());
}
#[test]
fn find_innermost_delimiter_double_backslash_does_not_escape_quote() {
let buffer = buf(&[r#""a\\"b""#]);
let result = find_innermost_delimiter(&buffer, 0, 2, "/buf").unwrap();
assert_eq!(result.start_col, 0);
assert_eq!(result.end_col, 5);
}
#[test]
fn find_innermost_delimiter_cursor_on_close_paren() {
let buffer = buf(&["foo(bar)"]);
let result = find_innermost_delimiter(&buffer, 0, 7, "/buf").unwrap();
assert_eq!(result.start_col, 3);
assert_eq!(result.end_col, 8);
}
#[test]
fn delimiter_scope_self_returns_full_delimiter_range() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["foo(bar, baz)"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Entire,
Scope::Delimiter,
Component::Self_,
None,
None,
Some((0, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.scope_range.start_col, 3);
assert_eq!(res.scope_range.end_col, 13);
assert_eq!(res.component_range.start_col, 3);
assert_eq!(res.component_range.end_col, 13);
}
#[test]
fn delimiter_scope_contents_shrinks_past_open_delimiter() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["foo(bar, baz)"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Entire,
Scope::Delimiter,
Component::Contents,
None,
None,
Some((0, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 4);
assert_eq!(res.component_range.end_col, 12);
}
#[test]
fn delimiter_inside_self_on_quotes_strips_both_quote_chars() {
let path = "/test/file.rs";
let buffer = named_buf(path, &[r#"let s = "hello";"#]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Inside,
Scope::Delimiter,
Component::Self_,
None,
None,
Some((0, 11)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.target_ranges[0].start_col, 9);
assert_eq!(res.target_ranges[0].end_col, 14);
}
#[test]
fn delimiter_entire_contents_on_braces_excludes_both_braces() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["{ block }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Entire,
Scope::Delimiter,
Component::Contents,
None,
None,
Some((0, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.component_range.start_col, 1);
assert_eq!(res.component_range.end_col, 8);
}
#[test]
fn positional_to_from_cursor_to_component_end() {
let buffer = buf(&["fn foo(x: i32) {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 18,
};
let comp = TextRange {
start_line: 0,
start_col: 6,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::To,
Scope::Function,
Component::Parameters,
None,
None,
Some((0, 2)),
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result[0].start_line, 0);
assert_eq!(result[0].start_col, 2);
assert_eq!(result[0].end_col, 14);
}
#[test]
fn positional_to_requires_cursor_pos() {
let buffer = buf(&["fn foo(x: i32) {}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 18,
};
let comp = TextRange {
start_line: 0,
start_col: 6,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::To,
Scope::Function,
Component::Parameters,
None,
None,
None,
);
assert!(apply_positional(&q, &buffer, &scope, &comp, "/buf").is_err());
}
#[test]
fn cursor_mode_jump_goes_to_edit_mode() {
let range = TextRange {
start_line: 3,
start_col: 7,
end_line: 5,
end_col: 0,
};
let q = query(
Action::Jump,
Positional::Entire,
Scope::Function,
Component::Self_,
None,
None,
None,
);
let (cursor, mode) = resolve_cursor_and_mode(&q, &range);
assert_eq!(cursor, Some(CursorPosition { line: 3, col: 7 }));
assert_eq!(mode, Some(EditorMode::Chord));
}
#[test]
fn jump_entire_function_name_sets_cursor_destination() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {", " x", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![sym_sel("foo", SymbolKind::Function, 0, 0, 2, 1, 0, 3, 0, 6)],
);
let q = query(
Action::Jump,
Positional::Entire,
Scope::Function,
Component::Name,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(
res.cursor_destination,
Some(CursorPosition { line: 0, col: 3 })
);
assert_eq!(res.mode_after, Some(EditorMode::Chord));
}
#[test]
fn jump_does_not_set_replacement() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 1, 1)]);
let q = query(
Action::Jump,
Positional::Entire,
Scope::Function,
Component::Self_,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(res.replacement.is_none());
assert!(res.cursor_destination.is_some());
}
#[test]
fn jump_outside_function_beginning_lands_on_previous_line() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["// preamble line", "fn foo() {", " x", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 1, 0, 3, 1)]);
let q = query(
Action::Jump,
Positional::Outside,
Scope::Function,
Component::Beginning,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let dest = res.cursor_destination.unwrap();
assert_eq!(dest.line, 0);
assert_eq!(dest.col, "// preamble line".chars().count());
assert_eq!(res.mode_after, Some(EditorMode::Chord));
}
#[test]
fn jump_outside_function_end_lands_on_next_line() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"// preamble",
"fn foo() {",
" x",
"}",
"// trailing line",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 1, 0, 3, 1)]);
let q = query(
Action::Jump,
Positional::Outside,
Scope::Function,
Component::End,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let dest = res.cursor_destination.unwrap();
assert_eq!(dest.line, 4);
assert_eq!(dest.col, 0);
assert_eq!(res.mode_after, Some(EditorMode::Chord));
}
#[test]
fn jump_outside_function_beginning_at_buffer_start_clamps_to_origin() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {", " x", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 2, 1)]);
let q = query(
Action::Jump,
Positional::Outside,
Scope::Function,
Component::Beginning,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let dest = res.cursor_destination.unwrap();
assert_eq!(dest.line, 0);
assert_eq!(dest.col, 0);
}
#[test]
fn jump_delimiter_scope_sets_cursor_to_open_delimiter() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["foo(bar, baz)"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Jump,
Positional::Entire,
Scope::Delimiter,
Component::Self_,
None,
None,
Some((0, 4)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(
res.cursor_destination,
Some(CursorPosition { line: 0, col: 3 })
);
assert_eq!(res.mode_after, Some(EditorMode::Chord));
}
#[test]
fn resolve_word_component_entire_cursor_on_word() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Word,
None,
None,
Some((0, 3)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 3);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 8);
}
#[test]
fn resolve_word_component_entire_cursor_on_whitespace_picks_next() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Word,
None,
None,
Some((0, 8)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 9);
assert_eq!(range.end_col, 14);
}
#[test]
fn resolve_word_component_next() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Jump,
Positional::Next,
Scope::Line,
Component::Word,
None,
None,
Some((0, 3)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 9);
assert_eq!(range.end_col, 14);
}
#[test]
fn resolve_word_component_previous() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Jump,
Positional::Previous,
Scope::Line,
Component::Word,
None,
None,
Some((0, 9)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 3);
assert_eq!(range.end_col, 8);
}
#[test]
fn resolve_word_component_first() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Jump,
Positional::First,
Scope::Line,
Component::Word,
None,
None,
Some((0, 9)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 3);
assert_eq!(range.end_col, 8);
}
#[test]
fn resolve_word_component_last() {
let buffer = buf(&[" hello world"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Jump,
Positional::Last,
Scope::Line,
Component::Word,
None,
None,
Some((0, 3)),
);
let range = resolve_word_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 9);
assert_eq!(range.end_col, 14);
}
#[test]
fn resolve_word_component_empty_line_errors() {
let buffer = buf(&[""]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 0,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Line,
Component::Word,
None,
None,
Some((0, 0)),
);
let result = resolve_word_component(&q, &buffer, &scope, "/buf");
assert!(result.is_err(), "empty line should error");
}
#[test]
fn resolve_definition_component_function_with_body() {
let buffer = buf(&["pub fn foo(x: i32) -> bool {", " true", "}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 2,
end_col: 1,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 26);
}
#[test]
fn resolve_definition_component_variable_with_type_and_assignment() {
let buffer = buf(&["let count: i32 = 0;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 19,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 14);
}
#[test]
fn resolve_definition_component_variable_without_assignment() {
let buffer = buf(&["let count: i32;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 15,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 0);
assert_eq!(range.end_col, 14);
}
#[test]
fn resolve_definition_component_variable_without_type() {
let buffer = buf(&["let count = 0;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 14,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Variable,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_col, 0);
assert_eq!(range.end_col, 9);
}
#[test]
fn resolve_definition_component_struct() {
let buffer = buf(&["pub struct Config<T> {", " field: T,", "}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 2,
end_col: 1,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Struct,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 20);
}
#[test]
fn resolve_definition_component_enum_with_struct_scope() {
let buffer = buf(&["enum Status {", " Active,", "}"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 2,
end_col: 1,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Struct,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 11);
}
#[test]
fn resolve_definition_component_trait_method_no_body() {
let buffer = buf(&["fn process(&self) -> Result<()>;"]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 0,
end_col: 32,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 0);
assert_eq!(range.end_col, 31);
}
#[test]
fn resolve_definition_component_multiline_signature() {
let buffer = buf(&[
"pub fn foo(",
" x: i32,",
" y: i32,",
") -> bool {",
" true",
"}",
]);
let scope = TextRange {
start_line: 0,
start_col: 0,
end_line: 5,
end_col: 1,
};
let q = query(
Action::Change,
Positional::Entire,
Scope::Function,
Component::Definition,
None,
None,
None,
);
let range = resolve_definition_component(&q, &buffer, &scope, "/buf").unwrap();
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 3);
assert_eq!(range.end_col, 9);
}
#[test]
fn resolve_list_definition_component_returns_signatures() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"pub fn foo(x: i32) -> bool {",
" true",
"}",
"fn bar() {",
" false",
"}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("foo", SymbolKind::Function, 0, 0, 2, 1),
sym("bar", SymbolKind::Function, 3, 0, 5, 1),
],
);
let q = query(
Action::List,
Positional::Entire,
Scope::Function,
Component::Definition,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 2);
assert!(
res.listed_items[0].val.starts_with("pub fn foo"),
"first item val: {}",
res.listed_items[0].val
);
assert!(
res.listed_items[1].val.starts_with("fn bar"),
"second item val: {}",
res.listed_items[1].val
);
}
#[test]
fn resolve_list_entire_returns_all_functions_in_source_order() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn alpha() {}", "fn beta() {}", "fn gamma() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 0, 0, 0, 13),
sym("beta", SymbolKind::Function, 1, 0, 1, 12),
sym("gamma", SymbolKind::Function, 2, 0, 2, 13),
],
);
let q = query(
Action::List,
Positional::Entire,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 3);
assert_eq!(res.listed_items[0].val, "alpha");
assert_eq!(res.listed_items[1].val, "beta");
assert_eq!(res.listed_items[2].val, "gamma");
assert!(res.listed_items[0].line < res.listed_items[1].line);
assert!(res.listed_items[1].line < res.listed_items[2].line);
}
#[test]
fn resolve_list_after_filter_returns_only_items_after_cursor() {
let path = "/test/file.rs";
let lines: Vec<&str> = vec![
"// line 0",
"// line 1",
"fn alpha() {}",
"// line 3",
"// line 4",
"fn beta() {}",
"// line 6",
"// line 7",
"fn gamma() {}",
];
let buffer = named_buf(path, &lines);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 2, 0, 2, 13),
sym("beta", SymbolKind::Function, 5, 0, 5, 12),
sym("gamma", SymbolKind::Function, 8, 0, 8, 13),
],
);
let q = query(
Action::List,
Positional::After,
Scope::Function,
Component::Name,
None,
None,
Some((5, 5)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 1);
assert_eq!(res.listed_items[0].val, "gamma");
assert_eq!(res.listed_items[0].line, 8);
}
#[test]
fn resolve_list_before_filter_returns_items_before_cursor() {
let path = "/test/file.rs";
let lines: Vec<&str> = vec![
"// line 0",
"// line 1",
"fn alpha() {}",
"// line 3",
"// line 4",
"fn beta() {}",
"// line 6",
"// line 7",
"fn gamma() {}",
];
let buffer = named_buf(path, &lines);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 2, 0, 2, 13),
sym("beta", SymbolKind::Function, 5, 0, 5, 12),
sym("gamma", SymbolKind::Function, 8, 0, 8, 13),
],
);
let q = query(
Action::List,
Positional::Before,
Scope::Function,
Component::Name,
None,
None,
Some((5, 5)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 2);
assert_eq!(res.listed_items[0].val, "alpha");
assert_eq!(res.listed_items[1].val, "beta");
}
#[test]
fn resolve_list_last_filter_returns_only_last_item() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn alpha() {}", "fn beta() {}", "fn gamma() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 0, 0, 0, 13),
sym("beta", SymbolKind::Function, 1, 0, 1, 12),
sym("gamma", SymbolKind::Function, 2, 0, 2, 13),
],
);
let q = query(
Action::List,
Positional::Last,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 1);
assert_eq!(res.listed_items[0].val, "gamma");
}
#[test]
fn resolve_list_first_filter_returns_only_first_item() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn alpha() {}", "fn beta() {}", "fn gamma() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("alpha", SymbolKind::Function, 0, 0, 0, 13),
sym("beta", SymbolKind::Function, 1, 0, 1, 12),
sym("gamma", SymbolKind::Function, 2, 0, 2, 13),
],
);
let q = query(
Action::List,
Positional::First,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 1);
assert_eq!(res.listed_items[0].val, "alpha");
}
#[test]
fn resolve_list_empty_results_when_no_functions() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["let x = 1;", "let y = 2;"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![]);
let q = query(
Action::List,
Positional::Entire,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(res.listed_items.is_empty());
}
#[test]
fn jump_last_function_name_resolves_to_last_function() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}", "fn bar() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym_sel("foo", SymbolKind::Function, 0, 0, 0, 11, 0, 3, 0, 6),
sym_sel("bar", SymbolKind::Function, 1, 0, 1, 11, 1, 3, 1, 6),
],
);
let q = query(
Action::Jump,
Positional::Last,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(
res.cursor_destination,
Some(CursorPosition { line: 1, col: 3 })
);
}
#[test]
fn jump_first_function_name_resolves_to_first_function() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {}", "fn bar() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym_sel("foo", SymbolKind::Function, 0, 0, 0, 11, 0, 3, 0, 6),
sym_sel("bar", SymbolKind::Function, 1, 0, 1, 11, 1, 3, 1, 6),
],
);
let q = query(
Action::Jump,
Positional::First,
Scope::Function,
Component::Name,
None,
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(
res.cursor_destination,
Some(CursorPosition { line: 0, col: 3 })
);
}
#[test]
fn count_3_line_self_selects_exactly_3_lines() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["line 0", "line 1", "line 2", "line 3", "line 4"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Count(3),
Scope::Line,
Component::Self_,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(
res.warnings.is_empty(),
"expected no warnings: {:?}",
res.warnings
);
assert_eq!(res.target_ranges.len(), 1);
let range = res.target_ranges[0];
assert_eq!(range.start_line, 0);
assert_eq!(range.start_col, 0);
assert_eq!(range.end_line, 2);
assert_eq!(range.end_col, 6, "end_col should be char count of 'line 2'");
}
#[test]
fn count_3_line_self_clamps_with_warning_when_fewer_lines() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["line 0", "line 1"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Count(3),
Scope::Line,
Component::Self_,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.warnings.len(), 1);
assert!(
res.warnings[0].contains("2") && res.warnings[0].contains("3"),
"warning should mention 2 found and 3 requested: {}",
res.warnings[0]
);
let range = res.target_ranges[0];
assert_eq!(range.start_line, 0);
assert_eq!(range.end_line, 1);
}
#[test]
fn count_5_line_word_resolves_to_5th_word_boundary() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["one two three four five six"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Jump,
Positional::Count(5),
Scope::Line,
Component::Word,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(
res.warnings.is_empty(),
"expected no warnings: {:?}",
res.warnings
);
assert_eq!(
res.target_ranges[0].end_col, 27,
"should reach the end of the 5th word 'six'"
);
}
#[test]
fn count_5_line_word_clamps_with_warning_when_fewer_words() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["one two three"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Jump,
Positional::Count(5),
Scope::Line,
Component::Word,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.warnings.len(), 1);
assert!(
res.warnings[0].contains("2") && res.warnings[0].contains("5"),
"warning should mention 2 found and 5 requested: {}",
res.warnings[0]
);
assert_eq!(
res.target_ranges[0].end_col, 13,
"should reach end of 'three'"
);
}
#[test]
fn count_3_function_name_returns_3_symbols_after_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"// header",
"fn a() {}",
"fn b() {}",
"fn c() {}",
"fn d() {}",
"fn e() {}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("a", SymbolKind::Function, 1, 0, 1, 9),
sym("b", SymbolKind::Function, 2, 0, 2, 9),
sym("c", SymbolKind::Function, 3, 0, 3, 9),
sym("d", SymbolKind::Function, 4, 0, 4, 9),
sym("e", SymbolKind::Function, 5, 0, 5, 9),
],
);
let q = query(
Action::Change,
Positional::Count(3),
Scope::Function,
Component::Name,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(
res.warnings.is_empty(),
"expected no warnings: {:?}",
res.warnings
);
assert_eq!(
res.scope_range.start_line, 1,
"should start at first function after cursor"
);
assert_eq!(
res.scope_range.end_line, 3,
"should end at 3rd function (line 3)"
);
}
#[test]
fn count_3_function_definition_returns_3_ranges_after_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"// header",
"fn a() {}",
"fn b() {}",
"fn c() {}",
"fn d() {}",
"fn e() {}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("a", SymbolKind::Function, 1, 0, 1, 9),
sym("b", SymbolKind::Function, 2, 0, 2, 9),
sym("c", SymbolKind::Function, 3, 0, 3, 9),
sym("d", SymbolKind::Function, 4, 0, 4, 9),
sym("e", SymbolKind::Function, 5, 0, 5, 9),
],
);
let q = query(
Action::Change,
Positional::Count(3),
Scope::Function,
Component::Definition,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert!(
res.warnings.is_empty(),
"expected no warnings: {:?}",
res.warnings
);
assert_eq!(
res.scope_range.start_line, 1,
"should start at first function after cursor"
);
assert_eq!(
res.scope_range.end_line, 3,
"should end at 3rd function (line 3)"
);
}
#[test]
fn count_positional_requires_cursor_errors_without_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["line 0", "line 1", "line 2"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Change,
Positional::Count(3),
Scope::Line,
Component::Self_,
None,
None,
None,
);
let result = resolve(&q, &buffers, &mut lsp);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("cursor"), "expected 'cursor' in error: {msg}");
}
#[test]
fn list_count_requires_cursor_errors_without_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn a() {}", "fn b() {}", "fn c() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("a", SymbolKind::Function, 0, 0, 0, 9),
sym("b", SymbolKind::Function, 1, 0, 1, 9),
sym("c", SymbolKind::Function, 2, 0, 2, 9),
],
);
let q = query(
Action::List,
Positional::Count(3),
Scope::Function,
Component::Definition,
None,
None,
None,
);
let result = resolve(&q, &buffers, &mut lsp);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("cursor"), "expected 'cursor' in error: {msg}");
}
#[test]
fn list_count_emits_warning_when_clamped() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn a() {}", "fn b() {}", "fn c() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("a", SymbolKind::Function, 0, 0, 0, 9),
sym("b", SymbolKind::Function, 1, 0, 1, 9),
sym("c", SymbolKind::Function, 2, 0, 2, 9),
],
);
let q = query(
Action::List,
Positional::Count(9),
Scope::Function,
Component::Definition,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 2, "should list b and c");
assert_eq!(res.warnings.len(), 1);
assert!(
res.warnings[0].contains("2") && res.warnings[0].contains("9"),
"warning should mention 2 found and 9 requested: {}",
res.warnings[0]
);
}
#[test]
fn list_count_no_warning_when_exact_match() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn a() {}", "fn b() {}", "fn c() {}", "fn d() {}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(
path,
vec![
sym("a", SymbolKind::Function, 0, 0, 0, 9),
sym("b", SymbolKind::Function, 1, 0, 1, 9),
sym("c", SymbolKind::Function, 2, 0, 2, 9),
sym("d", SymbolKind::Function, 3, 0, 3, 9),
],
);
let q = query(
Action::List,
Positional::Count(3),
Scope::Function,
Component::Definition,
None,
None,
Some((0, 0)),
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.listed_items.len(), 3);
assert!(
res.warnings.is_empty(),
"no warning when count matches exactly"
);
}
#[test]
fn append_after_line_self_inserts_at_line_end() {
let buffer = buf(&["line0", "line1", "line2", "line3", "line4"]);
let comp = TextRange {
start_line: 1,
start_col: 0,
end_line: 1,
end_col: 5,
};
let scope = comp;
let q = query(
Action::Append,
Positional::After,
Scope::Line,
Component::Self_,
None,
Some(2),
None,
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result.len(), 1);
let r = result[0];
assert_eq!(
r.start_line, 1,
"point must be on the target line, not buffer tail"
);
assert_eq!(r.start_col, 5);
assert_eq!(r.end_line, 1);
assert_eq!(r.end_col, 5);
}
#[test]
fn prepend_before_line_self_inserts_at_line_start() {
let buffer = buf(&["line0", "line1", "line2", "line3", "line4"]);
let comp = TextRange {
start_line: 2,
start_col: 0,
end_line: 2,
end_col: 5,
};
let scope = comp;
let q = query(
Action::Prepend,
Positional::Before,
Scope::Line,
Component::Self_,
None,
Some(3),
None,
);
let result = apply_positional(&q, &buffer, &scope, &comp, "/buf").unwrap();
assert_eq!(result.len(), 1);
let r = result[0];
assert_eq!(
r.start_line, 2,
"point must be on the target line, not buffer start"
);
assert_eq!(r.start_col, 0);
assert_eq!(r.end_line, 2);
assert_eq!(r.end_col, 0);
}
#[test]
fn cifc_multiline_preserves_brace_lines() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn foo() {", " let x = 1;", " x", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("foo", SymbolKind::Function, 0, 0, 3, 1)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("foo"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 1);
assert_eq!(target.start_col, 0);
assert_eq!(target.end_line, 2);
assert_eq!(target.end_col, 5);
}
#[test]
fn cifc_single_line_unchanged() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn bar() { 99 }"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("bar", SymbolKind::Function, 0, 0, 0, 15)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("bar"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 0);
assert_eq!(target.start_col, 10);
assert_eq!(target.end_line, 0);
assert_eq!(target.end_col, 14);
}
#[test]
fn cifc_indented_closing_brace_excluded() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"impl Foo {",
" fn bar() {",
" let x = 1;",
" x",
" }",
"}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("bar", SymbolKind::Function, 1, 4, 4, 5)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("bar"),
None,
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
let target = res.target_ranges.first().unwrap();
assert_eq!(target.start_line, 2);
assert_eq!(target.start_col, 0);
assert_eq!(target.end_line, 3);
assert_eq!(target.end_col, 9);
}
#[test]
fn cifc_no_value_provides_indented_replacement_and_cursor() {
let path = "/test/file.rs";
let buffer = named_buf(
path,
&[
"impl Foo {",
" fn bar() {",
" let x = 1;",
" }",
"}",
],
);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("bar", SymbolKind::Function, 1, 4, 3, 5)]);
let q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("bar"),
None,
None,
);
let mut q_no_value = q.clone();
q_no_value.args.value = None;
let resolved = resolve(&q_no_value, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement.as_deref(), Some(" "));
let cursor = res.cursor_destination.unwrap();
assert_eq!(cursor.line, 2);
assert_eq!(cursor.col, 8);
}
#[test]
fn cifc_no_value_top_level_fn_uses_four_spaces() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " println!(\"hi\");", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = mock_lsp(path, vec![sym("main", SymbolKind::Function, 0, 0, 2, 1)]);
let mut q = query(
Action::Change,
Positional::Inside,
Scope::Function,
Component::Contents,
Some("main"),
None,
None,
);
q.args.value = None;
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement.as_deref(), Some(" "));
let cursor = res.cursor_destination.unwrap();
assert_eq!(cursor.line, 1);
assert_eq!(cursor.col, 4);
}
#[test]
fn aals_no_value_inherits_target_line_indentation() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let x = 1;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let mut q = query(
Action::Append,
Positional::After,
Scope::Line,
Component::Self_,
None,
Some(1),
None,
);
q.args.value = None;
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement.as_deref(), Some(" "));
let cursor = res.cursor_destination.unwrap();
assert_eq!(cursor.line, 2);
assert_eq!(cursor.col, 8);
}
#[test]
fn pbls_no_value_inherits_target_line_indentation() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let x = 1;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let mut q = query(
Action::Prepend,
Positional::Before,
Scope::Line,
Component::Self_,
None,
Some(1),
None,
);
q.args.value = None;
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement.as_deref(), Some(" "));
let cursor = res.cursor_destination.unwrap();
assert_eq!(cursor.line, 1);
assert_eq!(cursor.col, 8);
}
#[test]
fn aals_no_value_no_indentation_on_unindented_line() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["use std::io;", "fn main() {", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let mut q = query(
Action::Append,
Positional::After,
Scope::Line,
Component::Self_,
None,
Some(1),
None,
);
q.args.value = None;
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement, None);
let cursor = res.cursor_destination.unwrap();
assert_eq!(cursor.line, 2);
assert_eq!(cursor.col, 0);
}
#[test]
fn aals_with_value_does_not_inject_indentation() {
let path = "/test/file.rs";
let buffer = named_buf(path, &["fn main() {", " let x = 1;", "}"]);
let mut buffers = HashMap::new();
buffers.insert(path.to_string(), buffer);
let mut lsp = no_lsp();
let q = query(
Action::Append,
Positional::After,
Scope::Line,
Component::Self_,
None,
Some(2),
None,
);
let resolved = resolve(&q, &buffers, &mut lsp).unwrap();
let res = resolved.resolutions.get(path).unwrap();
assert_eq!(res.replacement, None);
}
}