Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/cli/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,34 @@ fn index_markdown_file(
Ok(section_count)
}

/// parent_symbol_id を解決する(2パス方式の2パス目)
fn resolve_parent_symbols(
symbol_store: &SymbolStore,
rel_path: &str,
parser_symbols: &[crate::parser::code::SymbolInfo],
) -> Result<(), IndexError> {
let inserted = symbol_store.find_by_file(rel_path)?;
if inserted.len() != parser_symbols.len() {
return Ok(());
}

// name → id マッピング構築
let name_to_id: std::collections::HashMap<&str, i64> = inserted
.iter()
.filter(|s| s.parent_symbol_id.is_none()) // 親候補のみ(クラス等)
.filter_map(|s| s.id.map(|id| (s.name.as_str(), id)))
.collect();

for (parser_sym, store_sym) in parser_symbols.iter().zip(inserted.iter()) {
if let (Some(parent_name), Some(child_id)) = (&parser_sym.parent, store_sym.id)
&& let Some(&parent_id) = name_to_id.get(parent_name.as_str())
{
symbol_store.update_parent_symbol_id(child_id, parent_id)?;
}
}
Ok(())
}

/// コードファイルのインデックス処理(symbols.db + tantivy)
fn index_code_file(
file_path: &Path,
Expand Down Expand Up @@ -377,6 +405,11 @@ fn index_code_file(
return Err(IndexFileResult::Skipped);
}

// 2パス方式: parent_symbol_id を解決
if let Err(e) = resolve_parent_symbols(symbol_store, rel_path, &result.symbols) {
eprintln!("Warning: parent symbol resolution failed for {rel_path}: {e}");
}

// tantivy: heading=ファイル名, body=全文
let filename = file_path
.file_name()
Expand Down
96 changes: 95 additions & 1 deletion src/cli/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ use std::fmt;
use std::path::Path;

use crate::indexer::reader::{IndexReaderWrapper, ReaderError, SearchFilters, SearchOptions};
use crate::output::{self, OutputError, OutputFormat};
use crate::indexer::symbol_store::{SymbolInfo, SymbolStore, SymbolStoreError};
use crate::output::{self, OutputError, OutputFormat, SymbolSearchResult};

#[derive(Debug)]
pub enum SearchError {
IndexNotFound,
Reader(ReaderError),
Output(OutputError),
SymbolStore(SymbolStoreError),
SymbolDbNotFound,
InvalidArgument(String),
}

impl fmt::Display for SearchError {
Expand All @@ -19,6 +23,14 @@ impl fmt::Display for SearchError {
}
SearchError::Reader(e) => write!(f, "{e}"),
SearchError::Output(e) => write!(f, "{e}"),
SearchError::SymbolStore(e) => write!(f, "{e}"),
SearchError::SymbolDbNotFound => {
write!(
f,
"Symbol database not found. Run `commandindex index` first."
)
}
SearchError::InvalidArgument(msg) => write!(f, "{msg}"),
}
}
}
Expand All @@ -29,6 +41,9 @@ impl std::error::Error for SearchError {
SearchError::IndexNotFound => None,
SearchError::Reader(e) => Some(e),
SearchError::Output(e) => Some(e),
SearchError::SymbolStore(e) => Some(e),
SearchError::SymbolDbNotFound => None,
SearchError::InvalidArgument(_) => None,
}
}
}
Expand All @@ -45,6 +60,12 @@ impl From<OutputError> for SearchError {
}
}

impl From<SymbolStoreError> for SearchError {
fn from(e: SymbolStoreError) -> Self {
SearchError::SymbolStore(e)
}
}

