2018-02-14 01:51:39 +00:00
|
|
|
# "Open External Editor" integration
|
2017-08-08 11:00:12 +00:00
|
|
|
|
|
|
|
GitHub Desktop supports the user choosing an external program to open their
|
2018-06-17 16:22:11 +00:00
|
|
|
local repositories, and this is available from the top-level **Repository** menu
|
2018-02-08 03:12:49 +00:00
|
|
|
or when right-clicking on a repository in the sidebar.
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
### My favourite editor XYZ isn't supported!
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 04:49:58 +00:00
|
|
|
This is the checklist of things that it needs to support:
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
- the editor supports opening a directory, not just a file
|
|
|
|
- the editor is installed by the user, so there is a reliable way to find it
|
|
|
|
on the user's machine
|
|
|
|
- it comes with a command-line interface that can be launched by Desktop
|
2017-10-13 04:49:58 +00:00
|
|
|
|
2017-10-24 22:53:05 +00:00
|
|
|
If you think your editor satisfies all these please read on to understand how
|
2017-10-13 05:42:14 +00:00
|
|
|
Desktop integrates with each OS, and if you're still keen to integrate this
|
|
|
|
please fork and contribute a pull request for the team to review.
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
## Windows
|
|
|
|
|
|
|
|
The source for the editor integration on Windows is found in
|
|
|
|
[`app/src/lib/editors/win32.ts`](https://github.com/desktop/desktop/blob/master/app/src/lib/editors/win32.ts).
|
|
|
|
|
|
|
|
These editors are currently supported:
|
2017-08-08 11:00:12 +00:00
|
|
|
|
|
|
|
- [Atom](https://atom.io/)
|
2018-02-08 03:12:49 +00:00
|
|
|
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
2017-08-08 11:00:12 +00:00
|
|
|
- [Sublime Text](https://www.sublimetext.com/)
|
2018-02-08 03:12:49 +00:00
|
|
|
- [ColdFusion Builder](https://www.adobe.com/products/coldfusion-builder.html)
|
2018-09-19 12:39:20 +00:00
|
|
|
- [Typora](https://typora.io/)
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:31:37 +00:00
|
|
|
These are defined in an enum at the top of the file:
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
```ts
|
|
|
|
export enum ExternalEditor {
|
|
|
|
Atom = 'Atom',
|
|
|
|
VisualStudioCode = 'Visual Studio Code',
|
2018-02-08 03:12:49 +00:00
|
|
|
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
2017-10-13 04:49:58 +00:00
|
|
|
SublimeText = 'Sublime Text',
|
2018-02-08 03:12:49 +00:00
|
|
|
CFBuilder = 'ColdFusion Builder',
|
2018-09-19 12:39:20 +00:00
|
|
|
Typora = 'Typora',
|
2017-10-13 04:49:58 +00:00
|
|
|
}
|
|
|
|
```
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
If you want to add another editor, add a new key to the `ExternalEditor`
|
|
|
|
enum with a friendly name for the value. This will trigger a number of compiler
|
|
|
|
errors, which are places in the module you need to add code.
|
|
|
|
|
|
|
|
The steps for resolving each editor can be found in `findApplication()` and in
|
2017-10-13 04:49:58 +00:00
|
|
|
pseudocode looks like this:
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 04:49:58 +00:00
|
|
|
```ts
|
2017-10-13 05:55:48 +00:00
|
|
|
async function findApplication(editor: ExternalEditor): Promise<string | null> {
|
2017-10-13 04:49:58 +00:00
|
|
|
// find install location in registry
|
|
|
|
// validate installation
|
|
|
|
// find executable to launch
|
|
|
|
}
|
|
|
|
```
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 04:49:58 +00:00
|
|
|
### Step 1: Find the Install Location
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
Windows programs are typically installed by the user. Installers will add
|
|
|
|
entries to the registry to help the OS with cleaning up later, if the user
|
|
|
|
wishes to uninstall. These entries are used by GitHub Desktop to identify
|
|
|
|
relevant programs and where they can be located.
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
The registry locations for each editor are listed in `getRegistryKeys()`.
|
|
|
|
Some editors support multiple install locations, but are structurally the
|
|
|
|
same (for example 64-bit or 32-bit application, or stable and developer
|
|
|
|
channels).
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
```ts
|
|
|
|
function getRegistryKeys(editor: ExternalEditor): ReadonlyArray<string> {
|
|
|
|
switch (editor) {
|
|
|
|
...
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return [
|
|
|
|
// 64-bit version of VSCode
|
|
|
|
'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{EA457B21-F73E-494C-ACAB-524FDE069978}_is1',
|
|
|
|
// 32-bit version of VSCode
|
|
|
|
'HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F8A2A208-72B3-4D61-95FC-8A65D340689B}_is1',
|
|
|
|
]
|
|
|
|
...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
If you're not sure how your editor is installed, check one of these locations:
|
2017-08-08 11:00:12 +00:00
|
|
|
|
|
|
|
- `HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall` -
|
|
|
|
uninstall information about 64-bit Windows software is found here
|
|
|
|
|
|
|
|
- `HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall` -
|
|
|
|
uninstall information about 32-bit Windows software is found here
|
|
|
|
|
|
|
|
- `HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall` -
|
2017-10-13 04:49:58 +00:00
|
|
|
uninstall information for software that doesn't require administrator
|
|
|
|
permissions is found here
|
|
|
|
|
|
|
|
|
2017-10-24 06:40:52 +00:00
|
|
|
Your editor is probably hiding behind a GUID in one of these locations - this
|
2017-12-20 03:39:24 +00:00
|
|
|
is the key that Desktop needs to read the registry and find the installation for your editor.
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 04:49:58 +00:00
|
|
|
### Step 2: Validate The Installation
|
|
|
|
|
|
|
|
As part of installing to the registry, a program will insert a
|
2017-08-08 11:00:12 +00:00
|
|
|
number of key-value pairs - Desktop will enumerate these to ensure it's the
|
|
|
|
application it expects, and identify where the install location of the
|
|
|
|
application.
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
There's two steps to this process. The first step is reading the registry, and
|
|
|
|
you can see this code in `extractApplicationInformation()`:
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
```ts
|
|
|
|
function extractApplicationInformation(
|
|
|
|
editor: ExternalEditor,
|
|
|
|
keys: ReadonlyArray<IRegistryEntry>
|
|
|
|
): { displayName: string; publisher: string; installLocation: string } {
|
|
|
|
let displayName = ''
|
|
|
|
let publisher = ''
|
|
|
|
let installLocation = ''
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
if (
|
|
|
|
editor === ExternalEditor.VisualStudioCode ||
|
|
|
|
editor === ExternalEditor.SublimeText
|
|
|
|
) {
|
|
|
|
for (const item of keys) {
|
|
|
|
if (item.name === 'Inno Setup: Icon Group') {
|
|
|
|
displayName = item.value
|
|
|
|
} else if (item.name === 'Publisher') {
|
|
|
|
publisher = item.value
|
|
|
|
} else if (item.name === 'Inno Setup: App Path') {
|
|
|
|
installLocation = item.value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return { displayName, publisher, installLocation }
|
|
|
|
}
|
|
|
|
|
|
|
|
...
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
If you launch `regedit` and browse to the key associated with your editor, you
|
|
|
|
should see a list like this in the right-hand pane:
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
![](https://user-images.githubusercontent.com/359239/31530323-696543d8-b02b-11e7-9421-3fad76230bea.png)
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
Desktop needs enough information to validate the installation - usually
|
2017-10-13 05:31:37 +00:00
|
|
|
something related to the name of the program, and the identity of the
|
2017-10-13 05:42:14 +00:00
|
|
|
publisher - along with the install location on disk.
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
The second step is to validate the installation, and this is done in
|
|
|
|
`isExpectedInstallation()`:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
function isExpectedInstallation(
|
|
|
|
editor: ExternalEditor,
|
|
|
|
displayName: string,
|
|
|
|
publisher: string
|
|
|
|
): boolean {
|
|
|
|
switch (editor) {
|
|
|
|
...
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return (
|
2018-09-19 12:39:20 +00:00
|
|
|
displayName.startsWith('Microsoft Visual Studio Code') &&
|
2017-10-13 04:49:58 +00:00
|
|
|
publisher === 'Microsoft Corporation'
|
|
|
|
)
|
|
|
|
...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Step 3: Launch the program
|
|
|
|
|
|
|
|
Now that Desktop knows the program is the one it expects, it can use the
|
|
|
|
install location to then find the executable to launch. Many editors provide a
|
2017-10-13 05:42:14 +00:00
|
|
|
shim or standalone tool to manage this, rather than launching the
|
2017-10-13 04:49:58 +00:00
|
|
|
executable directly. Whatever options there are, this should be a known
|
|
|
|
location with an interface that doesn't change between updates.
|
|
|
|
|
|
|
|
```ts
|
|
|
|
function getExecutableShim(
|
|
|
|
editor: ExternalEditor,
|
|
|
|
installLocation: string
|
|
|
|
): string {
|
|
|
|
switch (editor) {
|
|
|
|
...
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return Path.join(installLocation, 'bin', 'code.cmd')
|
|
|
|
...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
Desktop will confirm this file exists on disk before launching - if it's
|
|
|
|
missing or lost it won't let you launch the external editor.
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
## macOS
|
|
|
|
|
|
|
|
The source for the editor integration on macOS is found in
|
|
|
|
[`app/src/lib/editors/darwin.ts`](https://github.com/desktop/desktop/blob/master/app/src/lib/editors/darwin.ts).
|
|
|
|
|
|
|
|
These editors are currently supported:
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 04:49:58 +00:00
|
|
|
- [Atom](https://atom.io/)
|
2018-04-25 23:59:58 +00:00
|
|
|
- [MacVim](https://macvim-dev.github.io/macvim/)
|
2018-02-08 03:12:49 +00:00
|
|
|
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
2017-10-13 04:49:58 +00:00
|
|
|
- [Sublime Text](https://www.sublimetext.com/)
|
2017-11-30 14:06:42 +00:00
|
|
|
- [BBEdit](http://www.barebones.com/products/bbedit/)
|
2018-01-09 09:29:39 +00:00
|
|
|
- [PhpStorm](https://www.jetbrains.com/phpstorm/)
|
2018-01-30 10:27:58 +00:00
|
|
|
- [RubyMine](https://www.jetbrains.com/rubymine/)
|
2018-02-02 17:50:37 +00:00
|
|
|
- [TextMate](https://macromates.com)
|
2018-06-17 16:22:11 +00:00
|
|
|
- [Brackets](http://brackets.io/)
|
2018-05-25 15:42:55 +00:00
|
|
|
- To use Brackets the Command Line shortcut must be installed.
|
|
|
|
- This can be done by opening Brackets, choosing File > Install Command Line Shortcut
|
2018-06-17 16:22:11 +00:00
|
|
|
- [WebStorm](https://www.jetbrains.com/webstorm/)
|
2018-09-19 12:39:20 +00:00
|
|
|
- [Typora](https://typora.io/)
|
2018-06-17 16:22:11 +00:00
|
|
|
|
2017-10-13 05:31:37 +00:00
|
|
|
These are defined in an enum at the top of the file:
|
|
|
|
|
|
|
|
```ts
|
2018-02-08 03:12:49 +00:00
|
|
|
|
2017-10-13 05:31:37 +00:00
|
|
|
export enum ExternalEditor {
|
|
|
|
Atom = 'Atom',
|
2018-04-25 23:59:58 +00:00
|
|
|
MacVim = 'MacVim',
|
2017-10-13 05:31:37 +00:00
|
|
|
VisualStudioCode = 'Visual Studio Code',
|
2018-02-08 03:12:49 +00:00
|
|
|
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
2017-10-13 05:31:37 +00:00
|
|
|
SublimeText = 'Sublime Text',
|
2018-02-08 03:12:49 +00:00
|
|
|
BBEdit = 'BBEdit',
|
|
|
|
PhpStorm = 'PhpStorm',
|
|
|
|
RubyMine = 'RubyMine',
|
|
|
|
TextMate = 'TextMate',
|
2018-05-05 20:51:34 +00:00
|
|
|
Brackets = 'Brackets',
|
2018-06-17 16:22:11 +00:00
|
|
|
WebStorm = 'WebStorm',
|
2018-09-19 12:39:20 +00:00
|
|
|
Typora = 'Typora',
|
2017-10-13 05:31:37 +00:00
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
If you want to add another editor, add a new key to the `ExternalEditor`
|
|
|
|
enum with a friendly name for the value. This will trigger a number of compiler
|
|
|
|
errors, which are places in the module you need to add code.
|
|
|
|
|
|
|
|
The steps for resolving each editor can be found in `findApplication()` and in
|
2017-10-13 05:31:37 +00:00
|
|
|
pseudocode looks like this:
|
|
|
|
|
|
|
|
```ts
|
2017-10-13 05:55:48 +00:00
|
|
|
async function findApplication(editor: ExternalEditor): Promise<string | null> {
|
2017-10-13 05:31:37 +00:00
|
|
|
// find path to installation
|
|
|
|
// find executable to launch
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### Step 1: Find installation path
|
2017-08-08 11:00:12 +00:00
|
|
|
|
|
|
|
macOS programs are packaged as application bundles, and applications can
|
2017-10-13 05:31:37 +00:00
|
|
|
read information from the OS to see if they are present.
|
2017-08-08 11:00:12 +00:00
|
|
|
|
|
|
|
The `CFBundleIdentifier` value in the plist is what applications use to
|
2017-10-12 00:32:10 +00:00
|
|
|
uniquely identify themselves, for example `com.github.GitHubClient` is the
|
2017-10-13 05:31:37 +00:00
|
|
|
identifier for GitHub Desktop.
|
|
|
|
|
|
|
|
The `getBundleIdentifier()` method is the lookup method for this value:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
function getBundleIdentifier(editor: ExternalEditor): string {
|
|
|
|
switch (editor) {
|
|
|
|
...
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return 'com.microsoft.VSCode'
|
|
|
|
...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
AppKit provides an [`API`](https://developer.apple.com/documentation/appkit/nsworkspace/1533086-absolutepathforappbundlewithiden?language=objc)
|
|
|
|
for searching for an application bundle. If it finds an application bundle,
|
|
|
|
it will return the path to the application on disk. Otherwise it will raise an
|
|
|
|
exception.
|
2017-10-13 05:31:37 +00:00
|
|
|
|
|
|
|
### Step 2: Find executable to launch
|
2017-08-08 11:00:12 +00:00
|
|
|
|
2017-10-13 05:42:14 +00:00
|
|
|
With that information, Desktop can resolve the executable and confirm it exists
|
|
|
|
on disk before launching.
|
2017-10-13 05:31:37 +00:00
|
|
|
|
|
|
|
This is done in the `getExecutableShim()` method:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
function getExecutableShim(
|
|
|
|
editor: ExternalEditor,
|
|
|
|
installPath: string
|
|
|
|
): string {
|
|
|
|
switch (editor) {
|
|
|
|
...
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return Path.join(
|
|
|
|
installPath,
|
|
|
|
'Contents',
|
|
|
|
'Resources',
|
|
|
|
'app',
|
|
|
|
'bin',
|
|
|
|
'code'
|
|
|
|
)
|
|
|
|
...
|
|
|
|
}
|
|
|
|
}
|
|
|
|
```
|
2017-10-13 04:49:58 +00:00
|
|
|
|
|
|
|
## Linux
|
|
|
|
|
2017-10-24 06:40:52 +00:00
|
|
|
|
2017-10-24 10:34:15 +00:00
|
|
|
The source for the editor integration on Linux is found in
|
2017-10-24 06:40:52 +00:00
|
|
|
[`app/src/lib/editors/linux.ts`](https://github.com/desktop/desktop/blob/master/app/src/lib/editors/linux.ts).
|
|
|
|
|
|
|
|
These editors are currently supported:
|
|
|
|
|
|
|
|
- [Atom](https://atom.io/)
|
2018-02-08 03:12:49 +00:00
|
|
|
- [Visual Studio Code](https://code.visualstudio.com/) - both stable and Insiders channel
|
2017-10-24 06:40:52 +00:00
|
|
|
- [Sublime Text](https://www.sublimetext.com/)
|
2018-09-19 12:39:20 +00:00
|
|
|
- [Typora](https://typora.io/)
|
2017-10-24 06:40:52 +00:00
|
|
|
|
|
|
|
These are defined in an enum at the top of the file:
|
|
|
|
|
|
|
|
```ts
|
|
|
|
export enum ExternalEditor {
|
|
|
|
Atom = 'Atom',
|
|
|
|
VisualStudioCode = 'Visual Studio Code',
|
2018-02-08 03:12:49 +00:00
|
|
|
VisualStudioCodeInsiders = 'Visual Studio Code (Insiders)',
|
2017-10-24 06:40:52 +00:00
|
|
|
SublimeText = 'Sublime Text',
|
2018-09-19 12:39:20 +00:00
|
|
|
Typora = 'Typora',
|
2017-10-24 06:40:52 +00:00
|
|
|
}
|
2017-10-13 05:00:28 +00:00
|
|
|
```
|
|
|
|
|
2017-10-24 06:40:52 +00:00
|
|
|
If you want to add another editor, add a new key to the `ExternalEditor`
|
|
|
|
enum with a friendly name for the value. This will trigger a compiler
|
|
|
|
error, and you need to add code to `getEditorPath()` to get the source
|
|
|
|
building again.
|
|
|
|
|
|
|
|
### Step 1: Find executable path
|
|
|
|
|
|
|
|
The `getEditorPath()` maps the editor enum to an expected path to the
|
|
|
|
editor executable. Add a new `case` statement for your editor.
|
|
|
|
|
|
|
|
```ts
|
|
|
|
case ExternalEditor.VisualStudioCode:
|
|
|
|
return getPathIfAvailable('/usr/bin/code')
|
|
|
|
```
|
|
|
|
### Step 2: Lookup executable
|
|
|
|
|
|
|
|
Once you've done that, add code to `getAvailableEditors()` so that it checks
|
|
|
|
for your new editor, following the existing patterns.
|
|
|
|
|
|
|
|
```ts
|
|
|
|
export async function getAvailableEditors(): Promise<
|
|
|
|
ReadonlyArray<IFoundEditor<ExternalEditor>>
|
|
|
|
> {
|
|
|
|
const results: Array<IFoundEditor<ExternalEditor>> = []
|
|
|
|
|
|
|
|
const [atomPath, codePath, sublimePath] = await Promise.all([
|
|
|
|
getEditorPath(ExternalEditor.Atom),
|
|
|
|
getEditorPath(ExternalEditor.VisualStudioCode),
|
|
|
|
getEditorPath(ExternalEditor.SublimeText),
|
2018-09-19 12:39:20 +00:00
|
|
|
getEditorPath(ExternalEditor.Typora),
|
2017-10-24 06:40:52 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
|
|
if (codePath) {
|
|
|
|
results.push({ editor: ExternalEditor.VisualStudioCode, path: codePath })
|
|
|
|
}
|
|
|
|
|
|
|
|
...
|
|
|
|
}
|
|
|
|
```
|