diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index 0b635eb31..ffa6baa94 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -846,7 +846,11 @@ class NoteTextComponent extends React.Component { } async commandStartExternalEditing() { - this.externalEditWatcher().openAndWatch(this.state.note); + try { + await this.externalEditWatcher().openAndWatch(this.state.note); + } catch (error) { + bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message)); + } } async commandStopExternalEditing() { diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js index 081cb8212..64f650c9d 100644 --- a/ReactNativeClient/lib/models/Setting.js +++ b/ReactNativeClient/lib/models/Setting.js @@ -32,7 +32,6 @@ class Setting extends BaseModel { this.metadata_ = { 'activeFolderId': { value: '', type: Setting.TYPE_STRING, public: false }, 'firstStart': { value: true, type: Setting.TYPE_BOOL, public: false }, - 'editor': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['cli'], label: () => _('Text editor'), description: () => _('The editor that will be used to open a note. If none is provided it will try to auto-detect the default editor.') }, 'locale': { value: defaultLocale(), type: Setting.TYPE_STRING, isEnum: true, public: true, label: () => _('Language'), options: () => { return ObjectUtils.sortByValue(supportedLocalesToLanguages()); }}, @@ -116,6 +115,7 @@ class Setting extends BaseModel { }}, 'noteVisiblePanes': { value: ['editor', 'viewer'], type: Setting.TYPE_ARRAY, public: false, appTypes: ['desktop'] }, 'sidebarVisibility': { value: true, type: Setting.TYPE_BOOL, public: false, appTypes: ['desktop'] }, + 'editor': { value: '', type: Setting.TYPE_STRING, public: true, appTypes: ['cli', 'desktop'], label: () => _('Text editor command'), description: () => _('The editor command (may include arguments) that will be used to open a note. If none is provided it will try to auto-detect the default editor.') }, 'showAdvancedOptions': { value: false, type: Setting.TYPE_BOOL, public: true, appTypes: ['mobile' ], label: () => _('Show advanced options') }, 'sync.target': { value: SyncTargetRegistry.nameToId('dropbox'), type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation target'), description: (appType) => { return appType !== 'cli' ? null : _('The target to synchonise to. Each sync target may have additional parameters which are named as `sync.NUM.NAME` (all documented below).') }, options: () => { return SyncTargetRegistry.idAndLabelPlainObject(); diff --git a/ReactNativeClient/lib/services/ExternalEditWatcher.js b/ReactNativeClient/lib/services/ExternalEditWatcher.js index 2e9164cab..745bb223a 100644 --- a/ReactNativeClient/lib/services/ExternalEditWatcher.js +++ b/ReactNativeClient/lib/services/ExternalEditWatcher.js @@ -4,6 +4,8 @@ const Setting = require('lib/models/Setting'); const { shim } = require('lib/shim'); const chokidar = require('chokidar'); const EventEmitter = require('events'); +const { splitCommandString } = require('lib/string-utils'); +const spawn = require('child_process').spawn; class ExternalEditWatcher { @@ -115,6 +117,40 @@ class ExternalEditWatcher { return false; } + textEditorCommand() { + const editorCommand = Setting.value('editor'); + if (!editorCommand) return null; + + const s = splitCommandString(editorCommand); + + const path = s.splice(0, 1); + if (!path.length) throw new Error('Invalid editor command: ' + editorCommand); + + return { + path: path[0], + args: s, + }; + } + + async spawnCommand(path, args, options) { + return new Promise((resolve, reject) => { + const subProcess = spawn(path, args, options); + + const iid = setInterval(() => { + if (subProcess && subProcess.pid) { + this.logger().debug('Started editor with PID ' + subProcess.pid); + clearInterval(iid); + resolve(); + } + }, 100); + + subProcess.on('error', (error) => { + clearInterval(iid); + reject(error); + }); + }); + } + async openAndWatch(note) { if (!note || !note.id) { this.logger().warn('ExternalEditWatcher: Cannot open note: ', note); @@ -123,7 +159,14 @@ class ExternalEditWatcher { const filePath = await this.writeNoteToFile_(note); this.watch(filePath); - bridge().openExternal('file://' + filePath); + + const cmd = this.textEditorCommand(); + if (!cmd) { + bridge().openExternal('file://' + filePath); + } else { + cmd.args.push(filePath); + await this.spawnCommand(cmd.path, cmd.args, { detached: true }); + } this.dispatch({ type: 'NOTE_FILE_WATCHER_ADD',