pub fn run(
options: &SearchOptions,
filters: &SearchFilters,
Expand All @@ -65,3 +86,76 @@ pub fn run(
output::format_results(&results, format, &mut handle)?;
Ok(())
}

pub fn run_symbol_search(
symbol_name: &str,
limit: usize,
format: OutputFormat,
) -> Result<(), SearchError> {
if symbol_name.is_empty() {
return Err(SearchError::InvalidArgument(
"Symbol name cannot be empty".to_string(),
));
}
if symbol_name.len() > 256 {
return Err(SearchError::InvalidArgument(
"Symbol name too long (max 256 characters)".to_string(),
));
}

let db_path = crate::indexer::symbol_db_path(Path::new("."));
if !db_path.exists() {
return Err(SearchError::SymbolDbNotFound);
}

let store = SymbolStore::open(&db_path)?;
let symbols = store.find_by_name_like(symbol_name, limit)?;
let results = build_symbol_tree(&store, &symbols)?;

if results.is_empty() {
eprintln!("No symbols found matching '{symbol_name}'");
return Ok(());
}

let stdout = std::io::stdout();
let mut handle = stdout.lock();
output::format_symbol_results(&results, format, &mut handle)?;
Ok(())
}

fn build_symbol_tree(
store: &SymbolStore,
symbols: &[SymbolInfo],
) -> Result<Vec<SymbolSearchResult>, SearchError> {
let mut results = Vec::new();
for sym in symbols {
let children = if let Some(id) = sym.id {
let child_symbols = store.find_children_by_parent_id(id)?;
child_symbols
.iter()
.map(|c| SymbolSearchResult {
name: c.name.clone(),
kind: c.kind.clone(),
file_path: c.file_path.clone(),
line_start: c.line_start,
line_end: c.line_end,
parent_name: Some(sym.name.clone()),
children: Vec::new(),
})
.collect()
} else {
Vec::new()
};

results.push(SymbolSearchResult {
name: sym.name.clone(),
kind: sym.kind.clone(),
file_path: sym.file_path.clone(),
line_start: sym.line_start,
line_end: sym.line_end,
parent_name: None,
children,
});
}
Ok(results)
}
62 changes: 62 additions & 0 deletions src/indexer/symbol_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ impl From<std::io::Error> for SymbolStoreError {
}
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Escape LIKE pattern special characters (`%`, `_`, `\`) for safe use in SQL LIKE queries.
pub fn escape_like_pattern(input: &str) -> String {
let mut result = String::with_capacity(input.len());
for ch in input.chars() {
match ch {
'\\' => result.push_str("\\\\"),
'%' => result.push_str("\\%"),
'_' => result.push_str("\\_"),
other => result.push(other),
}
}
result
}

// ---------------------------------------------------------------------------
// SymbolStore
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -193,6 +211,7 @@ impl SymbolStore {
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
CREATE INDEX IF NOT EXISTS idx_symbols_parent ON symbols(parent_symbol_id);
CREATE INDEX IF NOT EXISTS idx_deps_source ON dependencies(source_file);
CREATE INDEX IF NOT EXISTS idx_deps_target ON dependencies(target_module);",
)?;
Expand Down Expand Up @@ -289,6 +308,49 @@ impl SymbolStore {
Ok(count as u64)
}

/// Find symbols whose name partially matches (LIKE %name%, case-insensitive).
pub fn find_by_name_like(
&self,
name: &str,
limit: usize,
) -> Result<Vec<SymbolInfo>, SymbolStoreError> {
let escaped = escape_like_pattern(name);
let pattern = format!("%{escaped}%");
let mut stmt = self.conn.prepare(
"SELECT id, name, kind, file_path, line_start, line_end, parent_symbol_id, file_hash
FROM symbols WHERE name LIKE ?1 ESCAPE '\\' COLLATE NOCASE
ORDER BY name LIMIT ?2",
)?;
let rows = stmt.query_map(params![pattern, limit as i64], symbol_from_row)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}

/// Find child symbols belonging to a parent symbol.
pub fn find_children_by_parent_id(
&self,
parent_id: i64,
) -> Result<Vec<SymbolInfo>, SymbolStoreError> {
let mut stmt = self.conn.prepare(
"SELECT id, name, kind, file_path, line_start, line_end, parent_symbol_id, file_hash
FROM symbols WHERE parent_symbol_id = ?1 ORDER BY line_start LIMIT 100",
)?;
let rows = stmt.query_map(params![parent_id], symbol_from_row)?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}

/// Update the parent_symbol_id for a given symbol (used by 2-pass parent resolution).
pub fn update_parent_symbol_id(
&self,
symbol_id: i64,
parent_id: i64,
) -> Result<(), SymbolStoreError> {
self.conn.execute(
"UPDATE symbols SET parent_symbol_id = ?1 WHERE id = ?2",
params![parent_id, symbol_id],
)?;
Ok(())
}

/// Find import records whose target module matches exactly.
pub fn find_imports_by_target(
&self,
Expand Down
40 changes: 28 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ enum Commands {
},
/// Search the index
Search {
/// Search query
query: String,
/// Search query (full-text search)
query: Option<String>,
/// Search by symbol name (function, class, method)
#[arg(long, conflicts_with = "query")]
symbol: Option<String>,
/// Output format (human, json, path)
#[arg(long, value_enum, default_value_t = commandindex::output::OutputFormat::Human)]
format: commandindex::output::OutputFormat,
Expand Down Expand Up @@ -93,24 +96,37 @@ fn main() {
},
Commands::Search {
query,
symbol,
format,
tag,
path,
file_type,
heading,
limit,
} => {
let options = commandindex::indexer::reader::SearchOptions {
query,
tag,
heading,
limit: limit.min(1000),
};
let filters = commandindex::indexer::reader::SearchFilters {
path_prefix: path,
file_type,
let result = match (query, symbol) {
(Some(q), None) => {
let options = commandindex::indexer::reader::SearchOptions {
query: q,
tag,
heading,
limit: limit.min(1000),
};
let filters = commandindex::indexer::reader::SearchFilters {
path_prefix: path,
file_type,
};
commandindex::cli::search::run(&options, &filters, format)
}
(None, Some(s)) => {
commandindex::cli::search::run_symbol_search(&s, limit.min(1000), format)
}
(None, None) => Err(commandindex::cli::search::SearchError::InvalidArgument(
"Either <QUERY> or --symbol <NAME> is required".to_string(),
)),
(Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"),
};
match commandindex::cli::search::run(&options, &filters, format) {
match result {
Ok(()) => 0,
Err(e) => {
eprintln!("Error: {e}");
Expand Down
32 changes: 31 additions & 1 deletion src/output/human.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use std::io::Write;
use colored::Colorize;

use crate::indexer::reader::SearchResult;
use crate::output::{OutputError, parse_tags, strip_control_chars, truncate_body};
use crate::output::{
OutputError, SymbolSearchResult, parse_tags, strip_control_chars, truncate_body,
};

/// Human形式で検索結果を出力する
pub fn format_human(results: &[SearchResult], writer: &mut dyn Write) -> Result<(), OutputError> {
Expand Down Expand Up @@ -36,3 +38,31 @@ pub fn format_human(results: &[SearchResult], writer: &mut dyn Write) -> Result<
}
Ok(())
}

/// シンボル検索結果をhuman形式で出力する
pub fn format_symbol_human(
results: &[SymbolSearchResult],
writer: &mut dyn Write,
) -> Result<(), OutputError> {
for (i, result) in results.iter().enumerate() {
if i > 0 {
writeln!(writer)?;
}
let kind = strip_control_chars(&result.kind).to_lowercase();
let name = strip_control_chars(&result.name);
let path = strip_control_chars(&result.file_path);
writeln!(writer, "{} {}", format!("[{kind}]").green(), name.bold())?;
writeln!(writer, " {path}:{}-{}", result.line_start, result.line_end)?;

for child in &result.children {
let child_kind = strip_control_chars(&child.kind).to_lowercase();
let child_name = strip_control_chars(&child.name);
writeln!(
writer,
" [{child_kind}] {child_name} (line {}-{})",
child.line_start, child.line_end
)?;
}
}
Ok(())
}
Loading
Loading