diff --git a/README.md b/README.md
index 18b28ff..8af8e54 100644
--- a/README.md
+++ b/README.md
@@ -49,28 +49,13 @@ The `find` command is useful when you want to search for code based on a specifi
teamsearch find . -c .github/CODEOWNERS -t "my-team" -p "c(o)+de"
```
-```py
-info: match found
- --> repo/sub/item3.html:2:11
- |
- 2 | const code = {
- | ----
- |
- ::: repo/sub/item3.html:3:15
- |
- 3 | "some-cooode-pattern": "some-value",
- | ------
- |
- ::: repo/sub/item3.html:4:18
- |
- 4 | "another-code-pattern": "some-value",
- | ----
- |
- ::: repo/sub/item3.html:10:40
- |
-10 |
Hello world, a fast way to find code owned by teams
- | ----
- |
+```
+repo/sub/item3.html
+2: const code = {
+3: "some-cooode-pattern": "some-value",
+4: "another-code-pattern": "some-value",
+10: Hello world, a fast way to find code owned by teams
+
info: found 4 matches in 7.918375ms
```
diff --git a/crates/teamsearch/src/lib.rs b/crates/teamsearch/src/lib.rs
index 12f296d..e7e70f7 100644
--- a/crates/teamsearch/src/lib.rs
+++ b/crates/teamsearch/src/lib.rs
@@ -8,18 +8,24 @@ mod crash;
pub(crate) mod version;
use std::{
+ collections::BTreeMap,
panic,
path::{Path, PathBuf},
process::ExitCode,
};
-use annotate_snippets::{Level, Renderer, Snippet};
use anyhow::{Ok, Result, anyhow};
use cli::{FindCommand, LookupCommand, OrphanCommand};
use commands::{find::FindResult, lookup::LookupEntry};
use crash::crash_handler;
use log::info;
-use teamsearch_utils::{logging::ToolLogger, stream::CompilerOutputStream};
+use teamsearch_matcher::Match;
+use teamsearch_utils::{
+ highlight::{Colour, highlight},
+ lines::get_line_range,
+ logging::ToolLogger,
+ stream::CompilerOutputStream,
+};
use teamsearch_workspace::settings::Settings;
#[derive(Copy, Clone)]
@@ -91,6 +97,40 @@ fn resolve_default_files(files: Vec, is_stdin: bool) -> Vec {
}
}
+/// Highlight matches in a line of text.
+fn highlight_line_matches(line_content: &str, matches: &[Match]) -> String {
+ if matches.is_empty() {
+ return line_content.to_string();
+ }
+
+ // Sort matches by start position
+ let mut sorted_matches: Vec<_> = matches.iter().collect();
+ sorted_matches.sort_by_key(|m| m.start);
+
+ let mut result = String::new();
+ let mut last_end = 0;
+
+ for m in sorted_matches {
+ // Add text before match
+ if m.start > last_end {
+ result.push_str(&line_content[last_end..m.start]);
+ }
+
+ // Add highlighted match
+ let matched_text = &line_content[m.start..m.end.min(line_content.len())];
+ result.push_str(&highlight(Colour::Red, matched_text));
+
+ last_end = m.end.min(line_content.len());
+ }
+
+ // Add remaining text after last match
+ if last_end < line_content.len() {
+ result.push_str(&line_content[last_end..]);
+ }
+
+ result
+}
+
fn find(args: FindCommand) -> Result {
let files = resolve_default_files(args.files, false);
@@ -116,28 +156,52 @@ fn find(args: FindCommand) -> Result {
// Print out the results in JSON format.
println!("{}", serde_json::to_string_pretty(&file_matches)?);
} else {
- // Create a new `annotate-snippets` renderer, and then use it to render the
- // produced results.
- let renderer = Renderer::styled();
-
- for result in &file_matches {
+ for (idx, result) in file_matches.iter().enumerate() {
if args.count {
info!("{}: {}", result.path.display(), result.len());
} else {
- let mut message = Level::Info.title("match found");
+ // Group matches by line number, keeping track of all matches on each line.
+ let mut line_matches: BTreeMap)> = BTreeMap::new();
- // Now, construct the reports so that we can emit them to the user.
for m in &result.matches {
- let level = Level::Info;
- message = message.snippet(
- Snippet::source(result.contents.as_str())
- .origin(result.path.as_os_str().to_str().unwrap())
- .fold(true)
- .annotation(level.span(m.start..m.end).label("")),
- );
+ let (line_num, line_start, line_end) =
+ get_line_range(&result.contents, m.start);
+
+ // Get the line content
+ let line_content = result.contents[line_start..line_end].trim_end().to_string();
+
+ // Adjust match positions relative to line start
+ let adjusted_match = Match {
+ start: m.start.saturating_sub(line_start),
+ end: m.end.saturating_sub(line_start),
+ };
+
+ line_matches
+ .entry(line_num)
+ .and_modify(|(_, matches)| {
+ matches.push(adjusted_match);
+ })
+ .or_insert_with(|| (line_content, vec![adjusted_match]));
}
- println!("{}", renderer.render(message))
+ // Print file path followed by all matching lines.
+ if !line_matches.is_empty() {
+ // File path in magenta/pink
+ println!("{}", highlight(Colour::Magenta, result.path.display()));
+
+ for (line_num, (line_content, matches)) in &line_matches {
+ // Highlight matches in the line
+ let highlighted_line = highlight_line_matches(line_content, matches);
+
+ // Line number in bright green, then the highlighted line
+ println!("{}:{}", highlight(Colour::Green, line_num), highlighted_line);
+ }
+
+ // Only print blank line between files, not after the last one.
+ if idx < file_matches.len() - 1 {
+ println!();
+ }
+ }
}
}
diff --git a/crates/teamsearch_utils/src/lib.rs b/crates/teamsearch_utils/src/lib.rs
index cafa7f2..b2790af 100644
--- a/crates/teamsearch_utils/src/lib.rs
+++ b/crates/teamsearch_utils/src/lib.rs
@@ -1,6 +1,7 @@
//! Various `teamsearch` utilities.
pub mod fs;
pub mod highlight;
+pub mod lines;
pub mod logging;
pub mod stream;
pub mod thread_pool;
diff --git a/crates/teamsearch_utils/src/lines.rs b/crates/teamsearch_utils/src/lines.rs
new file mode 100644
index 0000000..5d8b9f1
--- /dev/null
+++ b/crates/teamsearch_utils/src/lines.rs
@@ -0,0 +1,21 @@
+/// Get the line number (1-indexed) and byte range for a given byte position.
+///
+/// Returns a tuple of (line_number, line_start_byte, line_end_byte) where
+/// line_number is 1-indexed.
+pub fn get_line_range(contents: &str, byte_pos: usize) -> (usize, usize, usize) {
+ // Count newlines before the position to get the line number.
+ // Line numbers are 1-indexed, so we add 1.
+ let line_num = contents[..byte_pos].bytes().filter(|&b| b == b'\n').count() + 1;
+
+ // Find the start and end of the line containing this byte position.
+ let line_start = if line_num == 1 {
+ 0
+ } else {
+ contents[..byte_pos].rfind('\n').map(|pos| pos + 1).unwrap_or(0)
+ };
+
+ let line_end =
+ contents[byte_pos..].find('\n').map(|offset| byte_pos + offset).unwrap_or(contents.len());
+
+ (line_num, line_start, line_end)
+}