-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
nvim version from env #207
Changes from all commits
59b105d
dae09c7
f7b353c
bdd9466
7ac7edc
72db928
be53f64
7d26a23
231a717
c5c2d46
0b7aaac
760cc93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* eslint-env jest */ | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import which from 'which'; | ||
import { | ||
getNvimFromEnv, | ||
compareVersions, | ||
parseVersion, | ||
} from './getNvimFromEnv'; | ||
|
||
try { | ||
which.sync('nvim'); | ||
} catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.error( | ||
'A Neovim installation is required to run the tests', | ||
'(see https://github.com/neovim/neovim/wiki/Installing)' | ||
); | ||
process.exit(1); | ||
} | ||
|
||
describe('get_nvim_from_env', () => { | ||
it('Parse version', () => { | ||
const a = parseVersion('0.5.0-dev+1357-g192f89ea1'); | ||
expect(a).toEqual([0, 5, 0, 'dev+1357-g192f89ea1']); | ||
const b = parseVersion('0.5.0-dev+1357-g192f89ea1-Homebrew'); | ||
expect(b).toEqual([0, 5, 0, 'dev+1357-g192f89ea1-Homebrew']); | ||
const c = parseVersion('0.9.1'); | ||
expect(c).toEqual([0, 9, 1, 'zzz']); | ||
}); | ||
|
||
it('Compare versions', () => { | ||
expect(compareVersions('0.3.0', '0.3.0')).toBe(0); | ||
expect(compareVersions('0.3.0', '0.3.1')).toBe(-1); | ||
expect(compareVersions('0.3.1', '0.3.0')).toBe(1); | ||
expect(compareVersions('0.3.0-abc', '0.3.0-dev-420')).toBe(-1); | ||
expect(compareVersions('0.3.0', '0.3.0-dev-658+g06694203e-Homebrew')).toBe( | ||
1 | ||
); | ||
expect(compareVersions('0.3.0-dev-658+g06694203e-Homebrew', '0.3.0')).toBe( | ||
-1 | ||
); | ||
expect( | ||
compareVersions( | ||
'0.3.0-dev-658+g06694203e-Homebrew', | ||
'0.3.0-dev-658+g06694203e-Homebrew' | ||
) | ||
).toBe(0); | ||
expect( | ||
compareVersions( | ||
'0.3.0-dev-658+g06694203e-Homebrew', | ||
'0.3.0-dev-659+g06694203e-Homebrew' | ||
) | ||
).toBe(-1); | ||
expect( | ||
compareVersions( | ||
'0.3.0-dev-659+g06694203e-Homebrew', | ||
'0.3.0-dev-658+g06694203e-Homebrew' | ||
) | ||
).toBe(1); | ||
}); | ||
|
||
it('Get matching nvim from specified min version', () => { | ||
const nvimRes = getNvimFromEnv({ minVersion: '0.3.0' }); | ||
expect(nvimRes).toBeTruthy(); | ||
expect(nvimRes).toEqual({ | ||
matches: expect.any(Array), | ||
unmatchedVersions: expect.any(Array), | ||
errors: expect.any(Array), | ||
}); | ||
expect(nvimRes.matches.length).toBeTruthy(); | ||
expect(nvimRes.matches.length).toBeGreaterThan(0); | ||
expect(nvimRes.matches[0]).toEqual({ | ||
nvimVersion: expect.any(String), | ||
path: expect.any(String), | ||
buildType: expect.any(String), | ||
luaJitVersion: expect.any(String), | ||
}); | ||
expect(nvimRes.unmatchedVersions.length).toEqual(0); | ||
expect(nvimRes.errors.length).toEqual(0); | ||
}); | ||
|
||
it('Get matching nvim without specified min version', () => { | ||
const nvimRes = getNvimFromEnv(); | ||
expect(nvimRes).toBeTruthy(); | ||
expect(nvimRes).toEqual({ | ||
matches: expect.any(Array), | ||
unmatchedVersions: expect.any(Array), | ||
errors: expect.any(Array), | ||
}); | ||
expect(nvimRes.matches.length).toBeTruthy(); | ||
expect(nvimRes.matches.length).toBeGreaterThan(0); | ||
expect(nvimRes.matches[0]).toEqual({ | ||
nvimVersion: expect.any(String), | ||
path: expect.any(String), | ||
buildType: expect.any(String), | ||
luaJitVersion: expect.any(String), | ||
}); | ||
expect(nvimRes.unmatchedVersions.length).toEqual(0); | ||
expect(nvimRes.errors.length).toEqual(0); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,202 @@ | ||
import { execSync } from 'child_process'; | ||
import { join, delimiter } from 'path'; | ||
import { constants, existsSync, accessSync } from 'fs'; | ||
|
||
export type NvimVersion = { | ||
readonly nvimVersion: string; | ||
readonly path: string; | ||
readonly buildType: string; | ||
readonly luaJitVersion: string; | ||
}; | ||
|
||
export type GetNvimFromEnvOptions = { | ||
/** | ||
* The minimum version of nvim to get. This is optional. | ||
* | ||
* - Example: `'0.5.0'` | ||
* - Note: This is inclusive. | ||
* - Note: If this is not set, then there is no minimum version. | ||
*/ | ||
readonly minVersion?: string; | ||
/** | ||
* The order to return the nvim versions in. This is optional. | ||
* | ||
* - `latest_nvim_first` - The latest version of nvim will be first. This is the default. | ||
* - Example: `['0.5.0', '0.4.4', '0.4.3']` | ||
* - Note: This will be slower than `latest_nvim_first`. | ||
* - `keep_path` - The order of the nvim versions will be the same as the order of the paths in the `PATH` environment variable. | ||
* - Example: `['0.4.4', '0.5.0', '0.4.3']` | ||
* - Note: This will be faster than `latest_nvim_first`. | ||
* - this is the default. | ||
*/ | ||
readonly orderBy?: 'latest_nvim_first' | 'keep_path'; | ||
}; | ||
|
||
export type GetNvimFromEnvError = { | ||
/** The executeable path that failed. */ | ||
readonly path: string; | ||
/** The catched error */ | ||
readonly exception: Readonly<Error>; | ||
}; | ||
|
||
export type GetNvimFromEnvResult = { | ||
/** | ||
* A list of nvim versions that match the minimum version. | ||
* This will be empty if no matching versions were found. | ||
* This will be sorted in the order specified by `orderBy`. | ||
*/ | ||
readonly matches: ReadonlyArray<NvimVersion>; | ||
/** | ||
* A list of nvim versions that do not match the minimum version. | ||
* This will be empty if all versions match the minimum version or if no minimum version was specified. | ||
* This will not be sorted (it will be in the order of the paths in the `PATH` environment variable). | ||
*/ | ||
readonly unmatchedVersions: ReadonlyArray<NvimVersion>; | ||
/** | ||
* A list of errors that occurred while trying to get the nvim versions. | ||
* This will be empty if no errors occurred. | ||
* This will not be sorted (it will be in the order of the paths in the `PATH` environment variable). | ||
* Unmatched versions are not treated as errors. | ||
*/ | ||
readonly errors: ReadonlyArray<GetNvimFromEnvError>; | ||
}; | ||
|
||
const versionRegex = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/; | ||
const nvimVersionRegex = /^NVIM\s+v(.+)$/m; | ||
const buildTypeRegex = /^Build\s+type:\s+(.+)$/m; | ||
const luaJitVersionRegex = /^LuaJIT\s+(.+)$/m; | ||
const windows = process.platform === 'win32'; | ||
|
||
export function parseVersion(version: string): (number | string)[] | null { | ||
if (typeof version !== 'string') { | ||
throw new Error('Invalid version format: not a string'); | ||
} | ||
|
||
const match = version.match(versionRegex); | ||
if (match === null) { | ||
return null; | ||
} | ||
|
||
const [, major, minor, patch, prerelease] = match; | ||
const majorNumber = Number(major); | ||
if (Number.isNaN(majorNumber)) { | ||
throw new Error('Invalid version format: major is not a number'); | ||
} | ||
|
||
const minorNumber = Number(minor); | ||
if (Number.isNaN(minorNumber)) { | ||
throw new Error('Invalid version format: minor is not a number'); | ||
} | ||
|
||
const patchNumber = Number(patch); | ||
if (Number.isNaN(patchNumber)) { | ||
throw new Error('Invalid version format: patch is not a number'); | ||
} | ||
|
||
const versionParts: Array<number | string> = [ | ||
majorNumber, | ||
minorNumber, | ||
patchNumber, | ||
]; | ||
if (prerelease !== undefined) { | ||
versionParts.push(prerelease); | ||
} else { | ||
versionParts.push('zzz'); | ||
} | ||
return versionParts; | ||
} | ||
|
||
/** | ||
* Compare two versions. | ||
* @param a - The first version to compare. | ||
* @param b - The second version to compare. | ||
* @returns -1 if a < b, 0 if a == b, 1 if a > b. | ||
* @throws {Error} If the versions are not valid. | ||
* | ||
* Format could be: | ||
* - 0.9.1 | ||
* - 0.10.0-dev-658+g06694203e-Homebrew | ||
*/ | ||
export function compareVersions(a: string, b: string): number { | ||
const versionA = parseVersion(a); | ||
const versionB = parseVersion(b); | ||
const length = Math.min(versionA.length, versionB.length); | ||
|
||
for (let i = 0; i < length; i = i + 1) { | ||
const partA = versionA[i]; | ||
const partB = versionB[i]; | ||
if (partA < partB) { | ||
return -1; | ||
} | ||
if (partA > partB) { | ||
return 1; | ||
} | ||
} | ||
|
||
if (versionB.length > versionA.length) { | ||
return -1; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
/** | ||
* Get the highest matching nvim version from the environment. | ||
*/ | ||
export function getNvimFromEnv( | ||
opt: GetNvimFromEnvOptions = {} | ||
): Readonly<GetNvimFromEnvResult> { | ||
const paths = process.env.PATH.split(delimiter); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. $PATH is likely not sufficient, especially for applications such as https://github.com/vscode-neovim/vscode-neovim which are started in GUI contexts and often (macOS, at least) the user's terminal $PATH is much different than whatever GUI apps are started with. Thus we also need to search "common locations". These are the ones I know of:
Ironically on Windows I think we can depend on $PATH. I assume that winget and scoop setup $PATH, which gets broadcast to GUI programs on Windows. Else we may also need to add those locations. But that could be improved later. |
||
const pathLength = paths.length; | ||
const matches = new Array<NvimVersion>(); | ||
const unmatchedVersions = new Array<NvimVersion>(); | ||
const errors = new Array<GetNvimFromEnvError>(); | ||
for (let i = 0; i !== pathLength; i = i + 1) { | ||
const possibleNvimPath = join(paths[i], windows ? 'nvim.exe' : 'nvim'); | ||
if (existsSync(possibleNvimPath)) { | ||
try { | ||
accessSync(possibleNvimPath, constants.X_OK); | ||
const nvimVersionFull = execSync( | ||
`${possibleNvimPath} --version` | ||
).toString(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
const nvimVersionMatch = nvimVersionRegex.exec(nvimVersionFull); | ||
const buildTypeMatch = buildTypeRegex.exec(nvimVersionFull); | ||
const luaJitVersionMatch = luaJitVersionRegex.exec(nvimVersionFull); | ||
if (nvimVersionMatch && buildTypeMatch && luaJitVersionMatch) { | ||
if ( | ||
'minVersion' in opt && | ||
compareVersions(opt.minVersion, nvimVersionMatch[1]) === 1 | ||
) { | ||
unmatchedVersions.push({ | ||
nvimVersion: nvimVersionMatch[1], | ||
path: possibleNvimPath, | ||
buildType: buildTypeMatch[1], | ||
luaJitVersion: luaJitVersionMatch[1], | ||
}); | ||
} | ||
matches.push({ | ||
nvimVersion: nvimVersionMatch[1], | ||
path: possibleNvimPath, | ||
buildType: buildTypeMatch[1], | ||
luaJitVersion: luaJitVersionMatch[1], | ||
}); | ||
} | ||
} catch (e) { | ||
errors.push({ | ||
path: possibleNvimPath, | ||
exception: e, | ||
} as const); | ||
} | ||
} | ||
} | ||
|
||
if (matches.length > 1 && opt.orderBy === 'latest_nvim_first') { | ||
matches.sort((a, b) => compareVersions(b.nvimVersion, a.nvimVersion)); | ||
} | ||
|
||
return { | ||
matches, | ||
unmatchedVersions, | ||
errors, | ||
} as const; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "find the highest" version logic seems nice, but it might be worth dropping it to simplify the code. Instead, if the first
nvim
found doesn't meet the minimum version, the caller can simply report this to the user (mentioning the foundnvim
path and version).fwiw, Nvim 0.9+ includes
vim.version.parse()
. Could eventually use that to avoid rolling our own semver parsing here.