first commit

This commit is contained in:
2026-09-08 20:28:54 +08:00
commit 2ada8d3d5a
37380 changed files with 4886169 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017-present, Yuxi (Evan) You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+30
View File
@@ -0,0 +1,30 @@
module.exports = {
atom: 'atom',
Brackets: 'brackets',
'code-insiders': 'code-insiders',
code: 'code',
vscodium: 'vscodium',
codium: 'codium',
cursor: 'cursor',
trae: 'trae',
antigravity: 'antigravity',
emacs: 'emacs',
gvim: 'gvim',
idea: 'idea',
'idea.sh': 'idea',
phpstorm: 'phpstorm',
'phpstorm.sh': 'phpstorm',
pycharm: 'pycharm',
'pycharm.sh': 'pycharm',
rubymine: 'rubymine',
'rubymine.sh': 'rubymine',
sublime_text: 'subl',
vim: 'vim',
webstorm: 'webstorm',
'webstorm.sh': 'webstorm',
goland: 'goland',
'goland.sh': 'goland',
rider: 'rider',
'rider.sh': 'rider',
zed: 'zed',
}
+46
View File
@@ -0,0 +1,46 @@
module.exports = {
'/Applications/Atom.app/Contents/MacOS/Atom': 'atom',
'/Applications/Atom Beta.app/Contents/MacOS/Atom Beta':
'/Applications/Atom Beta.app/Contents/MacOS/Atom Beta',
'/Applications/Brackets.app/Contents/MacOS/Brackets': 'brackets',
'/Applications/Sublime Text.app/Contents/MacOS/Sublime Text':
'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
'/Applications/Sublime Text.app/Contents/MacOS/sublime_text':
'/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl',
'/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2':
'/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl',
'/Applications/Sublime Text Dev.app/Contents/MacOS/Sublime Text':
'/Applications/Sublime Text Dev.app/Contents/SharedSupport/bin/subl',
'/Applications/Visual Studio Code.app/Contents/MacOS/Code': 'code',
'/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code',
'/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders': 'code-insiders',
'/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron': 'code-insiders',
'/Applications/VSCodium.app/Contents/MacOS/Electron': 'codium',
'/Applications/Cursor.app/Contents/MacOS/Cursor': 'cursor',
'/Applications/Trae.app/Contents/MacOS/Electron': 'trae',
'/Applications/Antigravity.app/Contents/MacOS/Electron': 'antigravity',
'/Applications/AppCode.app/Contents/MacOS/appcode':
'/Applications/AppCode.app/Contents/MacOS/appcode',
'/Applications/CLion.app/Contents/MacOS/clion': '/Applications/CLion.app/Contents/MacOS/clion',
'/Applications/IntelliJ IDEA.app/Contents/MacOS/idea':
'/Applications/IntelliJ IDEA.app/Contents/MacOS/idea',
'/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea':
'/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea',
'/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea':
'/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea',
'/Applications/PhpStorm.app/Contents/MacOS/phpstorm':
'/Applications/PhpStorm.app/Contents/MacOS/phpstorm',
'/Applications/PyCharm.app/Contents/MacOS/pycharm':
'/Applications/PyCharm.app/Contents/MacOS/pycharm',
'/Applications/PyCharm CE.app/Contents/MacOS/pycharm':
'/Applications/PyCharm CE.app/Contents/MacOS/pycharm',
'/Applications/RubyMine.app/Contents/MacOS/rubymine':
'/Applications/RubyMine.app/Contents/MacOS/rubymine',
'/Applications/WebStorm.app/Contents/MacOS/webstorm':
'/Applications/WebStorm.app/Contents/MacOS/webstorm',
'/Applications/MacVim.app/Contents/MacOS/MacVim': 'mvim',
'/Applications/GoLand.app/Contents/MacOS/goland':
'/Applications/GoLand.app/Contents/MacOS/goland',
'/Applications/Rider.app/Contents/MacOS/rider': '/Applications/Rider.app/Contents/MacOS/rider',
'/Applications/Zed.app/Contents/MacOS/zed': 'zed',
}
+29
View File
@@ -0,0 +1,29 @@
module.exports = [
'Brackets.exe',
'Code.exe',
'Code - Insiders.exe',
'VSCodium.exe',
'Cursor.exe',
'atom.exe',
'sublime_text.exe',
'notepad++.exe',
'clion.exe',
'clion64.exe',
'idea.exe',
'idea64.exe',
'phpstorm.exe',
'phpstorm64.exe',
'pycharm.exe',
'pycharm64.exe',
'rubymine.exe',
'rubymine64.exe',
'webstorm.exe',
'webstorm64.exe',
'goland.exe',
'goland64.exe',
'rider.exe',
'rider64.exe',
'Trae.exe',
'zed.exe',
'Antigravity.exe',
]
+71
View File
@@ -0,0 +1,71 @@
const path = require('path')
// normalize file/line numbers into command line args for specific editors
module.exports = function getArgumentsForPosition(editor, fileName, lineNumber, columnNumber = 1) {
const editorBasename = path.basename(editor).replace(/\.(exe|cmd|bat)$/i, '')
switch (editorBasename) {
case 'atom':
case 'Atom':
case 'Atom Beta':
case 'subl':
case 'sublime':
case 'sublime_text':
case 'wstorm':
case 'charm':
case 'zed':
return [`${fileName}:${lineNumber}:${columnNumber}`]
case 'notepad++':
return ['-n' + lineNumber, '-c' + columnNumber, fileName]
case 'vim':
case 'mvim':
return [`+call cursor(${lineNumber}, ${columnNumber})`, fileName]
case 'joe':
case 'gvim':
return [`+${lineNumber}`, fileName]
case 'emacs':
case 'emacsclient':
return [`+${lineNumber}:${columnNumber}`, fileName]
case 'rmate':
case 'mate':
case 'mine':
return ['--line', lineNumber, fileName]
case 'code':
case 'Code':
case 'code-insiders':
case 'Code - Insiders':
case 'codium':
case 'trae':
case 'antigravity':
case 'cursor':
case 'vscodium':
case 'VSCodium':
return ['-r', '-g', `${fileName}:${lineNumber}:${columnNumber}`]
case 'appcode':
case 'clion':
case 'clion64':
case 'idea':
case 'idea64':
case 'phpstorm':
case 'phpstorm64':
case 'pycharm':
case 'pycharm64':
case 'rubymine':
case 'rubymine64':
case 'webstorm':
case 'webstorm64':
case 'goland':
case 'goland64':
case 'rider':
case 'rider64':
return ['--line', lineNumber, '--column', columnNumber, fileName]
}
if (process.env.LAUNCH_EDITOR) {
return [fileName, lineNumber, columnNumber]
}
// For all others, drop the lineNumber until we have
// a mapping above, since providing the lineNumber incorrectly
// can result in errors or confusing behavior.
return [fileName]
}
+124
View File
@@ -0,0 +1,124 @@
const assert = require('node:assert/strict')
const { describe, test } = require('node:test')
const getArgumentsForPosition = require('./get-args.js')
describe('getArgumentsForPosition', () => {
describe('editor-specific argument formats', () => {
for (const editor of [
'atom',
'Atom',
'Atom Beta',
'subl',
'sublime',
'sublime_text',
'wstorm',
'charm',
'zed',
]) {
test(`${editor} uses "file:line:column"`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), ['file:10:5'])
})
}
test('notepad++ uses -n/-c flags', () => {
assert.deepEqual(getArgumentsForPosition('notepad++', 'file', 10, 5), ['-n10', '-c5', 'file'])
})
for (const editor of ['vim', 'mvim']) {
test(`${editor} uses +call cursor()`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), [
'+call cursor(10, 5)',
'file',
])
})
}
for (const editor of ['joe', 'gvim']) {
test(`${editor} uses +line`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), ['+10', 'file'])
})
}
for (const editor of ['emacs', 'emacsclient']) {
test(`${editor} uses +line:column`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), ['+10:5', 'file'])
})
}
for (const editor of ['rmate', 'mate', 'mine']) {
test(`${editor} uses --line`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), ['--line', 10, 'file'])
})
}
for (const editor of [
'code',
'Code',
'code-insiders',
'Code - Insiders',
'codium',
'trae',
'antigravity',
'cursor',
'vscodium',
'VSCodium',
]) {
test(`${editor} uses -r -g "file:line:column"`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), ['-r', '-g', 'file:10:5'])
})
}
for (const editor of ['idea', 'idea64', 'webstorm', 'pycharm', 'clion', 'rider']) {
test(`${editor} uses --line/--column`, () => {
assert.deepEqual(getArgumentsForPosition(editor, 'file', 10, 5), [
'--line',
10,
'--column',
5,
'file',
])
})
}
})
test('defaults columnNumber to 1 when omitted', () => {
assert.deepEqual(getArgumentsForPosition('code', 'file', 10), ['-r', '-g', 'file:10:1'])
})
describe('editor resolution via path.basename and extension stripping', () => {
test('resolves a full POSIX path', () => {
assert.deepEqual(getArgumentsForPosition('/usr/local/bin/code', 'file', 10, 5), [
'-r',
'-g',
'file:10:5',
])
})
if (process.platform === 'win32') {
test('resolves a full Windows path with .exe', () => {
assert.deepEqual(getArgumentsForPosition('C:\\path\\Code.exe', 'file', 10, 5), [
'-r',
'-g',
'file:10:5',
])
})
test('resolves notepad++ from a full path with .exe', () => {
assert.deepEqual(getArgumentsForPosition('C:\\tools\\notepad++.exe', 'file', 10, 5), [
'-n10',
'-c5',
'file',
])
})
}
test('strips .cmd extension case-insensitively', () => {
assert.deepEqual(getArgumentsForPosition('code.CMD', 'file', 10, 5), [
'-r',
'-g',
'file:10:5',
])
})
})
})
+139
View File
@@ -0,0 +1,139 @@
const path = require('path')
const shellQuote = require('shell-quote')
const childProcess = require('child_process')
// Map from full process name to binary that starts the process
// We can't just re-use full process name, because it will spawn a new instance
// of the app every time
const COMMON_EDITORS_MACOS = require('./editor-info/macos')
const COMMON_EDITORS_LINUX = require('./editor-info/linux')
const COMMON_EDITORS_WIN = require('./editor-info/windows')
function getEditorFromMacProcesses(output) {
const processNames = Object.keys(COMMON_EDITORS_MACOS)
const processList = output.split('\n')
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
// Find editor by exact match.
if (processList.includes(processName)) {
return COMMON_EDITORS_MACOS[processName]
}
const processNameWithoutApplications = processName.replace('/Applications', '')
// Find editor installation not in /Applications.
if (output.indexOf(processNameWithoutApplications) !== -1) {
// Use the CLI command if one is specified
if (processName !== COMMON_EDITORS_MACOS[processName]) {
return COMMON_EDITORS_MACOS[processName]
}
// Use a partial match to find the running process path. If one is found, use the
// existing path since it can be running from anywhere.
const runningProcess = processList.find((procName) =>
procName.endsWith(processNameWithoutApplications),
)
if (runningProcess !== undefined) {
return runningProcess
}
}
}
return undefined
}
function getEditorFromWindowsProcesses(output) {
const runningProcesses = output.split('\r\n')
for (let i = 0; i < runningProcesses.length; i++) {
const fullProcessPath = runningProcesses[i].trim()
const shortProcessName = path.win32.basename(fullProcessPath)
if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return fullProcessPath
}
}
return undefined
}
function getEditorFromLinuxProcesses(output) {
const processNames = Object.keys(COMMON_EDITORS_LINUX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return COMMON_EDITORS_LINUX[processName]
}
}
return undefined
}
function guessEditor(specifiedEditor) {
if (specifiedEditor) {
return shellQuote.parse(specifiedEditor)
}
if (process.env.LAUNCH_EDITOR) {
return [process.env.LAUNCH_EDITOR]
}
if (process.versions.webcontainer) {
return [process.env.EDITOR || 'code']
}
// We can find out which editor is currently running by:
// `ps x` on macOS and Linux
// `Get-Process` on Windows
try {
if (process.platform === 'darwin') {
const output = childProcess
.execSync('ps x -o comm=', {
stdio: ['pipe', 'pipe', 'ignore'],
})
.toString()
const editor = getEditorFromMacProcesses(output)
if (editor !== undefined) {
return [editor]
}
} else if (process.platform === 'win32') {
const output = childProcess
.execSync(
'powershell -NoProfile -Command "' +
'[Console]::OutputEncoding=[Text.Encoding]::UTF8;' +
'Get-CimInstance -Query \\"select executablepath from win32_process where executablepath is not null\\" | % { $_.ExecutablePath }' +
'"',
{
stdio: ['pipe', 'pipe', 'ignore'],
},
)
.toString()
const editor = getEditorFromWindowsProcesses(output)
if (editor !== undefined) {
return [editor]
}
} else if (process.platform === 'linux') {
// --no-heading No header line
// x List all processes owned by you
// -o comm Need only names column
const output = childProcess
.execSync('ps x --no-heading -o comm --sort=comm', {
stdio: ['pipe', 'pipe', 'ignore'],
})
.toString()
const editor = getEditorFromLinuxProcesses(output)
if (editor !== undefined) {
return [editor]
}
}
} catch (ignoreError) {
// Ignore...
}
// Last resort, use old skool env vars
if (process.env.VISUAL) {
return [process.env.VISUAL]
} else if (process.env.EDITOR) {
return [process.env.EDITOR]
}
return [null]
}
module.exports = guessEditor
module.exports.getEditorFromMacProcesses = getEditorFromMacProcesses
module.exports.getEditorFromWindowsProcesses = getEditorFromWindowsProcesses
module.exports.getEditorFromLinuxProcesses = getEditorFromLinuxProcesses
+72
View File
@@ -0,0 +1,72 @@
const assert = require('node:assert/strict')
const { describe, test } = require('node:test')
const guessEditor = require('./guess.js')
const { getEditorFromMacProcesses, getEditorFromWindowsProcesses, getEditorFromLinuxProcesses } =
guessEditor
describe('getEditorFromMacProcesses', () => {
test('maps an exact process path to its CLI command', () => {
const output = [
'/sbin/launchd',
'/Applications/Visual Studio Code.app/Contents/MacOS/Code',
'/usr/sbin/cfprefsd',
].join('\n')
assert.equal(getEditorFromMacProcesses(output), 'code')
})
test('uses the CLI command for an install outside /Applications', () => {
// `Atom` maps to the short command `atom`, so even when running from a
// custom location we should return that command.
const output = ['/sbin/launchd', '/Users/me/dev/Atom.app/Contents/MacOS/Atom'].join('\n')
assert.equal(getEditorFromMacProcesses(output), 'atom')
})
test('returns the running process path when the map points at itself', () => {
// `CLion` maps to its own full path, so for an install outside
// /Applications we should return the actual running path.
const runningPath = '/Users/me/dev/CLion.app/Contents/MacOS/clion'
const output = ['/sbin/launchd', runningPath].join('\n')
assert.equal(getEditorFromMacProcesses(output), runningPath)
})
test('returns undefined when no editor is running', () => {
const output = ['/sbin/launchd', '/usr/sbin/cfprefsd'].join('\n')
assert.equal(getEditorFromMacProcesses(output), undefined)
})
})
describe('getEditorFromWindowsProcesses', () => {
test('returns the full executable path of a known editor', () => {
const codePath = 'C:\\Program Files\\Microsoft VS Code\\Code.exe'
const output = ['C:\\Windows\\System32\\svchost.exe', codePath].join('\r\n')
assert.equal(getEditorFromWindowsProcesses(output), codePath)
})
test('returns undefined when no editor is running', () => {
const output = ['C:\\Windows\\System32\\svchost.exe', 'C:\\Windows\\explorer.exe'].join('\r\n')
assert.equal(getEditorFromWindowsProcesses(output), undefined)
})
})
describe('getEditorFromLinuxProcesses', () => {
test('maps a process name to its CLI command', () => {
const output = ['systemd', 'code', 'bash'].join('\n')
assert.equal(getEditorFromLinuxProcesses(output), 'code')
})
test('maps sublime_text to subl', () => {
const output = ['systemd', 'sublime_text'].join('\n')
assert.equal(getEditorFromLinuxProcesses(output), 'subl')
})
test('matches code-insiders before code due to key order', () => {
const output = ['systemd', 'code-insiders'].join('\n')
assert.equal(getEditorFromLinuxProcesses(output), 'code-insiders')
})
test('returns undefined when no editor is running', () => {
const output = ['systemd', 'bash', 'sshd'].join('\n')
assert.equal(getEditorFromLinuxProcesses(output), undefined)
})
})
+27
View File
@@ -0,0 +1,27 @@
type ErrorCallback = (fileName: string, errorMessage: string | null) => void
/**
* Launch an editor to open a file at a specific line and column.
*
* @param file File path with optional line and column numbers (e.g.
* "/path/to/file.js:10:2").
* @param onErrorCallback Optional callback for handling errors.
*/
declare function launchEditor(file: string, onErrorCallback?: ErrorCallback): void
/**
* Launch an editor to open a file at a specific line and column.
*
* @param file File path with optional line and column numbers (e.g.
* "/path/to/file.js:10:2").
* @param specifiedEditor Optional editor command or path to use. Will be
* parsed using `shell-quote`.
* @param onErrorCallback Optional callback for handling errors.
*/
declare function launchEditor(
file: string,
specifiedEditor?: string,
onErrorCallback?: ErrorCallback,
): void
export = launchEditor
+191
View File
@@ -0,0 +1,191 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file at
* https://github.com/facebookincubator/create-react-app/blob/master/LICENSE
*
* Modified by Yuxi Evan You
*/
const fs = require('fs')
const os = require('os')
const path = require('path')
const colors = require('picocolors')
const childProcess = require('child_process')
const guessEditor = require('./guess')
const getArgumentsForPosition = require('./get-args')
function wrapErrorCallback(cb) {
return (fileName, errorMessage) => {
console.log()
console.log(colors.red('Could not open ' + path.basename(fileName) + ' in the editor.'))
if (errorMessage) {
if (errorMessage[errorMessage.length - 1] !== '.') {
errorMessage += '.'
}
console.log(colors.red('The editor process exited with an error: ' + errorMessage))
}
console.log()
if (cb) cb(fileName, errorMessage)
}
}
function isTerminalEditor(editor) {
switch (editor) {
case 'vim':
case 'emacs':
case 'nano':
return true
}
return false
}
const positionRE = /:(\d+)(:(\d+))?$/
function parseFile(file) {
// support `file://` protocol
if (file.startsWith('file://')) {
file = require('url').fileURLToPath(file)
}
const fileName = file.replace(positionRE, '')
const match = file.match(positionRE)
const lineNumber = match && match[1]
const columnNumber = match && match[3]
return {
fileName,
lineNumber,
columnNumber,
}
}
let currentChildProcess = null
function launchEditor(file, specifiedEditor, onErrorCallback) {
const parsed = parseFile(file)
let { fileName } = parsed
const { lineNumber, columnNumber } = parsed
if (process.platform === 'win32' && path.resolve(fileName).startsWith('\\\\')) {
return onErrorCallback(
fileName,
'UNC paths are not supported on Windows to avoid security issues. ' +
'See https://github.com/vitejs/launch-editor/tree/main/packages/launch-editor#unc-paths-on-windows for details.',
)
}
if (!fs.existsSync(fileName)) {
return
}
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
onErrorCallback = wrapErrorCallback(onErrorCallback)
const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) {
onErrorCallback(fileName, null)
return
}
if (
process.platform === 'linux' &&
fileName.startsWith('/mnt/') &&
/Microsoft/i.test(os.release())
) {
// Assume WSL / "Bash on Ubuntu on Windows" is being used, and
// that the file exists on the Windows file system.
// `os.release()` is "4.4.0-43-Microsoft" in the current release
// build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
// When a Windows editor is specified, interop functionality can
// handle the path translation, but only if a relative path is used.
fileName = path.relative('', fileName)
}
if (lineNumber) {
const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
args.push.apply(args, extraArgs)
} else {
args.push(fileName)
}
if (currentChildProcess && isTerminalEditor(editor)) {
// There's an existing editor process already and it's attached
// to the terminal, so go kill it. Otherwise two separate editor
// instances attach to the stdin/stdout which gets confusing.
currentChildProcess.kill('SIGKILL')
}
if (process.platform === 'win32') {
// On Windows, we need to use `exec` with the `shell: true` option,
// and some more sanitization is required.
// However, CMD.exe on Windows is vulnerable to RCE attacks given a file name of the
// form "C:\Users\myusername\Downloads\& curl 172.21.93.52".
// `create-react-app` used a safe file name pattern to validate user-provided file names:
// - https://github.com/facebook/create-react-app/pull/4866
// - https://github.com/facebook/create-react-app/pull/5431
// But that's not a viable solution for this package because
// it's depended on by so many meta frameworks that heavily rely on
// special characters in file names for filesystem-based routing.
// We need to at least:
// - Support `+` because it's used in SvelteKit and Vike
// - Support `$` because it's used in Remix
// - Support `(` and `)` because they are used in Analog, SolidStart, and Vike
// - Support `@` because it's used in Vike
// - Support `[` and `]` because they are widely used for [slug]
// So here we choose to use `^` to escape special characters instead.
// According to https://ss64.com/nt/syntax-esc.html,
// we can use `^` to escape `&`, `<`, `>`, `|`, `%`, and `^`
// I'm not sure if we have to escape all of these, but let's do it anyway
function escapeCmdArgs(cmdArgs) {
return cmdArgs.replace(/([&|<>,;=^])/g, '^$1')
}
// Need to double quote the editor path in case it contains spaces;
// If the fileName contains spaces, we also need to double quote it in the arguments
// However, there's a case that it's concatenated with line number and column number
// which is separated by `:`. We need to double quote the whole string in this case.
// Also, if the string contains the escape character `^`, it needs to be quoted, too.
function doubleQuoteIfNeeded(str) {
if (str.includes('^')) {
// If a string includes an escaped character, not only does it need to be quoted,
// but the quotes need to be escaped too.
return `^"${str}^"`
} else if (str.includes(' ')) {
return `"${str}"`
}
return str
}
const launchCommand = [editor, ...args.map(escapeCmdArgs)].map(doubleQuoteIfNeeded).join(' ')
currentChildProcess = childProcess.exec(launchCommand, {
stdio: 'inherit',
shell: true,
})
} else {
currentChildProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}
currentChildProcess.on('exit', function (errorCode) {
currentChildProcess = null
if (errorCode) {
onErrorCallback(fileName, '(code ' + errorCode + ')')
}
})
currentChildProcess.on('error', function (error) {
let { code, message } = error
if ('ENOENT' === code) {
message = `${message} ('${editor}' command does not exist in 'PATH')`
}
onErrorCallback(fileName, message)
})
}
module.exports = launchEditor
+50
View File
@@ -0,0 +1,50 @@
const assert = require('node:assert/strict')
const { describe, test, mock } = require('node:test')
const launchEditor = require('./index.js')
const UNC_ERROR = 'UNC paths are not supported on Windows to avoid security issues.'
describe('launchEditor UNC path guard', () => {
if (process.platform === 'win32') {
test('rejects UNC paths on Windows via the error callback', () => {
const onError = mock.fn()
launchEditor('\\\\server\\share\\file.js', 'vim', onError)
assert.equal(onError.mock.callCount(), 1)
const [fileName, message] = onError.mock.calls[0].arguments
assert.equal(fileName, '\\\\server\\share\\file.js')
assert.ok(message.includes(UNC_ERROR))
})
test('strips the position suffix before reporting the rejected UNC path', () => {
const onError = mock.fn()
launchEditor('\\\\server\\share\\file.js:10:5', 'vim', onError)
assert.equal(onError.mock.callCount(), 1)
const [fileName, message] = onError.mock.calls[0].arguments
assert.equal(fileName, '\\\\server\\share\\file.js')
assert.ok(message.includes(UNC_ERROR))
})
test('does not treat a normal absolute Windows path as UNC', () => {
const onError = mock.fn()
// Non-existent file: without the UNC guard firing, launchEditor returns
// silently at the `fs.existsSync` check without invoking the callback.
launchEditor('C:\\Users\\me\\does-not-exist-xyz.js', 'vim', onError)
assert.equal(onError.mock.callCount(), 0)
})
} else {
test('does not apply the UNC guard on non-Windows platforms', () => {
const onError = mock.fn()
launchEditor('\\\\server\\share\\does-not-exist-xyz.js', 'vim', onError)
assert.equal(onError.mock.callCount(), 0)
})
}
})
+27
View File
@@ -0,0 +1,27 @@
{
"name": "launch-editor",
"version": "2.14.1",
"description": "launch editor from node.js",
"keywords": [
"editor",
"launch"
],
"homepage": "https://github.com/vitejs/launch-editor#readme",
"bugs": {
"url": "https://github.com/vitejs/launch-editor/issues"
},
"license": "MIT",
"author": "Evan You",
"repository": {
"type": "git",
"url": "git+https://github.com/vitejs/launch-editor.git"
},
"main": "index.js",
"dependencies": {
"picocolors": "^1.1.1",
"shell-quote": "^1.8.4"
},
"scripts": {
"test": "node --test --experimental-test-module-mocks"
}
}