From 586162b46a958188a30d7118876e6b8508df862e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Anton=20=C3=84lgmyr?= <anton@algmyr.se>
Date: Wed, 6 Mar 2024 23:46:02 +0100
Subject: [PATCH] Add configuration options for node symbols in the graphs.

---
 CHANGELOG.md                  |  2 ++
 cli/src/config-schema.json    |  8 +++++
 cli/src/graphlog.rs           | 27 +++++++++++---
 cli/tests/test_log_command.rs | 68 +++++++++++++++++++++++++++++++++++
 lib/src/settings.rs           | 15 ++++++++
 5 files changed, 115 insertions(+), 5 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ebcb160c3..2870323e97 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 * `ui.default-command` now accepts multiple string arguments, for more complex
   default `jj` commands.
 
+* Graph node symbols are now configurable via `ui.graph.default_node` and `ui.graph.elided_node`.
+
 ### Fixed bugs
 
 ## [0.15.1] - 2024-03-06
diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json
index 29a0b3e552..4ecd168163 100644
--- a/cli/src/config-schema.json
+++ b/cli/src/config-schema.json
@@ -122,6 +122,14 @@
                                 "ascii-large"
                             ],
                             "default": "curved"
+                        },
+                        "default_node": {
+                            "type": "string",
+                            "description": "The symbol used as the default symbol for nodes."
+                        },
+                        "elided_node": {
+                            "type": "string",
+                            "description": "The symbol used for elided nodes."
                         }
                     }
                 },
diff --git a/cli/src/graphlog.rs b/cli/src/graphlog.rs
index 9e9c598397..9f4da5b622 100644
--- a/cli/src/graphlog.rs
+++ b/cli/src/graphlog.rs
@@ -124,16 +124,33 @@ pub fn get_graphlog<'a, K: Clone + Eq + Hash + 'a>(
 ) -> Box<dyn GraphLog<K> + 'a> {
     let builder = GraphRowRenderer::new().output().with_min_row_height(0);
 
+    let (default_node_symbol, elided_node_symbol) = settings.node_symbols();
+
     match settings.graph_style().as_str() {
         "square" => SaplingGraphLog::create(
             builder.build_box_drawing().with_square_glyphs(),
             formatter,
-            "◉",
-            "◌",
+            &default_node_symbol,
+            &elided_node_symbol,
+        ),
+        "ascii" => SaplingGraphLog::create(
+            builder.build_ascii(),
+            formatter,
+            &default_node_symbol,
+            &elided_node_symbol,
+        ),
+        "ascii-large" => SaplingGraphLog::create(
+            builder.build_ascii_large(),
+            formatter,
+            &default_node_symbol,
+            &elided_node_symbol,
         ),
-        "ascii" => SaplingGraphLog::create(builder.build_ascii(), formatter, "o", "."),
-        "ascii-large" => SaplingGraphLog::create(builder.build_ascii_large(), formatter, "o", "."),
         // "curved"
-        _ => SaplingGraphLog::create(builder.build_box_drawing(), formatter, "◉", "◌"),
+        _ => SaplingGraphLog::create(
+            builder.build_box_drawing(),
+            formatter,
+            &default_node_symbol,
+            &elided_node_symbol,
+        ),
     }
 }
diff --git a/cli/tests/test_log_command.rs b/cli/tests/test_log_command.rs
index 18561523ca..f5ce34c262 100644
--- a/cli/tests/test_log_command.rs
+++ b/cli/tests/test_log_command.rs
@@ -1410,3 +1410,71 @@ fn test_elided() {
     ◉
     "###);
 }
+
+#[test]
+fn test_custom_symbols() {
+    // Test that elided commits are shown as synthetic nodes.
+    let test_env = TestEnvironment::default();
+    test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
+    let repo_path = test_env.env_root().join("repo");
+
+    test_env.jj_cmd_ok(&repo_path, &["describe", "-m", "initial"]);
+    test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 1"]);
+    test_env.jj_cmd_ok(&repo_path, &["new", "-m", "main branch 2"]);
+    test_env.jj_cmd_ok(&repo_path, &["new", "@--", "-m", "side branch 1"]);
+    test_env.jj_cmd_ok(&repo_path, &["new", "-m", "side branch 2"]);
+    test_env.jj_cmd_ok(
+        &repo_path,
+        &["new", "-m", "merge", r#"description("main branch 2")"#, "@"],
+    );
+
+    let get_log = |revs: &str| -> String {
+        test_env.jj_cmd_success(
+            &repo_path,
+            &["log", "-T", r#"description ++ "\n""#, "-r", revs],
+        )
+    };
+
+    // Simple test with showing default and elided nodes.
+    test_env.add_config(concat!(
+        "ui.log-synthetic-elided-nodes = true\n",
+        "ui.graph.default_node = '┝'\n",
+        "ui.graph.elided_node = '🮀'",
+    ));
+    insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###"
+    @    merge
+    ├─╮
+    │ ┝  side branch 2
+    │ │
+    │ 🮀  (elided revisions)
+    ┝ │  main branch 2
+    │ │
+    🮀 │  (elided revisions)
+    ├─╯
+    ┝  initial
+    │
+    ~
+    "###);
+
+    // Simple test with showing default and elided nodes, ascii style.
+    test_env.add_config(concat!(
+        "ui.log-synthetic-elided-nodes = true\n",
+        "ui.graph.style = 'ascii'\n",
+        "ui.graph.default_node = '*'\n",
+        "ui.graph.elided_node = ':'",
+    ));
+    insta::assert_snapshot!(get_log("@ | @- | description(initial)"), @r###"
+    @    merge
+    |\
+    | *  side branch 2
+    | |
+    | :  (elided revisions)
+    * |  main branch 2
+    | |
+    : |  (elided revisions)
+    |/
+    *  initial
+    |
+    ~
+    "###);
+}
diff --git a/lib/src/settings.rs b/lib/src/settings.rs
index 34a7bc31e3..af94683535 100644
--- a/lib/src/settings.rs
+++ b/lib/src/settings.rs
@@ -242,6 +242,21 @@ impl UserSettings {
             .unwrap_or_else(|_| "curved".to_string())
     }
 
+    pub fn node_symbols(&self) -> (String, String) {
+        let default_node_symbol = self.config.get_string("ui.graph.default_node");
+        let elided_node_symbol = self.config.get_string("ui.graph.elided_node");
+        match self.graph_style().as_str() {
+            "ascii" | "ascii-large" => (
+                default_node_symbol.unwrap_or("o".to_owned()),
+                elided_node_symbol.unwrap_or(".".to_owned()),
+            ),
+            _ => (
+                default_node_symbol.unwrap_or("◉".to_owned()),
+                elided_node_symbol.unwrap_or("◌".to_owned()),
+            ),
+        }
+    }
+
     pub fn max_new_file_size(&self) -> Result<u64, config::ConfigError> {
         let cfg = self
             .config