git-ents.gitmain
⌘K
foforge
editor.rs136 lines · 4.9 KB · rusthistorycomment on this file
1//! Which editor the serving user works in, and deep links into it.
2//!
3//! The web surface is an escalation from the editor, never a destination
4//! of its own (`docs/web-workbench-plan.adoc`), so every code location a
5//! page renders carries an "open in editor" affordance pointing back at
6//! the desk the reader came from (`crate::pages`'s `editor_open`). The
7//! editor is resolved from `ENTS_EDITOR`, then `EDITOR` -- the same
8//! override-then-general order git applies to `GIT_EDITOR`/`EDITOR` --
9//! once per process ([`detected`]); an absent or unrecognized value
10//! renders no affordance at all rather than a dead link.
11//!
12//! Deep links use each editor's own URL scheme (`zed://file/...`,
13//! `vscode://file/...`). Neovim has no scheme of its own, so its links
14//! use the community `nvim://file/...` shape -- they work only where the
15//! reader has registered a handler for it, which is stated here rather
16//! than hidden: the affordance's `title` names the editor the user
17//! configured (`crate::pages`'s `editor_open` renders the shared teal
18//! `↗` pill for every editor alike).
19
20use std::path::Path;
21use std::sync::LazyLock;
22
23/// The editors this crate can deep-link into, resolved by [`detected`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub(crate) enum Editor {
26 /// `zed://file/<path>:<line>` (Zed's own scheme).
27 Zed,
28 /// `vscode://file/<path>:<line>` (VS Code's own scheme; Codium
29 /// installs it too).
30 VsCode,
31 /// `nvim://file/<path>:<line>` -- no official scheme exists, so this
32 /// is the community handler shape (see this module's own doc).
33 Neovim,
34}
35
36impl Editor {
37 /// The editor's display name, the affordance's `title` text.
38 pub(crate) fn label(self) -> &'static str {
39 match self {
40 Self::Zed => "Zed",
41 Self::VsCode => "VS Code",
42 Self::Neovim => "Neovim",
43 }
44 }
45
46 /// The URL-scheme prefix up to and including `file` -- the deep link
47 /// is `<scheme>/<absolute path>[:<line>]`.
48 fn scheme(self) -> &'static str {
49 match self {
50 Self::Zed => "zed://file",
51 Self::VsCode => "vscode://file",
52 Self::Neovim => "nvim://file",
53 }
54 }
55
56 /// The deep link opening `abs` (an absolute path) in this editor,
57 /// at `line` when given.
58 pub(crate) fn deep_link(self, abs: &Path, line: Option<u64>) -> String {
59 let scheme = self.scheme();
60 let path = abs.display();
61 match line {
62 Some(line) => format!("{scheme}{path}:{line}"),
63 None => format!("{scheme}{path}"),
64 }
65 }
66}
67
68/// Parse one editor-variable value: the command's first token's basename,
69/// matched against the launchers each recognized editor ships. `None` for
70/// anything else -- an unknown editor gets no affordance, never a dead
71/// link.
72fn parse(value: &str) -> Option<Editor> {
73 let command = value.split_whitespace().next()?;
74 let name = Path::new(command)
75 .file_name()?
76 .to_string_lossy()
77 .to_lowercase();
78 match name.as_str() {
79 "zed" | "zeditor" => Some(Editor::Zed),
80 "code" | "code-insiders" | "codium" | "vscodium" => Some(Editor::VsCode),
81 "nvim" | "neovim" | "neovide" | "vim" | "gvim" => Some(Editor::Neovim),
82 _ => None,
83 }
84}
85
86/// The serving user's editor: the first of `ENTS_EDITOR`, `EDITOR` that
87/// names one this crate recognizes ([`parse`]), read once per process --
88/// `git ents serve` runs in the user's own environment, so the variables
89/// are the same ones their shell hands every other tool.
90pub(crate) fn detected() -> Option<Editor> {
91 static DETECTED: LazyLock<Option<Editor>> = LazyLock::new(|| {
92 ["ENTS_EDITOR", "EDITOR"]
93 .iter()
94 .filter_map(|name| std::env::var(name).ok())
95 .find_map(|value| parse(&value))
96 });
97 *DETECTED
98}
99
100#[cfg(test)]
101mod tests {
102 use rstest::rstest;
103
104 use super::*;
105
106 #[rstest]
107 #[case::bare_zed("zed", Some(Editor::Zed))]
108 #[case::zed_with_flags("zed --wait", Some(Editor::Zed))]
109 #[case::absolute_code("/usr/local/bin/code -g", Some(Editor::VsCode))]
110 #[case::codium("codium", Some(Editor::VsCode))]
111 #[case::nvim("nvim", Some(Editor::Neovim))]
112 #[case::vim_maps_to_the_neovim_icon("vim", Some(Editor::Neovim))]
113 #[case::neovide("neovide", Some(Editor::Neovim))]
114 #[case::unknown("ed", None)]
115 #[case::empty("", None)]
116 fn parse_matches_the_command_basename(#[case] value: &str, #[case] expected: Option<Editor>) {
117 assert_eq!(parse(value), expected);
118 }
119
120 #[rstest]
121 fn deep_link_carries_scheme_path_and_line() {
122 let abs = Path::new("/repo/src/main.rs");
123 assert_eq!(
124 Editor::Zed.deep_link(abs, Some(21)),
125 "zed://file/repo/src/main.rs:21"
126 );
127 assert_eq!(
128 Editor::VsCode.deep_link(abs, None),
129 "vscode://file/repo/src/main.rs"
130 );
131 assert_eq!(
132 Editor::Neovim.deep_link(abs, Some(3)),
133 "nvim://file/repo/src/main.rs:3"
134 );
135 }
136}