Skip to content

Commit 747bd2b

Browse files
authored
feat: findNvim({ paths: ..., dirs: ...}) #400
Problem: Apps such as https://github.com/vscode-neovim/vscode-neovim use `findNvim` to find Nvim on the user's system, but: - they also support user settings to explicitly set a path. - the app may want to search other directories besides the default ones. Since `findNvim` currently doesn't allow the caller to provide extra paths/directories, consumers such as vscode-neovim have to re-invent their own way to validate those extra paths/directories, which is redundant with what `findNvim` already does. Solution: Introduce `paths: string[]` and `dirs: string[]` options to `findNvim`.
1 parent 78100f6 commit 747bd2b

File tree

2 files changed

+79
-8
lines changed

2 files changed

+79
-8
lines changed

packages/neovim/src/utils/findNvim.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,26 @@
11
/* eslint-env jest */
2+
import { join } from 'node:path';
3+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
24
import { findNvim, exportsForTesting, FindNvimResult } from './findNvim';
35

46
const parseVersion = exportsForTesting.parseVersion;
57
const compareVersions = exportsForTesting.compareVersions;
8+
const normalizePath = exportsForTesting.normalizePath as (p: string) => string;
69

710
describe('findNvim', () => {
11+
const testDir = join(process.cwd(), 'test-dir');
12+
const nvimExecutablePath = normalizePath(
13+
join(testDir, process.platform === 'win32' ? 'nvim.exe' : 'nvim')
14+
);
15+
16+
beforeAll(() => {
17+
mkdirSync(testDir, { recursive: true });
18+
writeFileSync(nvimExecutablePath, 'fake-nvim-executable');
19+
});
20+
21+
afterAll(() => {
22+
rmSync(testDir, { recursive: true, force: true });
23+
});
824
it('parseVersion()', () => {
925
expect(parseVersion('0.5.0-dev+1357-g192f89ea1')).toEqual([
1026
0,
@@ -119,4 +135,30 @@ describe('findNvim', () => {
119135
error: undefined,
120136
});
121137
});
138+
139+
it('searches in additional custom paths', () => {
140+
const customPaths = [
141+
join(process.cwd(), 'package.json'),
142+
'/custom/path/to/nvim',
143+
'/another/custom/path',
144+
].map(normalizePath);
145+
const nvimRes = findNvim({ paths: customPaths });
146+
147+
expect(nvimRes.matches.length).toBeGreaterThanOrEqual(1);
148+
149+
expect(nvimRes.invalid.length).toBe(3);
150+
151+
const invalidPaths = nvimRes.invalid.map(i => i.path);
152+
expect(invalidPaths).toEqual(customPaths);
153+
});
154+
155+
it('searches in additional custom dirs', () => {
156+
const customDirs = [testDir, '/non/existent/dir'].map(normalizePath);
157+
const nvimRes = findNvim({ dirs: customDirs });
158+
159+
expect(nvimRes.matches.length).toBeGreaterThanOrEqual(1);
160+
161+
expect(nvimRes.invalid.length).toBe(1);
162+
expect(nvimRes.invalid[0].path).toBe(nvimExecutablePath);
163+
});
122164
});

packages/neovim/src/utils/findNvim.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,23 @@ export type FindNvimOptions = {
3535
* (Optional) Stop searching after found a valid match
3636
*/
3737
readonly firstMatch?: boolean;
38+
/**
39+
* (Optional) Additional specific file paths to check for Nvim executables.
40+
* These paths will be checked before searching `dirs`.
41+
* Useful for allowing users to specify exact Nvim executable locations.
42+
*
43+
* Example: ['/usr/local/bin/nvim', '/opt/homebrew/bin/nvim']
44+
*/
45+
readonly paths?: string[];
46+
/**
47+
* (Optional) Additional directories to search for Nvim executables.
48+
* These directories will be searched after checking `paths`
49+
* but before searching `$PATH` and other default locations.
50+
* Useful for including non-standard installation directories.
51+
*
52+
* Example: ['/opt/neovim/bin', '/home/user/custom/bin']
53+
*/
54+
readonly dirs?: string[];
3855
};
3956

4057
export type FindNvimResult = {
@@ -117,13 +134,14 @@ function compareVersions(a: string, b: string): number {
117134
return 0;
118135
}
119136

120-
function getPlatformPaths() {
137+
function normalizePath(path: string): string {
138+
return normalize(windows ? path.toLowerCase() : path);
139+
}
140+
141+
function getPlatformSearchDirs(): Set<string> {
121142
const paths = new Set<string>();
122143
const { PATH, USERPROFILE, LOCALAPPDATA, PROGRAMFILES, HOME } = process.env;
123144

124-
const normalizePath = (path: string) =>
125-
normalize(windows ? path.toLowerCase() : path);
126-
127145
PATH?.split(delimiter).forEach(p => paths.add(normalizePath(p)));
128146

129147
// Add common Neovim installation paths not always in the system's PATH.
@@ -147,6 +165,7 @@ function getPlatformPaths() {
147165
paths.add(normalizePath(`${PROGRAMFILES} (x86)/WinGet/Packages`));
148166
}
149167
} else {
168+
// Common paths for Unix-like systems
150169
[
151170
'/usr/local/bin',
152171
'/usr/bin',
@@ -170,15 +189,24 @@ function getPlatformPaths() {
170189
* @param opt.minVersion See {@link FindNvimOptions.minVersion}
171190
* @param opt.orderBy See {@link FindNvimOptions.orderBy}
172191
* @param opt.firstMatch See {@link FindNvimOptions.firstMatch}
192+
* @param opt.paths See {@link FindNvimOptions.paths}
193+
* @param opt.dirs See {@link FindNvimOptions.dirs}
173194
*/
174195
export function findNvim(opt: FindNvimOptions = {}): Readonly<FindNvimResult> {
175-
const paths = getPlatformPaths();
196+
const platformDirs = getPlatformSearchDirs();
197+
const nvimExecutable = windows ? 'nvim.exe' : 'nvim';
198+
const normalizedPathsFromUser = (opt.paths ?? []).map(normalizePath);
199+
200+
const allPaths = new Set<string>([
201+
...normalizedPathsFromUser,
202+
...(opt.dirs ?? []).map(dir => normalizePath(join(dir, nvimExecutable))),
203+
...[...platformDirs].map(dir => join(dir, nvimExecutable)),
204+
]);
176205

177206
const matches = new Array<NvimVersion>();
178207
const invalid = new Array<NvimVersion>();
179-
for (const path of paths) {
180-
const nvimPath = join(path, windows ? 'nvim.exe' : 'nvim');
181-
if (existsSync(nvimPath)) {
208+
for (const nvimPath of allPaths) {
209+
if (existsSync(nvimPath) || normalizedPathsFromUser.includes(nvimPath)) {
182210
try {
183211
accessSync(nvimPath, constants.X_OK);
184212
const nvimVersionFull = execFileSync(nvimPath, [
@@ -245,5 +273,6 @@ if (process.env.NODE_ENV === 'test') {
245273
exportsForTesting = {
246274
parseVersion,
247275
compareVersions,
276+
normalizePath,
248277
};
249278
}

0 commit comments

Comments
 (0)