├── .npmrc ├── .gitattributes ├── .gitignore ├── example.js ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── index.test-d.ts ├── license ├── package.json ├── index.d.ts ├── readme.md ├── index.js └── test.js /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import openEditor from './index.js'; 2 | 3 | openEditor([ 4 | 'index.js:5:5', 5 | 'package.json:10:10', 6 | ]); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 24 14 | - 20 15 | steps: 16 | - uses: actions/checkout@v5 17 | - uses: actions/setup-node@v6 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import openEditor, {getEditorInfo, type EditorInfo} from './index.js'; 3 | 4 | void openEditor([ 5 | 'unicorn.js:5:3', 6 | { 7 | file: 'readme.md', 8 | line: 10, 9 | column: 2, 10 | }, 11 | ]); 12 | 13 | void openEditor( 14 | [ 15 | 'unicorn.js:5:3', 16 | { 17 | file: 'readme.md', 18 | line: 10, 19 | column: 2, 20 | }, 21 | ], 22 | {editor: 'vi'}, 23 | ); 24 | 25 | expectType(getEditorInfo([ 26 | 'unicorn.js:5:3', 27 | { 28 | file: 'readme.md', 29 | line: 10, 30 | column: 2, 31 | }, 32 | new URL('file://path/to/file'), 33 | { 34 | file: new URL('file://path/to/file'), 35 | }, 36 | ])); 37 | 38 | expectType(getEditorInfo( 39 | [ 40 | 'unicorn.js:5:3', 41 | { 42 | file: 'readme.md', 43 | line: 10, 44 | column: 2, 45 | }, 46 | ], 47 | {editor: 'vi'}, 48 | )); 49 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "open-editor", 3 | "version": "6.0.0", 4 | "description": "Open files in your editor at a specific line and column", 5 | "license": "MIT", 6 | "repository": "sindresorhus/open-editor", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=20" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "index.js", 27 | "index.d.ts" 28 | ], 29 | "keywords": [ 30 | "open", 31 | "editor", 32 | "launch", 33 | "files", 34 | "file", 35 | "line", 36 | "column", 37 | "position", 38 | "path", 39 | "filepath", 40 | "editors", 41 | "start", 42 | "app", 43 | "sublime", 44 | "atom", 45 | "vscode", 46 | "webstorm", 47 | "textmate", 48 | "vim", 49 | "neovim", 50 | "intellij", 51 | "emacs", 52 | "nano" 53 | ], 54 | "dependencies": { 55 | "env-editor": "^1.3.0", 56 | "execa": "^9.6.0", 57 | "line-column-path": "^4.0.0", 58 | "open": "^11.0.0" 59 | }, 60 | "devDependencies": { 61 | "ava": "^6.4.1", 62 | "tsd": "^0.33.0", 63 | "xo": "^1.2.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {type PathLike} from 'line-column-path'; 2 | 3 | export type Options = { 4 | /** 5 | The name, command, or binary path of the editor. 6 | 7 | Default: [Auto-detected](https://github.com/sindresorhus/env-editor). 8 | 9 | __Only use this option if you really have to.__ Can be useful if you want to force a specific editor or implement your own auto-detection. 10 | */ 11 | readonly editor?: string; 12 | 13 | /** 14 | Wait until the editor is closed. 15 | 16 | @default false 17 | 18 | @example 19 | ``` 20 | import openEditor from 'open-editor'; 21 | 22 | await openEditor(['unicorn.js:5:3'], {wait: true}); 23 | console.log('File was closed'); 24 | ``` 25 | */ 26 | readonly wait?: boolean; 27 | }; 28 | 29 | export type EditorInfo = { 30 | /** 31 | THe editor binary name. 32 | */ 33 | readonly binary: string; 34 | 35 | /** 36 | The arguments provided to the editor binary. 37 | */ 38 | readonly arguments: string[]; 39 | 40 | /** 41 | A flag indicating whether the editor runs in the terminal. 42 | */ 43 | readonly isTerminalEditor: boolean; 44 | }; 45 | 46 | /** 47 | Open the given files in the user's editor at specific line and column if supported by the editor. It does not wait for the editor to start or quit unless you specify `wait: true` in the options. 48 | 49 | @param files - Items should be in the format `foo.js:1:5` or `{file: 'foo.js', line: 1: column: 5}`. 50 | 51 | @returns Promise - If options.wait is true, the returned promise resolves as soon as the editor closes. Otherwise it resolves when the editor starts. 52 | 53 | @example 54 | ``` 55 | import openEditor from 'open-editor'; 56 | 57 | openEditor([ 58 | { 59 | file: 'readme.md', 60 | line: 10, 61 | column: 2, 62 | } 63 | ]); 64 | 65 | openEditor([ 66 | 'unicorn.js:5:3', 67 | ]); 68 | ``` 69 | */ 70 | export default function openEditor(files: readonly PathLike[], options?: Options): Promise; 71 | 72 | /** 73 | Same as `openEditor()`, but returns an object with the binary name, arguments, and a flag indicating whether the editor runs in the terminal. 74 | 75 | Can be useful if you want to handle opening the files yourself. 76 | 77 | @example 78 | ``` 79 | import {getEditorInfo} from 'open-editor'; 80 | 81 | getEditorInfo([ 82 | { 83 | file: 'foo.js', 84 | line: 1, 85 | column: 5, 86 | } 87 | ]); 88 | //=> {binary: 'subl', arguments: ['foo.js:1:5'], isTerminalEditor: false} 89 | ``` 90 | */ 91 | export function getEditorInfo(files: readonly PathLike[], options?: Options): EditorInfo; 92 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # open-editor 2 | 3 | > Open files in your editor at a specific line and column 4 | 5 | Supports any editor, but only the following editors will open at a specific line and column: 6 | 7 | - Sublime Text 8 | - Atom 9 | - Zed 10 | - Visual Studio Code 11 | - VSCodium 12 | - WebStorm* 13 | - TextMate 14 | - Vim 15 | - NeoVim 16 | - IntelliJ IDEA* 17 | 18 | *\*Doesn't support column.* 19 | 20 | ## Install 21 | 22 | ```sh 23 | npm install open-editor 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | import openEditor from 'open-editor'; 30 | 31 | openEditor([ 32 | { 33 | file: 'readme.md', 34 | line: 10, 35 | column: 2, 36 | } 37 | ]); 38 | 39 | openEditor([ 40 | 'unicorn.js:5:3', 41 | ]); 42 | ``` 43 | 44 | ## API 45 | 46 | ### openEditor(files, options?) 47 | 48 | Open the given files in the user's editor at specific line and column if supported by the editor. It does not wait for the editor to start or quit unless you specify `wait: true` in the options. 49 | 50 | #### files 51 | 52 | Type: `Array` 53 | 54 | Items should be in the format `foo.js:1:5` or `{file: 'foo.js', line: 1: column: 5}`. 55 | 56 | #### options 57 | 58 | Type: `object` 59 | 60 | ##### wait 61 | 62 | Type: `boolean`\ 63 | Default: `false` 64 | 65 | Wait until the editor is closed. 66 | 67 | ```js 68 | import openEditor from 'open-editor'; 69 | 70 | await openEditor(['unicorn.js:5:3'], {wait: true}); 71 | 72 | console.log('File was closed'); 73 | ``` 74 | 75 | ##### editor 76 | 77 | Type: `string`\ 78 | Default: [Auto-detected](https://github.com/sindresorhus/env-editor) 79 | 80 | The name, command, or binary path of the editor. 81 | 82 | **Only use this option if you really have to.** Can be useful if you want to force a specific editor or implement your own auto-detection. 83 | 84 | ### getEditorInfo(files, options?) 85 | 86 | Same as `openEditor()`, but returns an object with the binary name, arguments, and a flag indicating whether the editor runs in the terminal. 87 | 88 | Example: `{binary: 'subl', arguments: ['foo.js:1:5'], isTerminalEditor: false}` 89 | 90 | Can be useful if you want to handle opening the files yourself. 91 | 92 | ```js 93 | import {getEditorInfo} from 'open-editor'; 94 | 95 | getEditorInfo([ 96 | { 97 | file: 'foo.js', 98 | line: 1, 99 | column: 5, 100 | } 101 | ]); 102 | //=> {binary: 'subl', arguments: ['foo.js:1:5'], isTerminalEditor: false} 103 | ``` 104 | 105 | ## Related 106 | 107 | - [open-editor-cli](https://github.com/sindresorhus/open-editor-cli) - CLI for this module 108 | - [open](https://github.com/sindresorhus/open) - Open stuff like URLs, files, executables 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {execa} from 'execa'; 3 | import {getEditor, defaultEditor} from 'env-editor'; 4 | import {parseLineColumnPath, stringifyLineColumnPath} from 'line-column-path'; 5 | import open from 'open'; 6 | 7 | export function getEditorInfo(files, options = {}) { 8 | if (!Array.isArray(files)) { 9 | throw new TypeError(`Expected an \`Array\`, got ${typeof files}`); 10 | } 11 | 12 | const editor = options.editor ? getEditor(options.editor) : defaultEditor(); 13 | const editorArguments = []; 14 | 15 | if (['vscode', 'vscodium'].includes(editor.id)) { 16 | editorArguments.push('--goto'); 17 | } 18 | 19 | for (const file of files) { 20 | const parsed = parseLineColumnPath(file); 21 | 22 | if (['sublime', 'atom', 'zed', 'vscode', 'vscodium'].includes(editor.id)) { 23 | editorArguments.push(stringifyLineColumnPath(parsed)); 24 | 25 | if (options.wait) { 26 | editorArguments.push('--wait'); 27 | } 28 | 29 | continue; 30 | } 31 | 32 | if (['webstorm', 'intellij'].includes(editor.id)) { 33 | editorArguments.push(stringifyLineColumnPath(parsed, {column: false})); 34 | 35 | if (options.wait) { 36 | editorArguments.push('--wait'); 37 | } 38 | 39 | continue; 40 | } 41 | 42 | if (editor.id === 'textmate') { 43 | editorArguments.push( 44 | '--line', 45 | stringifyLineColumnPath(parsed, { 46 | file: false, 47 | }), 48 | parsed.file, 49 | ); 50 | 51 | if (options.wait) { 52 | editorArguments.push('--wait'); 53 | } 54 | 55 | continue; 56 | } 57 | 58 | if (['vim', 'neovim'].includes(editor.id)) { 59 | editorArguments.push( 60 | `+call cursor(${parsed.line}, ${parsed.column})`, 61 | parsed.file, 62 | ); 63 | 64 | continue; 65 | } 66 | 67 | editorArguments.push(parsed.file); 68 | } 69 | 70 | return { 71 | binary: editor.binary, 72 | arguments: editorArguments, 73 | isTerminalEditor: editor.isTerminalEditor, 74 | }; 75 | } 76 | 77 | export default async function openEditor(files, options = {}) { 78 | const result = getEditorInfo(files, options); 79 | const stdio = result.isTerminalEditor ? 'inherit' : 'ignore'; 80 | 81 | const subprocess = execa(result.binary, result.arguments, { 82 | detached: true, 83 | stdio, 84 | }); 85 | 86 | // Fallback 87 | subprocess.on('error', () => { 88 | const result = getEditorInfo(files, { 89 | ...options, 90 | editor: '', 91 | }); 92 | 93 | for (const file of result.arguments) { 94 | open(file); 95 | } 96 | }); 97 | 98 | if (options.wait) { 99 | return new Promise(resolve => { 100 | subprocess.on('exit', resolve); 101 | }); 102 | } 103 | 104 | if (result.isTerminalEditor) { 105 | subprocess.on('exit', process.exit); 106 | } else { 107 | subprocess.unref(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import url from 'node:url'; 2 | import path from 'node:path'; 3 | import test from 'ava'; 4 | import {getEditorInfo} from './index.js'; 5 | 6 | const fixtureFiles = [ 7 | 'unicorn.js:10:20', 8 | 'rainbow.js:43:4', 9 | ]; 10 | const fixturePath = path.resolve('/foo.js'); 11 | const fixtureUrl = url.pathToFileURL(fixturePath); 12 | 13 | test('files', t => { 14 | t.deepEqual( 15 | getEditorInfo( 16 | [ 17 | { 18 | file: 'unicorn.js', 19 | line: 10, 20 | column: 20, 21 | }, 22 | { 23 | file: 'rainbow.js', 24 | line: 43, 25 | column: 4, 26 | }, 27 | ], 28 | { 29 | editor: 'sublime', 30 | }, 31 | ), 32 | { 33 | binary: 'subl', 34 | arguments: fixtureFiles, 35 | isTerminalEditor: false, 36 | }, 37 | ); 38 | t.deepEqual( 39 | getEditorInfo( 40 | [ 41 | fixtureUrl, 42 | { 43 | file: fixtureUrl, 44 | line: 43, 45 | column: 4, 46 | }, 47 | ], 48 | { 49 | editor: 'sublime', 50 | }, 51 | ), 52 | { 53 | binary: 'subl', 54 | arguments: [ 55 | `${fixturePath}:1:1`, 56 | `${fixturePath}:43:4`, 57 | ], 58 | isTerminalEditor: false, 59 | }, 60 | ); 61 | }); 62 | 63 | test('editor - generic', t => { 64 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'noop'}), { 65 | binary: 'noop', 66 | arguments: [ 67 | 'unicorn.js', 68 | 'rainbow.js', 69 | ], 70 | isTerminalEditor: false, 71 | }); 72 | }); 73 | 74 | test('editor - Sublime', t => { 75 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'sublime'}), { 76 | binary: 'subl', 77 | arguments: fixtureFiles, 78 | isTerminalEditor: false, 79 | }); 80 | }); 81 | 82 | test('editor - Atom', t => { 83 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'atom'}), { 84 | binary: 'atom', 85 | arguments: fixtureFiles, 86 | isTerminalEditor: false, 87 | }); 88 | }); 89 | 90 | test('editor - VS Code', t => { 91 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'vscode'}), { 92 | binary: 'code', 93 | arguments: ['--goto', ...fixtureFiles], 94 | isTerminalEditor: false, 95 | }); 96 | }); 97 | 98 | test('editor - WebStorm', t => { 99 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'webstorm'}), { 100 | binary: 'webstorm', 101 | arguments: [ 102 | 'unicorn.js:10', 103 | 'rainbow.js:43', 104 | ], 105 | isTerminalEditor: false, 106 | }); 107 | }); 108 | 109 | test('editor - TextMate', t => { 110 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'textmate'}), { 111 | binary: 'mate', 112 | arguments: [ 113 | '--line', 114 | '10:20', 115 | 'unicorn.js', 116 | '--line', 117 | '43:4', 118 | 'rainbow.js', 119 | ], 120 | isTerminalEditor: false, 121 | }); 122 | }); 123 | 124 | test('editor - Vim', t => { 125 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'vim'}), { 126 | binary: 'vim', 127 | arguments: [ 128 | '+call cursor(10, 20)', 129 | 'unicorn.js', 130 | '+call cursor(43, 4)', 131 | 'rainbow.js', 132 | ], 133 | isTerminalEditor: true, 134 | }); 135 | }); 136 | 137 | test('editor - NeoVim', t => { 138 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'neovim'}), { 139 | binary: 'nvim', 140 | arguments: [ 141 | '+call cursor(10, 20)', 142 | 'unicorn.js', 143 | '+call cursor(43, 4)', 144 | 'rainbow.js', 145 | ], 146 | isTerminalEditor: true, 147 | }); 148 | }); 149 | 150 | test('editor - IntelliJ IDEA', t => { 151 | t.deepEqual(getEditorInfo(fixtureFiles, {editor: 'intellij'}), { 152 | binary: 'idea', 153 | arguments: [ 154 | 'unicorn.js:10', 155 | 'rainbow.js:43', 156 | ], 157 | isTerminalEditor: false, 158 | }); 159 | }); 160 | --------------------------------------------------------------------------------