launch-editor-py 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- launch_editor_py-1.0.0/LICENSE +34 -0
- launch_editor_py-1.0.0/PKG-INFO +258 -0
- launch_editor_py-1.0.0/README.rst +232 -0
- launch_editor_py-1.0.0/pyproject.toml +117 -0
- launch_editor_py-1.0.0/pyproject.toml.orig +123 -0
- launch_editor_py-1.0.0/src/launch_editor/__init__.py +535 -0
- launch_editor_py-1.0.0/src/launch_editor/__main__.py +71 -0
- launch_editor_py-1.0.0/src/launch_editor/py.typed +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adam Johnson
|
|
4
|
+
Copyright (c) 2017-present, Yuxi (Evan) You
|
|
5
|
+
Copyright (c) 2015-present, Facebook, Inc.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
This package is a Python port of the launch-editor npm package:
|
|
28
|
+
https://github.com/vitejs/launch-editor
|
|
29
|
+
|
|
30
|
+
launch-editor is Copyright (c) 2017-present, Yuxi (Evan) You, and released
|
|
31
|
+
under the MIT License. Its main module is derived from code in
|
|
32
|
+
create-react-app, Copyright (c) 2015-present, Facebook, Inc., also released
|
|
33
|
+
under the MIT License:
|
|
34
|
+
https://github.com/facebook/create-react-app
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: launch-editor-py
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Open files in the user’s editor, optionally at a given line and column.
|
|
5
|
+
Keywords: editor,launch-editor,open-in-editor
|
|
6
|
+
Author: Adam Johnson
|
|
7
|
+
Author-email: Adam Johnson <me@adamj.eu>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Changelog, https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst
|
|
23
|
+
Project-URL: Funding, https://adamj.eu/books/
|
|
24
|
+
Project-URL: Repository, https://github.com/adamchainz/launch-editor-py
|
|
25
|
+
Description-Content-Type: text/x-rst
|
|
26
|
+
|
|
27
|
+
================
|
|
28
|
+
launch-editor-py
|
|
29
|
+
================
|
|
30
|
+
|
|
31
|
+
.. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/launch-editor-py/main.yml.svg?branch=main&style=for-the-badge
|
|
32
|
+
:target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
|
|
33
|
+
|
|
34
|
+
.. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge
|
|
35
|
+
:target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
|
|
36
|
+
|
|
37
|
+
.. image:: https://img.shields.io/pypi/v/launch-editor-py.svg?style=for-the-badge
|
|
38
|
+
:target: https://pypi.org/project/launch-editor-py/
|
|
39
|
+
|
|
40
|
+
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
|
|
41
|
+
:target: https://github.com/psf/black
|
|
42
|
+
|
|
43
|
+
.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
|
|
44
|
+
:target: https://github.com/pre-commit/pre-commit
|
|
45
|
+
:alt: pre-commit
|
|
46
|
+
|
|
47
|
+
----
|
|
48
|
+
|
|
49
|
+
Open files in the user’s editor, optionally at a given line and column.
|
|
50
|
+
|
|
51
|
+
A Python port of the `launch-editor <https://github.com/vitejs/launch-editor>`__ npm package, as used by Vite to open files from the browser.
|
|
52
|
+
It detects which editor is running, and knows how to pass a line and column to many editors.
|
|
53
|
+
|
|
54
|
+
----
|
|
55
|
+
|
|
56
|
+
**Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
|
|
57
|
+
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
Requirements
|
|
61
|
+
------------
|
|
62
|
+
|
|
63
|
+
Python 3.11 to 3.15 supported.
|
|
64
|
+
|
|
65
|
+
Installation
|
|
66
|
+
------------
|
|
67
|
+
|
|
68
|
+
1. Install with **pip**:
|
|
69
|
+
|
|
70
|
+
.. code-block:: sh
|
|
71
|
+
|
|
72
|
+
python -m pip install launch-editor-py
|
|
73
|
+
|
|
74
|
+
The package imports as ``launch_editor``.
|
|
75
|
+
|
|
76
|
+
Usage
|
|
77
|
+
-----
|
|
78
|
+
|
|
79
|
+
Call ``launch_editor()`` with a filename, and optionally a line and column:
|
|
80
|
+
|
|
81
|
+
.. code-block:: python
|
|
82
|
+
|
|
83
|
+
from launch_editor import LaunchEditorError, launch_editor
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
launch_editor("src/example.py", line=12, column=4)
|
|
87
|
+
except LaunchEditorError as exc:
|
|
88
|
+
print(f"Could not open editor: {exc}")
|
|
89
|
+
|
|
90
|
+
API
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
``launch_editor(filename, line=None, column=None, *, editor=None)``
|
|
94
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
95
|
+
|
|
96
|
+
Open ``filename`` in the user’s editor, at ``line`` and ``column`` if given, and return the editor’s ``subprocess.Popen`` object.
|
|
97
|
+
|
|
98
|
+
The editor is picked by ``guess_editor()``, passing it ``editor``.
|
|
99
|
+
|
|
100
|
+
GUI editors start in the background, and the function returns immediately.
|
|
101
|
+
If a GUI editor exits with an error, a warning is logged to the ``launch_editor`` logger.
|
|
102
|
+
|
|
103
|
+
Terminal editors, like Vim, take over the current terminal until they exit.
|
|
104
|
+
While one is open, further attempts to open a terminal editor raise ``LaunchEditorError``.
|
|
105
|
+
|
|
106
|
+
Raises ``LaunchEditorError`` if the file does not exist, no editor is found, or the editor cannot be run.
|
|
107
|
+
|
|
108
|
+
``guess_editor(editor=None)``
|
|
109
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
110
|
+
|
|
111
|
+
Return the command for the user’s editor, as a list of strings, or an empty list if none is found.
|
|
112
|
+
The editor is chosen from, in order:
|
|
113
|
+
|
|
114
|
+
1. The ``editor`` argument, as a command string like ``"code --new-window"`` or a list of arguments.
|
|
115
|
+
2. The ``LAUNCH_EDITOR`` environment variable.
|
|
116
|
+
3. A supported editor that is currently running, on macOS, Linux, and Windows.
|
|
117
|
+
4. The ``VISUAL`` environment variable.
|
|
118
|
+
5. The ``EDITOR`` environment variable.
|
|
119
|
+
|
|
120
|
+
``LAUNCH_EDITOR`` is shared with the launch-editor npm package, so you can set it once for both.
|
|
121
|
+
Like there, it’s treated as a single command, not split into arguments.
|
|
122
|
+
It may name a script, which is passed the file, line, and column as separate arguments, unless it’s a supported editor.
|
|
123
|
+
|
|
124
|
+
``position_args(editor, filename, line, column=None)``
|
|
125
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
126
|
+
|
|
127
|
+
Return the arguments that open ``filename`` at ``line`` and ``column`` in the given ``editor`` command.
|
|
128
|
+
Unsupported editors get only the filename, since passing a position incorrectly can cause errors or confusing behaviour.
|
|
129
|
+
|
|
130
|
+
``LaunchEditorError``
|
|
131
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
132
|
+
|
|
133
|
+
A basic exception subclass, raised when the editor cannot be launched.
|
|
134
|
+
|
|
135
|
+
Supported editors
|
|
136
|
+
-----------------
|
|
137
|
+
|
|
138
|
+
Opening at a line and column is supported for:
|
|
139
|
+
|
|
140
|
+
* VS Code and its forks: Antigravity, Cursor, Trae, VSCodium
|
|
141
|
+
* JetBrains IDEs: AppCode, CLion, GoLand, IntelliJ IDEA, PhpStorm, PyCharm, Rider, RubyMine, WebStorm
|
|
142
|
+
* Atom, Emacs, Helix, joe, micro, nano, Neovim, Notepad++, Sublime Text, TextMate, Vim, Zed
|
|
143
|
+
|
|
144
|
+
Command line
|
|
145
|
+
------------
|
|
146
|
+
|
|
147
|
+
Run ``python -m launch_editor`` to open a file from the command line, or check which editor would be used:
|
|
148
|
+
|
|
149
|
+
.. code-block:: console
|
|
150
|
+
|
|
151
|
+
$ python -m launch_editor --guess
|
|
152
|
+
code
|
|
153
|
+
$ python -m launch_editor src/example.py:12:4
|
|
154
|
+
|
|
155
|
+
Full help:
|
|
156
|
+
|
|
157
|
+
.. [[[cog
|
|
158
|
+
.. import cog
|
|
159
|
+
.. import os
|
|
160
|
+
.. import subprocess
|
|
161
|
+
.. import sys
|
|
162
|
+
.. result = subprocess.run(
|
|
163
|
+
.. [sys.executable, "-m", "launch_editor", "--help"],
|
|
164
|
+
.. capture_output=True,
|
|
165
|
+
.. text=True,
|
|
166
|
+
.. env={**os.environ, "COLUMNS": "80"},
|
|
167
|
+
.. )
|
|
168
|
+
.. cog.outl("")
|
|
169
|
+
.. cog.outl(".. code-block:: console")
|
|
170
|
+
.. cog.outl("")
|
|
171
|
+
.. for line in result.stdout.splitlines():
|
|
172
|
+
.. if line.strip() == "":
|
|
173
|
+
.. cog.outl("")
|
|
174
|
+
.. else:
|
|
175
|
+
.. cog.outl(" " + line.rstrip())
|
|
176
|
+
.. cog.outl("")
|
|
177
|
+
.. ]]]
|
|
178
|
+
|
|
179
|
+
.. code-block:: console
|
|
180
|
+
|
|
181
|
+
usage: python -m launch_editor [-h] [--editor EDITOR] [--guess] [FILE]
|
|
182
|
+
|
|
183
|
+
Open a file in your editor.
|
|
184
|
+
|
|
185
|
+
positional arguments:
|
|
186
|
+
FILE The file to open, optionally followed by :LINE or
|
|
187
|
+
:LINE:COLUMN, like example.py:12:4.
|
|
188
|
+
|
|
189
|
+
options:
|
|
190
|
+
-h, --help show this help message and exit
|
|
191
|
+
--editor EDITOR The editor command to use, instead of guessing.
|
|
192
|
+
--guess Print the editor command that would be used, and exit.
|
|
193
|
+
|
|
194
|
+
.. [[[end]]]
|
|
195
|
+
|
|
196
|
+
Security
|
|
197
|
+
--------
|
|
198
|
+
|
|
199
|
+
Web applications often use this package to open files from a browser, via an endpoint on a local development server.
|
|
200
|
+
Such an endpoint runs a program, so protect it:
|
|
201
|
+
|
|
202
|
+
* Accept an identifier for a file the application already knows about, rather than an arbitrary path.
|
|
203
|
+
|
|
204
|
+
* Only accept POST requests, and check the ``Sec-Fetch-Site`` and ``Origin`` headers to reject cross-origin requests.
|
|
205
|
+
Any website the user visits can send requests to ``localhost``.
|
|
206
|
+
|
|
207
|
+
* Check the ``Host`` header is a local name, like ``localhost`` or ``127.0.0.1``, to prevent DNS rebinding attacks.
|
|
208
|
+
|
|
209
|
+
Filenames are passed to editors as absolute paths, so a name starting with ``-`` cannot be interpreted as an option.
|
|
210
|
+
|
|
211
|
+
On Windows, editors launched through batch files, such as VS Code’s ``code.cmd``, run through ``cmd.exe``, which has its own command line parsing.
|
|
212
|
+
To prevent command injection, launch-editor-py quotes every argument and refuses to pass arguments containing ``"``, ``%``, ``!``, or line breaks to them.
|
|
213
|
+
UNC paths are also refused on Windows, like the npm package, to avoid leaking NTLM credentials to remote hosts—see `its explanation <https://github.com/vitejs/launch-editor/tree/main/packages/launch-editor#unc-paths-on-windows>`__.
|
|
214
|
+
|
|
215
|
+
Differences from the launch-editor npm package
|
|
216
|
+
----------------------------------------------
|
|
217
|
+
|
|
218
|
+
This package was last compared against the npm package at version `2.14.2 <https://github.com/vitejs/launch-editor/tree/v2.14.2>`__ (commit ``b533489``), on 2026-10-28.
|
|
219
|
+
Upstream ``main`` had no further changes to the package at that time.
|
|
220
|
+
|
|
221
|
+
It also includes the fix from upstream `PR #163 <https://github.com/vitejs/launch-editor/pull/163>`__, unmerged at that time, so Cursor, Trae, and Antigravity open at the given line and column when detected running on Windows.
|
|
222
|
+
|
|
223
|
+
Additions:
|
|
224
|
+
|
|
225
|
+
* Opening at a line and column for Helix (``hx``), micro, nano, Neovim (``nvim``), and vi.
|
|
226
|
+
|
|
227
|
+
* Helix, micro, Neovim, and vi are treated as terminal editors, alongside Emacs, nano, and Vim.
|
|
228
|
+
|
|
229
|
+
* A command line interface, ``python -m launch_editor``.
|
|
230
|
+
|
|
231
|
+
Changes:
|
|
232
|
+
|
|
233
|
+
* Errors raise ``LaunchEditorError``, rather than calling an error callback and printing to the console.
|
|
234
|
+
A missing file is an error, rather than being silently ignored.
|
|
235
|
+
A GUI editor that exits with an error logs a warning to the ``launch_editor`` logger.
|
|
236
|
+
|
|
237
|
+
* ``launch_editor()`` takes the line and column as separate arguments, rather than parsing them from ``file:line:column``.
|
|
238
|
+
The command line interface still accepts ``file:line:column``, but not ``file://`` URLs.
|
|
239
|
+
|
|
240
|
+
* Opening a terminal editor while one is already open raises an error, rather than killing the open editor.
|
|
241
|
+
|
|
242
|
+
* ``VISUAL`` and ``EDITOR`` are split into arguments, so values like ``code --wait`` work.
|
|
243
|
+
``LAUNCH_EDITOR`` is still treated as a single command, like the npm package.
|
|
244
|
+
|
|
245
|
+
* On Linux, running editors are detected by exact process name, rather than by substring, so for example ``nvim`` isn’t detected as ``vim``.
|
|
246
|
+
|
|
247
|
+
* On Windows, editor executables are run directly, rather than through ``cmd.exe``.
|
|
248
|
+
Batch file editors, like VS Code’s ``code.cmd``, are still run through ``cmd.exe``, with every argument quoted, rather than escaped with ``^``.
|
|
249
|
+
Arguments containing ``"``, ``%``, ``!``, or line breaks are refused for them, where the npm package refuses only ``%`` and line breaks.
|
|
250
|
+
|
|
251
|
+
* There’s no special handling for StackBlitz WebContainers.
|
|
252
|
+
|
|
253
|
+
License
|
|
254
|
+
-------
|
|
255
|
+
|
|
256
|
+
MIT.
|
|
257
|
+
This package is a port of the launch-editor npm package, Copyright (c) 2017-present Yuxi (Evan) You, which in turn derives from code in create-react-app, Copyright (c) 2015-present Facebook, Inc., both MIT licensed.
|
|
258
|
+
See the ``LICENSE`` file for details.
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
================
|
|
2
|
+
launch-editor-py
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
.. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/launch-editor-py/main.yml.svg?branch=main&style=for-the-badge
|
|
6
|
+
:target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
|
|
7
|
+
|
|
8
|
+
.. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge
|
|
9
|
+
:target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
|
|
10
|
+
|
|
11
|
+
.. image:: https://img.shields.io/pypi/v/launch-editor-py.svg?style=for-the-badge
|
|
12
|
+
:target: https://pypi.org/project/launch-editor-py/
|
|
13
|
+
|
|
14
|
+
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
|
|
15
|
+
:target: https://github.com/psf/black
|
|
16
|
+
|
|
17
|
+
.. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
|
|
18
|
+
:target: https://github.com/pre-commit/pre-commit
|
|
19
|
+
:alt: pre-commit
|
|
20
|
+
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
Open files in the user’s editor, optionally at a given line and column.
|
|
24
|
+
|
|
25
|
+
A Python port of the `launch-editor <https://github.com/vitejs/launch-editor>`__ npm package, as used by Vite to open files from the browser.
|
|
26
|
+
It detects which editor is running, and knows how to pass a line and column to many editors.
|
|
27
|
+
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
**Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
|
|
31
|
+
|
|
32
|
+
----
|
|
33
|
+
|
|
34
|
+
Requirements
|
|
35
|
+
------------
|
|
36
|
+
|
|
37
|
+
Python 3.11 to 3.15 supported.
|
|
38
|
+
|
|
39
|
+
Installation
|
|
40
|
+
------------
|
|
41
|
+
|
|
42
|
+
1. Install with **pip**:
|
|
43
|
+
|
|
44
|
+
.. code-block:: sh
|
|
45
|
+
|
|
46
|
+
python -m pip install launch-editor-py
|
|
47
|
+
|
|
48
|
+
The package imports as ``launch_editor``.
|
|
49
|
+
|
|
50
|
+
Usage
|
|
51
|
+
-----
|
|
52
|
+
|
|
53
|
+
Call ``launch_editor()`` with a filename, and optionally a line and column:
|
|
54
|
+
|
|
55
|
+
.. code-block:: python
|
|
56
|
+
|
|
57
|
+
from launch_editor import LaunchEditorError, launch_editor
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
launch_editor("src/example.py", line=12, column=4)
|
|
61
|
+
except LaunchEditorError as exc:
|
|
62
|
+
print(f"Could not open editor: {exc}")
|
|
63
|
+
|
|
64
|
+
API
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
``launch_editor(filename, line=None, column=None, *, editor=None)``
|
|
68
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
69
|
+
|
|
70
|
+
Open ``filename`` in the user’s editor, at ``line`` and ``column`` if given, and return the editor’s ``subprocess.Popen`` object.
|
|
71
|
+
|
|
72
|
+
The editor is picked by ``guess_editor()``, passing it ``editor``.
|
|
73
|
+
|
|
74
|
+
GUI editors start in the background, and the function returns immediately.
|
|
75
|
+
If a GUI editor exits with an error, a warning is logged to the ``launch_editor`` logger.
|
|
76
|
+
|
|
77
|
+
Terminal editors, like Vim, take over the current terminal until they exit.
|
|
78
|
+
While one is open, further attempts to open a terminal editor raise ``LaunchEditorError``.
|
|
79
|
+
|
|
80
|
+
Raises ``LaunchEditorError`` if the file does not exist, no editor is found, or the editor cannot be run.
|
|
81
|
+
|
|
82
|
+
``guess_editor(editor=None)``
|
|
83
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
84
|
+
|
|
85
|
+
Return the command for the user’s editor, as a list of strings, or an empty list if none is found.
|
|
86
|
+
The editor is chosen from, in order:
|
|
87
|
+
|
|
88
|
+
1. The ``editor`` argument, as a command string like ``"code --new-window"`` or a list of arguments.
|
|
89
|
+
2. The ``LAUNCH_EDITOR`` environment variable.
|
|
90
|
+
3. A supported editor that is currently running, on macOS, Linux, and Windows.
|
|
91
|
+
4. The ``VISUAL`` environment variable.
|
|
92
|
+
5. The ``EDITOR`` environment variable.
|
|
93
|
+
|
|
94
|
+
``LAUNCH_EDITOR`` is shared with the launch-editor npm package, so you can set it once for both.
|
|
95
|
+
Like there, it’s treated as a single command, not split into arguments.
|
|
96
|
+
It may name a script, which is passed the file, line, and column as separate arguments, unless it’s a supported editor.
|
|
97
|
+
|
|
98
|
+
``position_args(editor, filename, line, column=None)``
|
|
99
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
100
|
+
|
|
101
|
+
Return the arguments that open ``filename`` at ``line`` and ``column`` in the given ``editor`` command.
|
|
102
|
+
Unsupported editors get only the filename, since passing a position incorrectly can cause errors or confusing behaviour.
|
|
103
|
+
|
|
104
|
+
``LaunchEditorError``
|
|
105
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
106
|
+
|
|
107
|
+
A basic exception subclass, raised when the editor cannot be launched.
|
|
108
|
+
|
|
109
|
+
Supported editors
|
|
110
|
+
-----------------
|
|
111
|
+
|
|
112
|
+
Opening at a line and column is supported for:
|
|
113
|
+
|
|
114
|
+
* VS Code and its forks: Antigravity, Cursor, Trae, VSCodium
|
|
115
|
+
* JetBrains IDEs: AppCode, CLion, GoLand, IntelliJ IDEA, PhpStorm, PyCharm, Rider, RubyMine, WebStorm
|
|
116
|
+
* Atom, Emacs, Helix, joe, micro, nano, Neovim, Notepad++, Sublime Text, TextMate, Vim, Zed
|
|
117
|
+
|
|
118
|
+
Command line
|
|
119
|
+
------------
|
|
120
|
+
|
|
121
|
+
Run ``python -m launch_editor`` to open a file from the command line, or check which editor would be used:
|
|
122
|
+
|
|
123
|
+
.. code-block:: console
|
|
124
|
+
|
|
125
|
+
$ python -m launch_editor --guess
|
|
126
|
+
code
|
|
127
|
+
$ python -m launch_editor src/example.py:12:4
|
|
128
|
+
|
|
129
|
+
Full help:
|
|
130
|
+
|
|
131
|
+
.. [[[cog
|
|
132
|
+
.. import cog
|
|
133
|
+
.. import os
|
|
134
|
+
.. import subprocess
|
|
135
|
+
.. import sys
|
|
136
|
+
.. result = subprocess.run(
|
|
137
|
+
.. [sys.executable, "-m", "launch_editor", "--help"],
|
|
138
|
+
.. capture_output=True,
|
|
139
|
+
.. text=True,
|
|
140
|
+
.. env={**os.environ, "COLUMNS": "80"},
|
|
141
|
+
.. )
|
|
142
|
+
.. cog.outl("")
|
|
143
|
+
.. cog.outl(".. code-block:: console")
|
|
144
|
+
.. cog.outl("")
|
|
145
|
+
.. for line in result.stdout.splitlines():
|
|
146
|
+
.. if line.strip() == "":
|
|
147
|
+
.. cog.outl("")
|
|
148
|
+
.. else:
|
|
149
|
+
.. cog.outl(" " + line.rstrip())
|
|
150
|
+
.. cog.outl("")
|
|
151
|
+
.. ]]]
|
|
152
|
+
|
|
153
|
+
.. code-block:: console
|
|
154
|
+
|
|
155
|
+
usage: python -m launch_editor [-h] [--editor EDITOR] [--guess] [FILE]
|
|
156
|
+
|
|
157
|
+
Open a file in your editor.
|
|
158
|
+
|
|
159
|
+
positional arguments:
|
|
160
|
+
FILE The file to open, optionally followed by :LINE or
|
|
161
|
+
:LINE:COLUMN, like example.py:12:4.
|
|
162
|
+
|
|
163
|
+
options:
|
|
164
|
+
-h, --help show this help message and exit
|
|
165
|
+
--editor EDITOR The editor command to use, instead of guessing.
|
|
166
|
+
--guess Print the editor command that would be used, and exit.
|
|
167
|
+
|
|
168
|
+
.. [[[end]]]
|
|
169
|
+
|
|
170
|
+
Security
|
|
171
|
+
--------
|
|
172
|
+
|
|
173
|
+
Web applications often use this package to open files from a browser, via an endpoint on a local development server.
|
|
174
|
+
Such an endpoint runs a program, so protect it:
|
|
175
|
+
|
|
176
|
+
* Accept an identifier for a file the application already knows about, rather than an arbitrary path.
|
|
177
|
+
|
|
178
|
+
* Only accept POST requests, and check the ``Sec-Fetch-Site`` and ``Origin`` headers to reject cross-origin requests.
|
|
179
|
+
Any website the user visits can send requests to ``localhost``.
|
|
180
|
+
|
|
181
|
+
* Check the ``Host`` header is a local name, like ``localhost`` or ``127.0.0.1``, to prevent DNS rebinding attacks.
|
|
182
|
+
|
|
183
|
+
Filenames are passed to editors as absolute paths, so a name starting with ``-`` cannot be interpreted as an option.
|
|
184
|
+
|
|
185
|
+
On Windows, editors launched through batch files, such as VS Code’s ``code.cmd``, run through ``cmd.exe``, which has its own command line parsing.
|
|
186
|
+
To prevent command injection, launch-editor-py quotes every argument and refuses to pass arguments containing ``"``, ``%``, ``!``, or line breaks to them.
|
|
187
|
+
UNC paths are also refused on Windows, like the npm package, to avoid leaking NTLM credentials to remote hosts—see `its explanation <https://github.com/vitejs/launch-editor/tree/main/packages/launch-editor#unc-paths-on-windows>`__.
|
|
188
|
+
|
|
189
|
+
Differences from the launch-editor npm package
|
|
190
|
+
----------------------------------------------
|
|
191
|
+
|
|
192
|
+
This package was last compared against the npm package at version `2.14.2 <https://github.com/vitejs/launch-editor/tree/v2.14.2>`__ (commit ``b533489``), on 2026-10-28.
|
|
193
|
+
Upstream ``main`` had no further changes to the package at that time.
|
|
194
|
+
|
|
195
|
+
It also includes the fix from upstream `PR #163 <https://github.com/vitejs/launch-editor/pull/163>`__, unmerged at that time, so Cursor, Trae, and Antigravity open at the given line and column when detected running on Windows.
|
|
196
|
+
|
|
197
|
+
Additions:
|
|
198
|
+
|
|
199
|
+
* Opening at a line and column for Helix (``hx``), micro, nano, Neovim (``nvim``), and vi.
|
|
200
|
+
|
|
201
|
+
* Helix, micro, Neovim, and vi are treated as terminal editors, alongside Emacs, nano, and Vim.
|
|
202
|
+
|
|
203
|
+
* A command line interface, ``python -m launch_editor``.
|
|
204
|
+
|
|
205
|
+
Changes:
|
|
206
|
+
|
|
207
|
+
* Errors raise ``LaunchEditorError``, rather than calling an error callback and printing to the console.
|
|
208
|
+
A missing file is an error, rather than being silently ignored.
|
|
209
|
+
A GUI editor that exits with an error logs a warning to the ``launch_editor`` logger.
|
|
210
|
+
|
|
211
|
+
* ``launch_editor()`` takes the line and column as separate arguments, rather than parsing them from ``file:line:column``.
|
|
212
|
+
The command line interface still accepts ``file:line:column``, but not ``file://`` URLs.
|
|
213
|
+
|
|
214
|
+
* Opening a terminal editor while one is already open raises an error, rather than killing the open editor.
|
|
215
|
+
|
|
216
|
+
* ``VISUAL`` and ``EDITOR`` are split into arguments, so values like ``code --wait`` work.
|
|
217
|
+
``LAUNCH_EDITOR`` is still treated as a single command, like the npm package.
|
|
218
|
+
|
|
219
|
+
* On Linux, running editors are detected by exact process name, rather than by substring, so for example ``nvim`` isn’t detected as ``vim``.
|
|
220
|
+
|
|
221
|
+
* On Windows, editor executables are run directly, rather than through ``cmd.exe``.
|
|
222
|
+
Batch file editors, like VS Code’s ``code.cmd``, are still run through ``cmd.exe``, with every argument quoted, rather than escaped with ``^``.
|
|
223
|
+
Arguments containing ``"``, ``%``, ``!``, or line breaks are refused for them, where the npm package refuses only ``%`` and line breaks.
|
|
224
|
+
|
|
225
|
+
* There’s no special handling for StackBlitz WebContainers.
|
|
226
|
+
|
|
227
|
+
License
|
|
228
|
+
-------
|
|
229
|
+
|
|
230
|
+
MIT.
|
|
231
|
+
This package is a port of the launch-editor npm package, Copyright (c) 2017-present Yuxi (Evan) You, which in turn derives from code in create-react-app, Copyright (c) 2015-present Facebook, Inc., both MIT licensed.
|
|
232
|
+
See the ``LICENSE`` file for details.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = ["uv-build>=0.12.1,<0.13"]
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "launch-editor-py"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Open files in the user’s editor, optionally at a given line and column."
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
keywords = [
|
|
11
|
+
"editor",
|
|
12
|
+
"launch-editor",
|
|
13
|
+
"open-in-editor",
|
|
14
|
+
]
|
|
15
|
+
license = "MIT"
|
|
16
|
+
license-files = ["LICENSE"]
|
|
17
|
+
requires-python = ">=3.11"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"Natural Language :: English",
|
|
22
|
+
"Operating System :: OS Independent",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: 3.15",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[[project.authors]]
|
|
33
|
+
name = "Adam Johnson"
|
|
34
|
+
email = "me@adamj.eu"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Changelog = "https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst"
|
|
38
|
+
Funding = "https://adamj.eu/books/"
|
|
39
|
+
Repository = "https://github.com/adamchainz/launch-editor-py"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
test = [
|
|
43
|
+
"cogapp>=3.6",
|
|
44
|
+
"coverage[toml]>=7.10",
|
|
45
|
+
"pytest>=9",
|
|
46
|
+
"pytest-randomly",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[tool.uv.build-backend]
|
|
50
|
+
module-name = "launch_editor"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = [
|
|
54
|
+
"B",
|
|
55
|
+
"C4",
|
|
56
|
+
"E",
|
|
57
|
+
"F",
|
|
58
|
+
"I",
|
|
59
|
+
"SIM",
|
|
60
|
+
"TID",
|
|
61
|
+
"UP",
|
|
62
|
+
"W",
|
|
63
|
+
]
|
|
64
|
+
ignore = [
|
|
65
|
+
"B9",
|
|
66
|
+
"E501",
|
|
67
|
+
"SIM105",
|
|
68
|
+
"SIM108",
|
|
69
|
+
]
|
|
70
|
+
extend-safe-fixes = ["UP006"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint.isort]
|
|
73
|
+
required-imports = ["from __future__ import annotations"]
|
|
74
|
+
|
|
75
|
+
[tool.pyproject-fmt]
|
|
76
|
+
max_supported_python = "3.15"
|
|
77
|
+
|
|
78
|
+
[tool.mypy]
|
|
79
|
+
mypy_path = "src/"
|
|
80
|
+
namespace_packages = false
|
|
81
|
+
warn_unreachable = true
|
|
82
|
+
enable_error_code = [
|
|
83
|
+
"ignore-without-code",
|
|
84
|
+
"redundant-expr",
|
|
85
|
+
"truthy-bool",
|
|
86
|
+
]
|
|
87
|
+
strict = true
|
|
88
|
+
|
|
89
|
+
[[tool.mypy.overrides]]
|
|
90
|
+
module = "tests.*"
|
|
91
|
+
allow_untyped_defs = true
|
|
92
|
+
|
|
93
|
+
[tool.pytest]
|
|
94
|
+
strict = true
|
|
95
|
+
|
|
96
|
+
[tool.coverage.run]
|
|
97
|
+
source = [
|
|
98
|
+
"launch_editor",
|
|
99
|
+
"tests",
|
|
100
|
+
]
|
|
101
|
+
branch = true
|
|
102
|
+
data_file = ".coverage/cov"
|
|
103
|
+
parallel = true
|
|
104
|
+
|
|
105
|
+
[tool.coverage.paths]
|
|
106
|
+
source = [
|
|
107
|
+
"src",
|
|
108
|
+
".tox/**/site-packages",
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
[tool.coverage.report]
|
|
112
|
+
skip_covered = true
|
|
113
|
+
skip_empty = true
|
|
114
|
+
show_missing = true
|
|
115
|
+
|
|
116
|
+
[tool.rstcheck]
|
|
117
|
+
report_level = "ERROR"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "uv_build"
|
|
3
|
+
requires = [
|
|
4
|
+
"uv-build>=0.12.1,<0.13",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
[project]
|
|
8
|
+
name = "launch-editor-py"
|
|
9
|
+
version = "1.0.0"
|
|
10
|
+
description = "Open files in the user’s editor, optionally at a given line and column."
|
|
11
|
+
readme = "README.rst"
|
|
12
|
+
keywords = [
|
|
13
|
+
"editor",
|
|
14
|
+
"launch-editor",
|
|
15
|
+
"open-in-editor",
|
|
16
|
+
]
|
|
17
|
+
license = "MIT"
|
|
18
|
+
license-files = [ "LICENSE" ]
|
|
19
|
+
authors = [
|
|
20
|
+
{ name = "Adam Johnson", email = "me@adamj.eu" },
|
|
21
|
+
]
|
|
22
|
+
requires-python = ">=3.11"
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 5 - Production/Stable",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Natural Language :: English",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Programming Language :: Python :: 3.15",
|
|
34
|
+
"Typing :: Typed",
|
|
35
|
+
]
|
|
36
|
+
urls.Changelog = "https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst"
|
|
37
|
+
urls.Funding = "https://adamj.eu/books/"
|
|
38
|
+
urls.Repository = "https://github.com/adamchainz/launch-editor-py"
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
test = [
|
|
42
|
+
"cogapp>=3.6",
|
|
43
|
+
"coverage[toml]>=7.10",
|
|
44
|
+
"pytest>=9",
|
|
45
|
+
"pytest-randomly",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.uv]
|
|
49
|
+
build-backend.module-name = "launch_editor"
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
lint.select = [
|
|
53
|
+
# flake8-bugbear
|
|
54
|
+
"B",
|
|
55
|
+
# flake8-comprehensions
|
|
56
|
+
"C4",
|
|
57
|
+
# pycodestyle
|
|
58
|
+
"E",
|
|
59
|
+
# Pyflakes errors
|
|
60
|
+
"F",
|
|
61
|
+
# isort
|
|
62
|
+
"I",
|
|
63
|
+
# flake8-simplify
|
|
64
|
+
"SIM",
|
|
65
|
+
# flake8-tidy-imports
|
|
66
|
+
"TID",
|
|
67
|
+
# pyupgrade
|
|
68
|
+
"UP",
|
|
69
|
+
# Pyflakes warnings
|
|
70
|
+
"W",
|
|
71
|
+
]
|
|
72
|
+
lint.ignore = [
|
|
73
|
+
# flake8-bugbear opinionated rules
|
|
74
|
+
"B9",
|
|
75
|
+
# line-too-long
|
|
76
|
+
"E501",
|
|
77
|
+
# suppressible-exception
|
|
78
|
+
"SIM105",
|
|
79
|
+
# if-else-block-instead-of-if-exp
|
|
80
|
+
"SIM108",
|
|
81
|
+
]
|
|
82
|
+
lint.extend-safe-fixes = [
|
|
83
|
+
# non-pep585-annotation
|
|
84
|
+
"UP006",
|
|
85
|
+
]
|
|
86
|
+
lint.isort.required-imports = [ "from __future__ import annotations" ]
|
|
87
|
+
|
|
88
|
+
[tool.pyproject-fmt]
|
|
89
|
+
max_supported_python = "3.15"
|
|
90
|
+
|
|
91
|
+
[tool.mypy]
|
|
92
|
+
mypy_path = "src/"
|
|
93
|
+
namespace_packages = false
|
|
94
|
+
warn_unreachable = true
|
|
95
|
+
enable_error_code = [
|
|
96
|
+
"ignore-without-code",
|
|
97
|
+
"redundant-expr",
|
|
98
|
+
"truthy-bool",
|
|
99
|
+
]
|
|
100
|
+
strict = true
|
|
101
|
+
overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
|
|
102
|
+
|
|
103
|
+
[tool.pytest]
|
|
104
|
+
strict = true
|
|
105
|
+
|
|
106
|
+
[tool.coverage]
|
|
107
|
+
run.source = [
|
|
108
|
+
"launch_editor",
|
|
109
|
+
"tests",
|
|
110
|
+
]
|
|
111
|
+
run.branch = true
|
|
112
|
+
run.data_file = ".coverage/cov"
|
|
113
|
+
run.parallel = true
|
|
114
|
+
paths.source = [
|
|
115
|
+
"src",
|
|
116
|
+
".tox/**/site-packages",
|
|
117
|
+
]
|
|
118
|
+
report.skip_covered = true
|
|
119
|
+
report.skip_empty = true
|
|
120
|
+
report.show_missing = true
|
|
121
|
+
|
|
122
|
+
[tool.rstcheck]
|
|
123
|
+
report_level = "ERROR"
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Open files in the user’s editor, at a given line and column.
|
|
3
|
+
|
|
4
|
+
A Python port of the launch-editor npm package:
|
|
5
|
+
https://github.com/vitejs/launch-editor
|
|
6
|
+
|
|
7
|
+
Last compared against version 2.14.2. See “Differences from the npm package”
|
|
8
|
+
in README.rst for the upstream PRs included and other changes.
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2026 Adam Johnson
|
|
11
|
+
Copyright (c) 2017-present, Yuxi (Evan) You
|
|
12
|
+
Copyright (c) 2015-present, Facebook, Inc.
|
|
13
|
+
|
|
14
|
+
Released under the MIT License, see the LICENSE file.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import ntpath
|
|
21
|
+
import os
|
|
22
|
+
import platform
|
|
23
|
+
import re
|
|
24
|
+
import shlex
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import threading
|
|
29
|
+
from collections.abc import Sequence
|
|
30
|
+
from pathlib import Path, PureWindowsPath
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"LaunchEditorError",
|
|
34
|
+
"guess_editor",
|
|
35
|
+
"launch_editor",
|
|
36
|
+
"position_args",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LaunchEditorError(Exception):
|
|
43
|
+
"""
|
|
44
|
+
Raised when the editor cannot be launched.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Process names, as listed by `ps` on macOS, mapped to the command that opens
|
|
49
|
+
# files in that editor. Where there’s no command-line launcher, the process
|
|
50
|
+
# path maps to itself.
|
|
51
|
+
MACOS_EDITORS = {
|
|
52
|
+
"/Applications/Atom.app/Contents/MacOS/Atom": "atom",
|
|
53
|
+
"/Applications/Atom Beta.app/Contents/MacOS/Atom Beta": (
|
|
54
|
+
"/Applications/Atom Beta.app/Contents/MacOS/Atom Beta"
|
|
55
|
+
),
|
|
56
|
+
"/Applications/Brackets.app/Contents/MacOS/Brackets": "brackets",
|
|
57
|
+
"/Applications/Sublime Text.app/Contents/MacOS/Sublime Text": (
|
|
58
|
+
"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"
|
|
59
|
+
),
|
|
60
|
+
"/Applications/Sublime Text.app/Contents/MacOS/sublime_text": (
|
|
61
|
+
"/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"
|
|
62
|
+
),
|
|
63
|
+
"/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2": (
|
|
64
|
+
"/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"
|
|
65
|
+
),
|
|
66
|
+
"/Applications/Sublime Text Dev.app/Contents/MacOS/Sublime Text": (
|
|
67
|
+
"/Applications/Sublime Text Dev.app/Contents/SharedSupport/bin/subl"
|
|
68
|
+
),
|
|
69
|
+
"/Applications/Visual Studio Code.app/Contents/MacOS/Code": "code",
|
|
70
|
+
"/Applications/Visual Studio Code.app/Contents/MacOS/Electron": "code",
|
|
71
|
+
"/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders": (
|
|
72
|
+
"code-insiders"
|
|
73
|
+
),
|
|
74
|
+
"/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron": (
|
|
75
|
+
"code-insiders"
|
|
76
|
+
),
|
|
77
|
+
"/Applications/VSCodium.app/Contents/MacOS/Electron": "codium",
|
|
78
|
+
"/Applications/Cursor.app/Contents/MacOS/Cursor": "cursor",
|
|
79
|
+
"/Applications/Trae.app/Contents/MacOS/Electron": "trae",
|
|
80
|
+
"/Applications/Antigravity.app/Contents/MacOS/Electron": "antigravity",
|
|
81
|
+
"/Applications/AppCode.app/Contents/MacOS/appcode": (
|
|
82
|
+
"/Applications/AppCode.app/Contents/MacOS/appcode"
|
|
83
|
+
),
|
|
84
|
+
"/Applications/CLion.app/Contents/MacOS/clion": (
|
|
85
|
+
"/Applications/CLion.app/Contents/MacOS/clion"
|
|
86
|
+
),
|
|
87
|
+
"/Applications/IntelliJ IDEA.app/Contents/MacOS/idea": (
|
|
88
|
+
"/Applications/IntelliJ IDEA.app/Contents/MacOS/idea"
|
|
89
|
+
),
|
|
90
|
+
"/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea": (
|
|
91
|
+
"/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea"
|
|
92
|
+
),
|
|
93
|
+
"/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea": (
|
|
94
|
+
"/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea"
|
|
95
|
+
),
|
|
96
|
+
"/Applications/PhpStorm.app/Contents/MacOS/phpstorm": (
|
|
97
|
+
"/Applications/PhpStorm.app/Contents/MacOS/phpstorm"
|
|
98
|
+
),
|
|
99
|
+
"/Applications/PyCharm.app/Contents/MacOS/pycharm": (
|
|
100
|
+
"/Applications/PyCharm.app/Contents/MacOS/pycharm"
|
|
101
|
+
),
|
|
102
|
+
"/Applications/PyCharm CE.app/Contents/MacOS/pycharm": (
|
|
103
|
+
"/Applications/PyCharm CE.app/Contents/MacOS/pycharm"
|
|
104
|
+
),
|
|
105
|
+
"/Applications/RubyMine.app/Contents/MacOS/rubymine": (
|
|
106
|
+
"/Applications/RubyMine.app/Contents/MacOS/rubymine"
|
|
107
|
+
),
|
|
108
|
+
"/Applications/WebStorm.app/Contents/MacOS/webstorm": (
|
|
109
|
+
"/Applications/WebStorm.app/Contents/MacOS/webstorm"
|
|
110
|
+
),
|
|
111
|
+
"/Applications/MacVim.app/Contents/MacOS/MacVim": "mvim",
|
|
112
|
+
"/Applications/GoLand.app/Contents/MacOS/goland": (
|
|
113
|
+
"/Applications/GoLand.app/Contents/MacOS/goland"
|
|
114
|
+
),
|
|
115
|
+
"/Applications/Rider.app/Contents/MacOS/rider": (
|
|
116
|
+
"/Applications/Rider.app/Contents/MacOS/rider"
|
|
117
|
+
),
|
|
118
|
+
"/Applications/Zed.app/Contents/MacOS/zed": "zed",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# Process names, as listed by `ps` on Linux, mapped to the command that opens
|
|
122
|
+
# files in that editor. Order matters: earlier entries take precedence.
|
|
123
|
+
LINUX_EDITORS = {
|
|
124
|
+
"atom": "atom",
|
|
125
|
+
"Brackets": "brackets",
|
|
126
|
+
"code-insiders": "code-insiders",
|
|
127
|
+
"code": "code",
|
|
128
|
+
"vscodium": "vscodium",
|
|
129
|
+
"codium": "codium",
|
|
130
|
+
"cursor": "cursor",
|
|
131
|
+
"trae": "trae",
|
|
132
|
+
"antigravity": "antigravity",
|
|
133
|
+
"emacs": "emacs",
|
|
134
|
+
"gvim": "gvim",
|
|
135
|
+
"idea": "idea",
|
|
136
|
+
"idea.sh": "idea",
|
|
137
|
+
"phpstorm": "phpstorm",
|
|
138
|
+
"phpstorm.sh": "phpstorm",
|
|
139
|
+
"pycharm": "pycharm",
|
|
140
|
+
"pycharm.sh": "pycharm",
|
|
141
|
+
"rubymine": "rubymine",
|
|
142
|
+
"rubymine.sh": "rubymine",
|
|
143
|
+
"sublime_text": "subl",
|
|
144
|
+
"vim": "vim",
|
|
145
|
+
"webstorm": "webstorm",
|
|
146
|
+
"webstorm.sh": "webstorm",
|
|
147
|
+
"goland": "goland",
|
|
148
|
+
"goland.sh": "goland",
|
|
149
|
+
"rider": "rider",
|
|
150
|
+
"rider.sh": "rider",
|
|
151
|
+
"zed": "zed",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Executable names of editors on Windows. A running one is launched by its
|
|
155
|
+
# full path.
|
|
156
|
+
WINDOWS_EDITORS = [
|
|
157
|
+
"Brackets.exe",
|
|
158
|
+
"Code.exe",
|
|
159
|
+
"Code - Insiders.exe",
|
|
160
|
+
"VSCodium.exe",
|
|
161
|
+
"Cursor.exe",
|
|
162
|
+
"atom.exe",
|
|
163
|
+
"sublime_text.exe",
|
|
164
|
+
"notepad++.exe",
|
|
165
|
+
"clion.exe",
|
|
166
|
+
"clion64.exe",
|
|
167
|
+
"idea.exe",
|
|
168
|
+
"idea64.exe",
|
|
169
|
+
"phpstorm.exe",
|
|
170
|
+
"phpstorm64.exe",
|
|
171
|
+
"pycharm.exe",
|
|
172
|
+
"pycharm64.exe",
|
|
173
|
+
"rubymine.exe",
|
|
174
|
+
"rubymine64.exe",
|
|
175
|
+
"webstorm.exe",
|
|
176
|
+
"webstorm64.exe",
|
|
177
|
+
"goland.exe",
|
|
178
|
+
"goland64.exe",
|
|
179
|
+
"rider.exe",
|
|
180
|
+
"rider64.exe",
|
|
181
|
+
"Trae.exe",
|
|
182
|
+
"zed.exe",
|
|
183
|
+
"Antigravity.exe",
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
# Editors that run in the terminal, taking it over until they exit.
|
|
187
|
+
TERMINAL_EDITORS = {"emacs", "hx", "micro", "nano", "nvim", "vi", "vim"}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def guess_editor(editor: str | Sequence[str] | None = None) -> list[str]:
|
|
191
|
+
"""
|
|
192
|
+
Return the command for the user’s editor, or an empty list if none is
|
|
193
|
+
found.
|
|
194
|
+
|
|
195
|
+
The editor is chosen from, in order:
|
|
196
|
+
|
|
197
|
+
1. The ``editor`` argument.
|
|
198
|
+
2. The ``LAUNCH_EDITOR`` environment variable.
|
|
199
|
+
3. A supported editor that is currently running.
|
|
200
|
+
4. The ``VISUAL`` environment variable.
|
|
201
|
+
5. The ``EDITOR`` environment variable.
|
|
202
|
+
"""
|
|
203
|
+
if editor:
|
|
204
|
+
if isinstance(editor, str):
|
|
205
|
+
return _split_command(editor)
|
|
206
|
+
return list(editor)
|
|
207
|
+
|
|
208
|
+
launch_editor_env = os.environ.get("LAUNCH_EDITOR")
|
|
209
|
+
if launch_editor_env:
|
|
210
|
+
# Like the npm package, treat this as a single command, which is often
|
|
211
|
+
# a script that receives the file, line, and column.
|
|
212
|
+
return [launch_editor_env]
|
|
213
|
+
|
|
214
|
+
running = _guess_running_editor()
|
|
215
|
+
if running is not None:
|
|
216
|
+
return [running]
|
|
217
|
+
|
|
218
|
+
for var in ("VISUAL", "EDITOR"):
|
|
219
|
+
value = os.environ.get(var)
|
|
220
|
+
if value:
|
|
221
|
+
return _split_command(value)
|
|
222
|
+
|
|
223
|
+
return []
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _split_command(command: str) -> list[str]:
|
|
227
|
+
return shlex.split(command, posix=(sys.platform != "win32"))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _guess_running_editor() -> str | None:
|
|
231
|
+
if sys.platform == "darwin":
|
|
232
|
+
args = ["ps", "x", "-o", "comm="]
|
|
233
|
+
parse = _editor_from_macos_processes
|
|
234
|
+
elif sys.platform == "win32":
|
|
235
|
+
args = [
|
|
236
|
+
"powershell",
|
|
237
|
+
"-NoProfile",
|
|
238
|
+
"-Command",
|
|
239
|
+
(
|
|
240
|
+
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
|
|
241
|
+
+ "Get-CimInstance -Query "
|
|
242
|
+
+ '"select executablepath from win32_process '
|
|
243
|
+
+ 'where executablepath is not null" '
|
|
244
|
+
+ "| % { $_.ExecutablePath }"
|
|
245
|
+
),
|
|
246
|
+
]
|
|
247
|
+
parse = _editor_from_windows_processes
|
|
248
|
+
elif sys.platform == "linux":
|
|
249
|
+
args = ["ps", "x", "--no-heading", "-o", "comm", "--sort=comm"]
|
|
250
|
+
parse = _editor_from_linux_processes
|
|
251
|
+
else:
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
output = subprocess.run(
|
|
256
|
+
args,
|
|
257
|
+
stdin=subprocess.DEVNULL,
|
|
258
|
+
stderr=subprocess.DEVNULL,
|
|
259
|
+
stdout=subprocess.PIPE,
|
|
260
|
+
encoding="utf-8",
|
|
261
|
+
errors="replace",
|
|
262
|
+
check=True,
|
|
263
|
+
).stdout
|
|
264
|
+
except (OSError, subprocess.CalledProcessError):
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
return parse(output)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _editor_from_macos_processes(output: str) -> str | None:
|
|
271
|
+
processes = output.splitlines()
|
|
272
|
+
for process_name, command in MACOS_EDITORS.items():
|
|
273
|
+
if process_name in processes:
|
|
274
|
+
return command
|
|
275
|
+
# Find editors installed outside /Applications.
|
|
276
|
+
suffix = process_name.removeprefix("/Applications")
|
|
277
|
+
if suffix in output:
|
|
278
|
+
# Use the command-line launcher, if there is one.
|
|
279
|
+
if command != process_name:
|
|
280
|
+
return command
|
|
281
|
+
# Otherwise, use the running process’ path.
|
|
282
|
+
for process in processes:
|
|
283
|
+
if process.endswith(suffix):
|
|
284
|
+
return process
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _editor_from_windows_processes(output: str) -> str | None:
|
|
289
|
+
for line in output.splitlines():
|
|
290
|
+
path = line.strip()
|
|
291
|
+
if PureWindowsPath(path).name in WINDOWS_EDITORS:
|
|
292
|
+
return path
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _editor_from_linux_processes(output: str) -> str | None:
|
|
297
|
+
processes = set(output.split())
|
|
298
|
+
for process_name, command in LINUX_EDITORS.items():
|
|
299
|
+
if process_name in processes:
|
|
300
|
+
return command
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Editor names, grouped by how they take a position.
|
|
305
|
+
_COLON_POSITION_EDITORS = {
|
|
306
|
+
"Atom",
|
|
307
|
+
"Atom Beta",
|
|
308
|
+
"atom",
|
|
309
|
+
"charm",
|
|
310
|
+
"hx",
|
|
311
|
+
"subl",
|
|
312
|
+
"sublime",
|
|
313
|
+
"sublime_text",
|
|
314
|
+
"wstorm",
|
|
315
|
+
"zed",
|
|
316
|
+
}
|
|
317
|
+
_VSCODE_LIKE_EDITORS = {
|
|
318
|
+
"Antigravity",
|
|
319
|
+
"Code",
|
|
320
|
+
"Code - Insiders",
|
|
321
|
+
"Cursor",
|
|
322
|
+
"Trae",
|
|
323
|
+
"VSCodium",
|
|
324
|
+
"antigravity",
|
|
325
|
+
"code",
|
|
326
|
+
"code-insiders",
|
|
327
|
+
"codium",
|
|
328
|
+
"cursor",
|
|
329
|
+
"trae",
|
|
330
|
+
"vscodium",
|
|
331
|
+
}
|
|
332
|
+
_JETBRAINS_EDITORS = {
|
|
333
|
+
"appcode",
|
|
334
|
+
"clion",
|
|
335
|
+
"clion64",
|
|
336
|
+
"goland",
|
|
337
|
+
"goland64",
|
|
338
|
+
"idea",
|
|
339
|
+
"idea64",
|
|
340
|
+
"phpstorm",
|
|
341
|
+
"phpstorm64",
|
|
342
|
+
"pycharm",
|
|
343
|
+
"pycharm64",
|
|
344
|
+
"rider",
|
|
345
|
+
"rider64",
|
|
346
|
+
"rubymine",
|
|
347
|
+
"rubymine64",
|
|
348
|
+
"webstorm",
|
|
349
|
+
"webstorm64",
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def position_args(
|
|
354
|
+
editor: str, filename: str, line: int, column: int | None = None
|
|
355
|
+
) -> list[str]:
|
|
356
|
+
"""
|
|
357
|
+
Return the arguments to open ``filename`` at ``line`` and ``column`` in
|
|
358
|
+
the given editor command.
|
|
359
|
+
"""
|
|
360
|
+
if column is None:
|
|
361
|
+
column = 1
|
|
362
|
+
name = PureWindowsPath(editor).name if "\\" in editor else Path(editor).name
|
|
363
|
+
name = re.sub(r"\.(exe|cmd|bat)$", "", name, flags=re.IGNORECASE)
|
|
364
|
+
|
|
365
|
+
if name in _COLON_POSITION_EDITORS:
|
|
366
|
+
return [f"{filename}:{line}:{column}"]
|
|
367
|
+
if name == "notepad++":
|
|
368
|
+
return [f"-n{line}", f"-c{column}", filename]
|
|
369
|
+
if name in {"vim", "mvim", "nvim"}:
|
|
370
|
+
return [f"+call cursor({line}, {column})", filename]
|
|
371
|
+
if name in {"joe", "gvim", "vi"}:
|
|
372
|
+
return [f"+{line}", filename]
|
|
373
|
+
if name in {"emacs", "emacsclient", "micro"}:
|
|
374
|
+
return [f"+{line}:{column}", filename]
|
|
375
|
+
if name == "nano":
|
|
376
|
+
return [f"+{line},{column}", filename]
|
|
377
|
+
if name in {"rmate", "mate", "mine"}:
|
|
378
|
+
return ["--line", str(line), filename]
|
|
379
|
+
if name in _VSCODE_LIKE_EDITORS:
|
|
380
|
+
return ["-r", "-g", f"{filename}:{line}:{column}"]
|
|
381
|
+
if name in _JETBRAINS_EDITORS:
|
|
382
|
+
return ["--line", str(line), "--column", str(column), filename]
|
|
383
|
+
|
|
384
|
+
if os.environ.get("LAUNCH_EDITOR"):
|
|
385
|
+
return [filename, str(line), str(column)]
|
|
386
|
+
|
|
387
|
+
# For other editors, drop the position, since passing it incorrectly can
|
|
388
|
+
# result in errors or confusing behaviour.
|
|
389
|
+
return [filename]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
_terminal_editor: subprocess.Popen[bytes] | None = None
|
|
393
|
+
_terminal_editor_lock = threading.Lock()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def launch_editor(
|
|
397
|
+
filename: str | os.PathLike[str],
|
|
398
|
+
line: int | None = None,
|
|
399
|
+
column: int | None = None,
|
|
400
|
+
*,
|
|
401
|
+
editor: str | Sequence[str] | None = None,
|
|
402
|
+
) -> subprocess.Popen[bytes]:
|
|
403
|
+
"""
|
|
404
|
+
Open ``filename`` in the user’s editor, at ``line`` and ``column`` if
|
|
405
|
+
given.
|
|
406
|
+
|
|
407
|
+
The editor is picked by :func:`guess_editor`, using ``editor`` if given.
|
|
408
|
+
GUI editors are started in the background and this function returns
|
|
409
|
+
immediately. Terminal editors, like Vim, take over the current terminal;
|
|
410
|
+
while one is open, further calls to open a terminal editor fail.
|
|
411
|
+
|
|
412
|
+
Returns the editor process. Raises :class:`LaunchEditorError` if the
|
|
413
|
+
editor cannot be launched.
|
|
414
|
+
"""
|
|
415
|
+
global _terminal_editor
|
|
416
|
+
|
|
417
|
+
filename = os.fspath(filename)
|
|
418
|
+
|
|
419
|
+
if sys.platform == "win32" and ntpath.abspath(filename).startswith("\\\\"):
|
|
420
|
+
raise LaunchEditorError(
|
|
421
|
+
f"Cannot open {filename}: UNC paths are not supported on Windows to "
|
|
422
|
+
+ "avoid security issues. See "
|
|
423
|
+
+ "https://github.com/vitejs/launch-editor/tree/main/packages/"
|
|
424
|
+
+ "launch-editor#unc-paths-on-windows for details."
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
# Like the npm package, use an absolute path, so a filename starting with
|
|
428
|
+
# "-" cannot be interpreted as an option by the editor.
|
|
429
|
+
filename = os.path.abspath(filename)
|
|
430
|
+
|
|
431
|
+
if not os.path.exists(filename):
|
|
432
|
+
raise LaunchEditorError(f"File does not exist: {filename}")
|
|
433
|
+
|
|
434
|
+
command = guess_editor(editor)
|
|
435
|
+
if not command:
|
|
436
|
+
raise LaunchEditorError(
|
|
437
|
+
"Could not find an editor. Set the LAUNCH_EDITOR, VISUAL, or EDITOR "
|
|
438
|
+
+ "environment variable."
|
|
439
|
+
)
|
|
440
|
+
editor_command, *args = command
|
|
441
|
+
|
|
442
|
+
if (
|
|
443
|
+
sys.platform == "linux"
|
|
444
|
+
and filename.startswith("/mnt/")
|
|
445
|
+
and "microsoft" in platform.release().lower()
|
|
446
|
+
):
|
|
447
|
+
# Assume WSL is being used, and that the file is on the Windows file
|
|
448
|
+
# system. Windows editors can translate the path, but only if it is
|
|
449
|
+
# relative. Prefix it with "./" so it cannot be interpreted as an
|
|
450
|
+
# option.
|
|
451
|
+
filename = "./" + os.path.relpath(filename)
|
|
452
|
+
|
|
453
|
+
if line is not None:
|
|
454
|
+
args += position_args(editor_command, filename, line, column)
|
|
455
|
+
else:
|
|
456
|
+
args.append(filename)
|
|
457
|
+
|
|
458
|
+
with _terminal_editor_lock:
|
|
459
|
+
is_terminal = Path(editor_command).name in TERMINAL_EDITORS
|
|
460
|
+
if is_terminal and _terminal_editor is not None:
|
|
461
|
+
if _terminal_editor.poll() is None:
|
|
462
|
+
raise LaunchEditorError(
|
|
463
|
+
f"{Path(editor_command).name} is already open in the "
|
|
464
|
+
+ "terminal. Close it first."
|
|
465
|
+
)
|
|
466
|
+
_terminal_editor = None
|
|
467
|
+
|
|
468
|
+
process = _run(editor_command, args)
|
|
469
|
+
|
|
470
|
+
if is_terminal:
|
|
471
|
+
_terminal_editor = process
|
|
472
|
+
|
|
473
|
+
threading.Thread(
|
|
474
|
+
name=f"launch_editor-wait-{process.pid}",
|
|
475
|
+
target=_report_failure,
|
|
476
|
+
args=(process, editor_command, filename),
|
|
477
|
+
daemon=True,
|
|
478
|
+
).start()
|
|
479
|
+
return process
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _run(editor: str, args: list[str]) -> subprocess.Popen[bytes]:
|
|
483
|
+
executable = shutil.which(editor) or editor
|
|
484
|
+
try:
|
|
485
|
+
if sys.platform == "win32" and executable.lower().endswith((".cmd", ".bat")):
|
|
486
|
+
# Windows runs batch files, such as VS Code’s code.cmd, with
|
|
487
|
+
# cmd.exe, which applies its own parsing to the command line. Quote
|
|
488
|
+
# every argument, and reject characters that cmd.exe would still
|
|
489
|
+
# interpret inside quotes, to prevent command injection.
|
|
490
|
+
return subprocess.Popen(_cmd_command_line([executable, *args]))
|
|
491
|
+
return subprocess.Popen([executable, *args])
|
|
492
|
+
except FileNotFoundError as exc:
|
|
493
|
+
raise LaunchEditorError(
|
|
494
|
+
f"Could not run {editor!r}: command not found on PATH."
|
|
495
|
+
) from exc
|
|
496
|
+
except OSError as exc:
|
|
497
|
+
raise LaunchEditorError(f"Could not run {editor!r}: {exc}") from exc
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
_CMD_UNSAFE_CHARS = {
|
|
501
|
+
'"': "a double quote",
|
|
502
|
+
"%": 'a percent sign ("%")',
|
|
503
|
+
"!": 'an exclamation mark ("!")',
|
|
504
|
+
"\r": "a line break",
|
|
505
|
+
"\n": "a line break",
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _cmd_command_line(args: list[str]) -> str:
|
|
510
|
+
for arg in args:
|
|
511
|
+
for char, description in _CMD_UNSAFE_CHARS.items():
|
|
512
|
+
if char in arg:
|
|
513
|
+
raise LaunchEditorError(
|
|
514
|
+
f"Cannot safely pass {arg!r} to {args[0]!r}: it contains "
|
|
515
|
+
+ f"{description}, which cmd.exe would interpret, allowing "
|
|
516
|
+
+ "command injection. Use an editor that is an executable "
|
|
517
|
+
+ "rather than a batch file, or rename the file. See "
|
|
518
|
+
+ "https://github.com/vitejs/launch-editor/blob/v2.14.2/"
|
|
519
|
+
+ "packages/launch-editor/construct-windows-launch-command.js "
|
|
520
|
+
+ "for details."
|
|
521
|
+
)
|
|
522
|
+
return " ".join(f'"{arg}"' for arg in args)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _report_failure(
|
|
526
|
+
process: subprocess.Popen[bytes], editor: str, filename: str
|
|
527
|
+
) -> None:
|
|
528
|
+
returncode = process.wait()
|
|
529
|
+
if returncode:
|
|
530
|
+
logger.warning(
|
|
531
|
+
"Could not open %s in the editor: %r exited with code %d.",
|
|
532
|
+
os.path.basename(filename),
|
|
533
|
+
editor,
|
|
534
|
+
returncode,
|
|
535
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import shlex
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from launch_editor import LaunchEditorError, guess_editor, launch_editor
|
|
10
|
+
|
|
11
|
+
_POSITION_RE = re.compile(r":(\d+)(?::(\d+))?$")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
prog="python -m launch_editor",
|
|
17
|
+
description="Open a file in your editor.",
|
|
18
|
+
allow_abbrev=False,
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"file",
|
|
22
|
+
metavar="FILE",
|
|
23
|
+
nargs="?",
|
|
24
|
+
help=(
|
|
25
|
+
"The file to open, optionally followed by :LINE or :LINE:COLUMN, "
|
|
26
|
+
+ "like example.py:12:4."
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--editor",
|
|
31
|
+
help="The editor command to use, instead of guessing.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--guess",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Print the editor command that would be used, and exit.",
|
|
37
|
+
)
|
|
38
|
+
args = parser.parse_args(argv)
|
|
39
|
+
|
|
40
|
+
if args.guess:
|
|
41
|
+
command = guess_editor(args.editor)
|
|
42
|
+
if not command:
|
|
43
|
+
print("No editor found.", file=sys.stderr)
|
|
44
|
+
return 1
|
|
45
|
+
print(shlex.join(command))
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
if args.file is None:
|
|
49
|
+
parser.error("FILE is required.")
|
|
50
|
+
|
|
51
|
+
filename = args.file
|
|
52
|
+
line = column = None
|
|
53
|
+
match = _POSITION_RE.search(filename)
|
|
54
|
+
if match:
|
|
55
|
+
filename = filename[: match.start()]
|
|
56
|
+
line = int(match[1])
|
|
57
|
+
if match[2] is not None:
|
|
58
|
+
column = int(match[2])
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
process = launch_editor(filename, line, column, editor=args.editor)
|
|
62
|
+
except LaunchEditorError as exc:
|
|
63
|
+
print(exc, file=sys.stderr)
|
|
64
|
+
return 1
|
|
65
|
+
|
|
66
|
+
# Wait, so terminal editors keep the terminal, and to report failures.
|
|
67
|
+
return process.wait()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__": # pragma: no cover
|
|
71
|
+
raise SystemExit(main())
|
|
File without changes
|