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
6 changes: 3 additions & 3 deletions extras/television-integration/codemark-collections.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ description = "Codemark collections picker - select a collection to view its boo
requirements = ["codemark"]

[source]
command = "cd \"$(dirname $(git rev-parse --path-format=absolute --git-common-dir))\" && codemark collection list --format tv --db .codemark/codemark.db"
command = "codemark collection list --line-format \"{NAME}\t{COUNT}\t{DESCRIPTION}\" --db .codemark/codemark.db"
display = "{split:\t:0} ({split:\t:1} bookmarks) {split:\t:2}"
output = "{split:\t:0}"

[preview]
command = "cd \"$(dirname $(git rev-parse --path-format=absolute --git-common-dir))\" && codemark collection show {split:\t:0} --format table --db .codemark/codemark.db"
command = "codemark collection show {split:\t:0} --format table --db .codemark/codemark.db"
offset = "0"

[ui.preview_panel]
Expand All @@ -21,5 +21,5 @@ enter = "actions:show"

[actions.show]
description = "Show bookmarks in this collection (opens television with codemark channel)"
command = "sh -c 'cd \"$(dirname $(git rev-parse --path-format=absolute --git-common-dir))\" && tv --source-command=\"codemark list --format tv --collection $1 --db .codemark/codemark.db\" codemark' placeholder {split:\t:0}"
command = "sh -c 'tv --source-command=\"codemark list --format tv --collection $1 --db .codemark/codemark.db\" codemark' placeholder {split:\t:0}"
mode = "execute"
42 changes: 42 additions & 0 deletions src/cli/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2160,6 +2160,26 @@ fn handle_collection_list(cli: &Cli, mode: &OutputMode, args: &CollectionListArg
if let Some(ref bookmark_id) = args.bookmark {
let bm = find_bookmark(&db, bookmark_id)?;
let collections = db.list_collections_for_bookmark(&bm.id)?;

// Custom line format for bookmark's collections
if let Some(ref template) = args.line_format {
let mut stdout = io::stdout().lock();
for c in &collections {
let short_id = output::short_id(&c.id);
let line = template
.replace("{ID}", short_id)
.replace("{id}", short_id)
.replace("{NAME}", &c.name)
.replace("{name}", &c.name)
.replace("{DESCRIPTION}", c.description.as_deref().unwrap_or(""))
.replace("{description}", c.description.as_deref().unwrap_or(""))
.replace("{CREATED}", &c.created_at)
.replace("{created}", &c.created_at);
writeln!(stdout, "{line}")?;
}
return Ok(());
}

match mode {
OutputMode::Json => write_json_success(&collections)?,
OutputMode::Table => {
Expand All @@ -2183,6 +2203,28 @@ fn handle_collection_list(cli: &Cli, mode: &OutputMode, args: &CollectionListArg
}
} else {
let collections = db.list_collections()?;

// Custom line format for collections with counts
if let Some(ref template) = args.line_format {
let mut stdout = io::stdout().lock();
for (c, count) in &collections {
let short_id = output::short_id(&c.id);
let line = template
.replace("{ID}", short_id)
.replace("{id}", short_id)
.replace("{NAME}", &c.name)
.replace("{name}", &c.name)
.replace("{COUNT}", &count.to_string())
.replace("{count}", &count.to_string())
.replace("{DESCRIPTION}", c.description.as_deref().unwrap_or(""))
.replace("{description}", c.description.as_deref().unwrap_or(""))
.replace("{CREATED}", &c.created_at)
.replace("{created}", &c.created_at);
writeln!(stdout, "{line}")?;
}
return Ok(());
}

match mode {
OutputMode::Json => write_json_success(&collections)?,
OutputMode::Table => {
Expand Down
4 changes: 4 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,10 @@ pub struct CollectionListArgs {
/// List collections containing this bookmark
#[arg(long)]
pub bookmark: Option<String>,

/// Custom line format template (placeholders: {ID}, {NAME}, {COUNT}, {DESCRIPTION}, {CREATED})
#[arg(long)]
pub line_format: Option<String>,
Comment on lines +496 to +498
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Doc lists {COUNT} but it's only populated when --bookmark is not used.

handle_collection_list only substitutes {COUNT}/{count} in the "list all collections" branch; the --bookmark <id> branch (which iterates list_collections_for_bookmark) has no count and leaves {COUNT} as a literal in the output. Either document the restriction or substitute it with a sensible value (e.g. empty string or per-collection count) in the bookmark branch.

Suggested doc clarification
-    /// Custom line format template (placeholders: {ID}, {NAME}, {COUNT}, {DESCRIPTION}, {CREATED})
+    /// Custom line format template (placeholders: {ID}, {NAME}, {DESCRIPTION}, {CREATED};
+    /// additionally {COUNT} when --bookmark is not used)
     #[arg(long)]
     pub line_format: Option<String>,

}

#[derive(Debug, clap::Args)]
Expand Down
11 changes: 6 additions & 5 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ impl StorageConfig {

impl HealthConfig {
/// Get the auto-archive days threshold (default: 7).
#[allow(dead_code)]
pub fn auto_archive_days(&self) -> u32 {
self.auto_archive_after_days.unwrap_or(7)
}
Expand Down Expand Up @@ -421,7 +422,7 @@ mod tests {
let config = Config::default();
assert_eq!(config.storage.max_resolutions(), 20);
assert_eq!(config.health.auto_archive_days(), 7);
assert_eq!(config.semantic.is_enabled(), true);
assert!(config.semantic.is_enabled());
}

#[test]
Expand Down Expand Up @@ -607,7 +608,7 @@ gui = ["mygui", "code"]
// Verify the config can be loaded
let config = Config::load(&tmp);
assert_eq!(config.storage.max_resolutions(), 20);
assert_eq!(config.semantic.is_enabled(), true);
assert!(config.semantic.is_enabled());

// Second call should not overwrite
let created_again = Config::init_default(&tmp).unwrap();
Expand Down Expand Up @@ -717,7 +718,7 @@ auto_archive_after_days = 14

// Global values preserved
assert_eq!(global.storage.max_resolutions(), 15);
assert_eq!(global.semantic.is_enabled(), true);
assert!(global.semantic.is_enabled());

// Local value applied
assert_eq!(global.health.auto_archive_days(), 14);
Expand Down Expand Up @@ -749,7 +750,7 @@ enabled = true

// Local values (equal to defaults) should win over global
assert_eq!(global.storage.max_resolutions(), 20);
assert_eq!(global.semantic.is_enabled(), true);
assert!(global.semantic.is_enabled());
}

#[test]
Expand All @@ -760,7 +761,7 @@ enabled = true
.expect("Default config template should parse correctly");
assert_eq!(config.storage.max_resolutions(), 20);
assert_eq!(config.health.auto_archive_days(), 7);
assert_eq!(config.semantic.is_enabled(), true);
assert!(config.semantic.is_enabled());
assert_eq!(config.open.default, Some("vim +{LINE_START} {FILE}".to_string()));
assert!(config.open.extensions.contains_key("rs"));
assert!(config.open.extensions.contains_key("swift"));
Expand Down
11 changes: 5 additions & 6 deletions src/engine/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,11 @@ mod tests {

fn find_function_range(tree: &tree_sitter::Tree, source: &str, name: &str) -> (usize, usize) {
fn search(node: tree_sitter::Node, source: &str, name: &str) -> Option<(usize, usize)> {
if node.kind() == "function_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if node.kind() == "function_declaration"
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down
2 changes: 1 addition & 1 deletion src/git/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ mod tests {
fn is_ancestor_true() {
// HEAD~1 should be an ancestor of HEAD in a repo with commits
let ctx = detect_context(Path::new(".")).unwrap();
let head = ctx.head_commit.unwrap();
let _head = ctx.head_commit.unwrap();

// Create a temp repo with known ancestry
let tmp = std::env::temp_dir().join("codemark_test_ancestor_true");
Expand Down
78 changes: 36 additions & 42 deletions src/query/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,12 +526,12 @@ mod tests {

fn find_function_byte_range(tree: &Tree, source: &str, func_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
if node.kind() == "function_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
let text = &source[name_node.byte_range()];
if text == name {
return Some((node.start_byte(), node.end_byte()));
}
if node.kind() == "function_declaration"
&& let Some(name_node) = node.child_by_field_name("name")
{
let text = &source[name_node.byte_range()];
if text == name {
return Some((node.start_byte(), node.end_byte()));
}
}
let mut cursor = node.walk();
Expand Down Expand Up @@ -651,12 +651,11 @@ mod tests {

fn find_rust_function_byte_range(tree: &Tree, source: &str, func_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
if node.kind() == "function_item" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if node.kind() == "function_item"
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down Expand Up @@ -764,12 +763,11 @@ mod tests {
fn find_ts_function_byte_range(tree: &Tree, source: &str, func_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
let kind = node.kind();
if kind == "function_declaration" || kind == "method_definition" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if (kind == "function_declaration" || kind == "method_definition")
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down Expand Up @@ -834,12 +832,11 @@ mod tests {
fn find_py_function_byte_range(tree: &Tree, source: &str, func_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
let kind = node.kind();
if kind == "function_definition" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if kind == "function_definition"
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down Expand Up @@ -917,12 +914,11 @@ mod tests {
fn find_go_function_range(tree: &Tree, source: &str, func_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
let kind = node.kind();
if kind == "function_declaration" || kind == "method_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if (kind == "function_declaration" || kind == "method_declaration")
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down Expand Up @@ -979,12 +975,11 @@ mod tests {

fn find_java_method_range(tree: &Tree, source: &str, method_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
if node.kind() == "method_declaration" || node.kind() == "constructor_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if (node.kind() == "method_declaration" || node.kind() == "constructor_declaration")
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down Expand Up @@ -1042,12 +1037,11 @@ mod tests {

fn find_csharp_method_range(tree: &Tree, source: &str, method_name: &str) -> (usize, usize) {
fn search(node: Node, source: &str, name: &str) -> Option<(usize, usize)> {
if node.kind() == "method_declaration" {
if let Some(name_node) = node.child_by_field_name("name") {
if &source[name_node.byte_range()] == name {
return Some((node.start_byte(), node.end_byte()));
}
}
if node.kind() == "method_declaration"
&& let Some(name_node) = node.child_by_field_name("name")
&& &source[name_node.byte_range()] == name
{
return Some((node.start_byte(), node.end_byte()));
}
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
Expand Down
20 changes: 10 additions & 10 deletions tests/cli_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl Codemark {
let tree = obj.peel_to_tree().unwrap();

// Checkout the tree with force
repo.checkout_tree(&tree.as_object(), Some(git2::build::CheckoutBuilder::new().force()))
repo.checkout_tree(tree.as_object(), Some(git2::build::CheckoutBuilder::new().force()))
.unwrap();

// Update HEAD to point to the commit (detached HEAD state)
Expand Down Expand Up @@ -1416,7 +1416,7 @@ fn git_repo_heal_force_bypasses_ancestry_check() {
// Create initial file and bookmark
let commit_a = cm.commit("test.rs", "fn original() {}", "Commit A");
let json = cm.run_json(&["add", "--file", &cm.file_path("test.rs"), "--range", "1"]);
let id = json["data"]["id"].as_str().unwrap().to_string();
let _id = json["data"]["id"].as_str().unwrap().to_string();
cm.run_json(&["heal"]);

// Make more commits and heal again
Expand Down Expand Up @@ -1512,7 +1512,7 @@ fn git_repo_move_method_then_heal_gets_new_resolution() {
let cm = Codemark::with_git_repo();

// Initial file with function at line 1
let commit_a = cm.commit("test.rs", "fn my_function() {}\nfn other() {}", "Commit A");
let _commit_a = cm.commit("test.rs", "fn my_function() {}\nfn other() {}", "Commit A");

// Create bookmark targeting the function
let json = cm.run_json(&[
Expand Down Expand Up @@ -1589,7 +1589,7 @@ fn git_repo_resolve_fails_when_function_deleted() {
let cm = Codemark::with_git_repo();

// Initial file with function
let commit_a = cm.commit("test.rs", "fn my_function() {}\nfn other() {}", "Commit A");
let _commit_a = cm.commit("test.rs", "fn my_function() {}\nfn other() {}", "Commit A");

// Create bookmark targeting the function
let json = cm.run_json(&[
Expand All @@ -1609,7 +1609,7 @@ fn git_repo_resolve_fails_when_function_deleted() {
assert_eq!(resolve_json["data"]["method"], "exact", "should find exact match initially");

// Delete the function (only keep other function)
let commit_b = cm.commit("test.rs", "fn other() {}", "Commit B");
let _commit_b = cm.commit("test.rs", "fn other() {}", "Commit B");

// Resolve should now fail or fall back to a different method
let resolve_json = cm.run_json(&["resolve", &id[..8]]);
Expand Down Expand Up @@ -1657,7 +1657,7 @@ fn git_repo_bookmark_goes_stale_when_code_completely_changed() {
let cm = Codemark::with_git_repo();

// Initial file with function
let commit_a = cm.commit("test.rs", "fn original_function() { return 42; }", "Commit A");
let _commit_a = cm.commit("test.rs", "fn original_function() { return 42; }", "Commit A");

// Create bookmark targeting the function
let json = cm.run_json(&[
Expand All @@ -1677,7 +1677,7 @@ fn git_repo_bookmark_goes_stale_when_code_completely_changed() {

// Completely remove the function and make the file empty
// This should cause resolution to fail entirely (stale, not drifted)
let commit_b = cm.commit("test.rs", "", "Commit B - empty file");
let _commit_b = cm.commit("test.rs", "", "Commit B - empty file");

// Heal should mark the bookmark as stale (can't find the function at all)
let heal_json = cm.run_json(&["heal"]);
Expand Down Expand Up @@ -1710,7 +1710,7 @@ fn git_repo_resolve_fails_when_file_deleted() {
let cm = Codemark::with_git_repo();

// Initial file with function
let commit_a = cm.commit("test.rs", "fn my_function() {}", "Commit A");
let _commit_a = cm.commit("test.rs", "fn my_function() {}", "Commit A");

// Create bookmark targeting the function
let json = cm.run_json(&[
Expand Down Expand Up @@ -1771,7 +1771,7 @@ fn git_repo_function_renamed_resolve_uses_fallback() {
let cm = Codemark::with_git_repo();

// Initial file with function
let commit_a = cm.commit("test.rs", "fn my_function() {}", "Commit A");
let _commit_a = cm.commit("test.rs", "fn my_function() {}", "Commit A");

// Create bookmark targeting the function
let json = cm.run_json(&[
Expand All @@ -1786,7 +1786,7 @@ fn git_repo_function_renamed_resolve_uses_fallback() {
let id = json["data"]["id"].as_str().unwrap().to_string();

// Rename the function
let commit_b = cm.commit("test.rs", "fn renamed_function() {}", "Commit B - function renamed");
let _commit_b = cm.commit("test.rs", "fn renamed_function() {}", "Commit B - function renamed");

// Resolve should use fallback (relaxed/minimal) or fail
let resolve_json = cm.run_json(&["resolve", &id[..8]]);
Expand Down
Loading