ohmu
·
2026-10-15
1#include "editor.h"
2#include "common/processing.h"
3#include "common/library.h"
4#include "common/strutil.h"
5#include "common/path.h"
6#include "common/binary.h"
7
8#include <stdlib.h>
9
10static bool extractNvimVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
11 if (!ffStrStartsWith(str, "NVIM v")) {
12 return true;
13 }
14 ffStrbufSetS((FFstrbuf*) userdata, str + strlen("NVIM v"));
15 return false;
16}
17
18static bool extractVimVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
19 if (!ffStrStartsWith(str, "VIM - Vi IMproved ")) {
20 return true;
21 }
22 ffStrbufSetS((FFstrbuf*) userdata, str + strlen("VIM - Vi IMproved "));
23 ffStrbufSubstrBeforeFirstC(userdata, ' ');
24 return false;
25}
26
27static bool extractNanoVersionFromBinary(const char* str, [[maybe_unused]] uint32_t len, void* userdata) {
28 if (!ffStrStartsWith(str, "GNU nano ")) {
29 return true;
30 }
31 ffStrbufSetS((FFstrbuf*) userdata, str + strlen("GNU nano "));
32 return false;
33}
34
35// The message is printed to the user verbatim, so it has to name the variable that was wrong:
36// `"Executable not found"` from `ffFindExecutableInPath()` doesn't say what was searched for.
37static const char* notAnExecutable(bool isVisual) {
38 return isVisual ? "$VISUAL does not point to an executable" : "$EDITOR does not point to an executable";
39}
40
41const char* ffDetectEditor(FFEditorResult* result) {
42 bool isVisual = true;
43 ffStrbufSetS(&result->name, getenv("VISUAL"));
44 if (!result->name.length) {
45 isVisual = false;
46 ffStrbufSetS(&result->name, getenv("EDITOR"));
47 }
48
49 if (!result->name.length) {
50 return "$VISUAL or $EDITOR not set";
51 }
52 result->type = isVisual ? "Visual" : "Editor";
53
54 // `$VISUAL`/`$EDITOR` hold a shell command line rather than a bare name, so they routinely
55 // carry arguments: `EDITOR="code -w"`, `EDITOR="vim -f"`. Only the first word names the
56 // program. The whole string used to be handed to `ffFindExecutableInPath()`, which could never
57 // match it, and the module then printed the unparsed value as if it had been resolved.
58 ffStrbufSubstrBefore(&result->name, (uint32_t) strcspn(result->name.chars, " \t\n\v\f\r"));
59
60 if (result->name.length == 0) {
61 return notAnExecutable(isVisual);
62 }
63
64 if (ffIsAbsolutePath(result->name.chars)) {
65 ffStrbufSet(&result->path, &result->name);
66 } else {
67 const char* error = ffFindExecutableInPath(result->name.chars, &result->path);
68 if (error) {
69 // Reporting success here is what made `EDITOR="code -w"` print the raw value: the module
70 // reads `nullptr` as "detection succeeded" and then falls back to printing `name`.
71 return notAnExecutable(isVisual);
72 }
73 }
74
75 {
76 char buf[PATH_MAX + 1];
77 if (!realpath(result->path.chars, buf)) {
78 // An absolute `$VISUAL`/`$EDITOR` is copied into `path` before it is verified, so a path
79 // that does not exist has to be reported here instead of being handed to the caller as
80 // if it had been checked.
81 return notAnExecutable(isVisual);
82 }
83
84 // WIN32: Should we handle scoop shim exe here?
85
86#ifdef __linux__
87 if (!ffStrEndsWith(buf, "/snap"))
88#endif
89 ffStrbufSetS(&result->path, buf);
90 }
91
92 {
93 uint32_t index = ffStrbufLastIndexC(&result->path,
94#ifndef _WIN32
95 '/'
96#else
97 '\\'
98#endif
99 );
100 if (index == result->path.length) {
101 // `realpath()` always yields an absolute path, so neither of these two is reachable in
102 // practice. They used to `return nullptr`, which the module read as a success.
103 return "Failed to determine the executable name";
104 }
105 ffStrbufSetS(&result->exe, &result->path.chars[index + 1]);
106 if (!result->exe.length) {
107 return "Failed to determine the executable name";
108 }
109
110#ifdef _WIN32
111 if (ffStrbufEndsWithS(&result->exe, ".exe")) {
112 ffStrbufSubstrBefore(&result->exe, result->exe.length - 4);
113 }
114#endif
115 }
116
117 if (!instance.config.general.detectVersion) {
118 // Not an error: the editor was found, version probing was just turned off.
119 return nullptr;
120 }
121
122 if (ffStrbufEqualS(&result->exe, "nvim")) {
123 ffBinaryExtractStrings(result->path.chars, extractNvimVersionFromBinary, &result->version, (uint32_t) strlen("NVIM v0.0.0"));
124 } else if (ffStrbufEqualS(&result->exe, "vim") || ffStrbufStartsWithS(&result->exe, "vim.")) {
125 ffBinaryExtractStrings(result->path.chars, extractVimVersionFromBinary, &result->version, (uint32_t) strlen("VIM - Vi IMproved 0.0"));
126 } else if (ffStrbufEqualS(&result->exe, "nano")) {
127 ffBinaryExtractStrings(result->path.chars, extractNanoVersionFromBinary, &result->version, (uint32_t) strlen("GNU nano 0.0"));
128 }
129
130 if (result->version.length > 0) {
131 return nullptr;
132 }
133
134 const char* param = nullptr;
135 if (
136 ffStrbufEqualS(&result->exe, "nano") ||
137 ffStrbufEqualS(&result->exe, "vim") ||
138 ffStrbufStartsWithS(&result->exe, "vim.") || // vim.basic/vim.tiny
139 ffStrbufEqualS(&result->exe, "nvim") ||
140 ffStrbufEqualS(&result->exe, "micro") ||
141 ffStrbufEqualS(&result->exe, "emacs") ||
142 ffStrbufStartsWithS(&result->exe, "emacs-") || // emacs-29.3
143 ffStrbufEqualS(&result->exe, "hx") ||
144 ffStrbufEqualS(&result->exe, "code") ||
145 ffStrbufEqualS(&result->exe, "pluma") ||
146 ffStrbufEqualS(&result->exe, "sublime_text") ||
147 ffStrbufEqualS(&result->exe, "zeditor")) {
148 param = "--version";
149 } else if (
150 ffStrbufEqualS(&result->exe, "kak") ||
151 ffStrbufEqualS(&result->exe, "pico")) {
152 param = "-version";
153 } else if (
154 ffStrbufEqualS(&result->exe, "ne")) {
155 param = "-h";
156 } else {
157 return nullptr;
158 }
159
160 ffProcessAppendStdOut(&result->version, (char* const[]) {
161 result->path.chars,
162 (char*) param,
163 nullptr,
164 });
165
166 if (result->version.length == 0) {
167 return nullptr;
168 }
169
170 ffStrbufSubstrBeforeFirstC(&result->version, '\n');
171 const char* versionStart = strpbrk(result->version.chars, "0123456789");
172 if (versionStart != nullptr) {
173 const char* versionEnd = strpbrk(versionStart, " \t\v\f\r");
174 if (versionEnd != nullptr) {
175 ffStrbufSubstrBefore(&result->version, (uint32_t) (versionEnd - result->version.chars));
176 }
177
178 if (versionStart != result->version.chars) {
179 ffStrbufSubstrAfter(&result->version, (uint32_t) (versionStart - result->version.chars - 1));
180 }
181 }
182
183 return nullptr;
184}