1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-13 22:12:50 +02:00

Electron: Resolves #611: Allow opening and editing note in external editor

This commit is contained in:
Laurent Cozic
2018-06-18 18:56:07 +00:00
parent a8b58aaec3
commit 1f3a1c49df
7 changed files with 1627 additions and 768 deletions

View File

@@ -41,6 +41,7 @@ const appDefaultState = Object.assign({}, defaultState, {
noteVisiblePanes: ['editor', 'viewer'],
sidebarVisibility: true,
windowContentSize: bridge().windowContentSize(),
watchedNoteFiles: [],
});
class Application extends BaseApplication {
@@ -142,6 +143,33 @@ class Application extends BaseApplication {
newState.sidebarVisibility = action.visibility;
break;
case 'NOTE_FILE_WATCHER_ADD':
if (newState.watchedNoteFiles.indexOf(action.id) < 0) {
newState = Object.assign({}, state);
const watchedNoteFiles = newState.watchedNoteFiles.slice();
watchedNoteFiles.push(action.id);
newState.watchedNoteFiles = watchedNoteFiles;
}
break;
case 'NOTE_FILE_WATCHER_REMOVE':
newState = Object.assign({}, state);
const idx = newState.watchedNoteFiles.indexOf(action.id);
if (idx >= 0) {
const watchedNoteFiles = newState.watchedNoteFiles.slice();
watchedNoteFiles.splice(idx, 1);
newState.watchedNoteFiles = watchedNoteFiles;
}
break;
case 'NOTE_FILE_WATCHER_CLEAR':
newState = Object.assign({}, state);
newState.watchedNoteFiles = [];
break;
}
} catch (error) {
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
@@ -417,6 +445,16 @@ class Application extends BaseApplication {
}, {
type: 'separator',
screens: ['Main'],
}, {
label: _('Edit in external editor'),
screens: ['Main'],
accelerator: 'CommandOrControl+E',
click: () => {
this.dispatch({
type: 'WINDOW_COMMAND',
name: 'commandStartExternalEditing',
});
},
}, {
label: _('Search in all the notes'),
screens: ['Main'],

View File

@@ -27,6 +27,7 @@ const ArrayUtils = require('lib/ArrayUtils');
const urlUtils = require('lib/urlUtils');
const dialogs = require('./dialogs');
const markdownUtils = require('lib/markdownUtils');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
require('brace/mode/markdown');
// https://ace.c9.io/build/kitchen-sink.html
@@ -144,6 +145,13 @@ class NoteTextComponent extends React.Component {
this.aceEditor_focus = (event) => {
updateSelectionRange();
}
this.externalEditWatcher_noteChange = (event) => {
if (!this.state.note || !this.state.note.id) return;
if (event.id === this.state.note.id) {
this.reloadNote(this.props);
}
}
}
// Note:
@@ -244,6 +252,8 @@ class NoteTextComponent extends React.Component {
eventManager.removeListener('alarmChange', this.onAlarmChange_);
eventManager.removeListener('noteTypeToggle', this.onNoteTypeToggle_);
eventManager.removeListener('todoToggle', this.onTodoToggle_);
this.destroyExternalEditWatcher();
}
async saveIfNeeded(saveIfNewNote = false) {
@@ -255,6 +265,8 @@ class NoteTextComponent extends React.Component {
if (!shared.isModified(this)) return;
}
await shared.saveNoteButton_press(this);
this.externalEditWatcherUpdateNoteFile(this.state.note);
}
async saveOneProperty(name, value) {
@@ -292,6 +304,7 @@ class NoteTextComponent extends React.Component {
if (props.newNote) {
note = Object.assign({}, props.newNote);
this.lastLoadedNoteId_ = null;
this.externalEditWatcherStopWatchingAll();
} else {
noteId = props.noteId;
loadingNewNote = stateNoteId !== noteId;
@@ -317,6 +330,8 @@ class NoteTextComponent extends React.Component {
// Scroll back to top when loading new note
if (loadingNewNote) {
this.externalEditWatcherStopWatchingAll();
this.editorMaxScrollTop_ = 0;
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
@@ -721,6 +736,8 @@ class NoteTextComponent extends React.Component {
this.commandTextBold();
} else if (command.name === 'textItalic') {
this.commandTextItalic();
} else if (command.name === 'commandStartExternalEditing') {
this.commandStartExternalEditing();
} else {
commandProcessed = false;
}
@@ -775,6 +792,42 @@ class NoteTextComponent extends React.Component {
});
}
externalEditWatcher() {
if (!this.externalEditWatcher_) {
this.externalEditWatcher_ = new ExternalEditWatcher((action) => { return this.props.dispatch(action) });
this.externalEditWatcher_.setLogger(reg.logger());
this.externalEditWatcher_.on('noteChange', this.externalEditWatcher_noteChange);
}
return this.externalEditWatcher_;
}
externalEditWatcherUpdateNoteFile(note) {
if (this.externalEditWatcher_) this.externalEditWatcher().updateNoteFile(note);
}
externalEditWatcherStopWatchingAll() {
if (this.externalEditWatcher_) this.externalEditWatcher().stopWatchingAll();
}
destroyExternalEditWatcher() {
if (!this.externalEditWatcher_) return;
console.info(this.externalEditWatcher_);
this.externalEditWatcher_.off('noteChange', this.externalEditWatcher_noteChange);
this.externalEditWatcher_.stopWatchingAll();
this.externalEditWatcher_ = null;
}
async commandStartExternalEditing() {
this.externalEditWatcher().openAndWatch(this.state.note);
}
async commandStopExternalEditing() {
this.externalEditWatcherStopWatchingAll();
}
async commandSetTags() {
await this.saveIfNeeded(true);
@@ -1030,6 +1083,21 @@ class NoteTextComponent extends React.Component {
type: 'separator',
});
if (note && this.props.watchedNoteFiles.indexOf(note.id) >= 0) {
toolbarItems.push({
tooltip: _('Click to stop external editing'),
title: _('Watching...'),
iconName: 'fa-external-link',
onClick: () => { return this.commandStopExternalEditing(); },
});
} else {
toolbarItems.push({
tooltip: _('Edit in external editor'),
iconName: 'fa-external-link',
onClick: () => { return this.commandStartExternalEditing(); },
});
}
toolbarItems.push({
tooltip: _('Tags'),
iconName: 'fa-tags',
@@ -1275,6 +1343,7 @@ const mapStateToProps = (state) => {
notesParentType: state.notesParentType,
searches: state.searches,
selectedSearchId: state.selectedSearchId,
watchedNoteFiles: state.watchedNoteFiles,
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -79,6 +79,7 @@
"app-module-path": "^2.2.0",
"async-mutex": "^0.1.3",
"base-64": "^0.1.0",
"chokidar": "^2.0.3",
"compare-versions": "^3.2.1",
"electron-context-menu": "^0.9.1",
"electron-is-dev": "^0.3.0",

View File

@@ -429,6 +429,8 @@ class BaseApplication {
Setting.setConstant('resourceDir', resourceDir);
Setting.setConstant('tempDir', tempDir);
await shim.fsDriver().remove(tempDir);
await fs.mkdirp(profileDir, 0o755);
await fs.mkdirp(resourceDir, 0o755);
await fs.mkdirp(tempDir, 0o755);

View File

@@ -48,7 +48,8 @@ class FsDriverNode extends FsDriverBase {
// same as rm -rf
async remove(path) {
try {
return await fs.remove(path);
const r = await fs.remove(path);
return r;
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}

View File

@@ -0,0 +1,194 @@
const { Logger } = require('lib/logger.js');
const Note = require('lib/models/Note');
const Setting = require('lib/models/Setting');
const { shim } = require('lib/shim');
const chokidar = require('chokidar');
const EventEmitter = require('events');
class ExternalEditWatcher {
constructor(dispatch = null) {
this.logger_ = new Logger();
this.dispatch_ = dispatch ? dispatch : (action) => {};
this.watcher_ = null;
this.eventEmitter_ = new EventEmitter();
this.skipNextChangeEvent_ = {};
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
off(eventName, callback) {
return this.eventEmitter_.removeListener(eventName, callback);
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
dispatch(action) {
this.dispatch_(action);
}
watch(fileToWatch) {
if (!this.watcher_) {
this.watcher_ = chokidar.watch(fileToWatch);
this.watcher_.on('all', async (event, path) => {
this.logger().debug('ExternalEditWatcher: Event: ' + event + ': ' + path);
if (event === 'unlink') {
this.watcher_.unwatch(path);
} else if (event === 'change') {
const id = Note.pathToId(path);
if (!this.skipNextChangeEvent_[id]) {
const note = await Note.load(id);
const noteContent = await shim.fsDriver().readFile(path, 'utf-8');
const updatedNote = await Note.unserializeForEdit(noteContent);
updatedNote.id = id;
await Note.save(updatedNote);
this.eventEmitter_.emit('noteChange', { id: updatedNote.id });
}
this.skipNextChangeEvent_ = {};
} else if (event === 'error') {
this.logger().error('ExternalEditWatcher:');
this.logger().error(error)
}
});
} else {
this.watcher_.add(fileToWatch);
}
return this.watcher_;
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ExternalEditWatcher();
return this.instance_;
}
noteFilePath(note) {
return Setting.value('tempDir') + '/' + note.id + '.md';
}
watchedFiles() {
if (!this.watcher_) return [];
const output = [];
const watchedPaths = this.watcher_.getWatched();
for (let dirName in watchedPaths) {
if (!watchedPaths.hasOwnProperty(dirName)) continue;
for (let i = 0; i < watchedPaths[dirName].length; i++) {
const f = watchedPaths[dirName][i];
output.push(Setting.value('tempDir') + '/' + f);
}
}
return output;
}
noteIsWatched(note) {
if (!this.watcher_) return false;
const noteFilename = Note.systemPath(note);
const watchedPaths = this.watcher_.getWatched();
for (let dirName in watchedPaths) {
if (!watchedPaths.hasOwnProperty(dirName)) continue;
for (let i = 0; i < watchedPaths[dirName].length; i++) {
const f = watchedPaths[dirName][i];
if (f === noteFilename) return true;
}
}
return false;
}
async openAndWatch(note) {
if (!note || !note.id) {
this.logger().warn('ExternalEditWatcher: Cannot open note: ', note);
return;
}
const filePath = await this.writeNoteToFile_(note);
this.watch(filePath);
bridge().openExternal('file://' + filePath);
this.dispatch({
type: 'NOTE_FILE_WATCHER_ADD',
id: note.id,
});
this.logger().info('ExternalEditWatcher: Started watching ' + filePath);
}
async stopWatching(note) {
if (!note || !note.id) return;
const filePath = this.noteFilePath(note);
if (this.watcher_) this.watcher_.unwatch(filePath);
await shim.fsDriver().remove(filePath);
this.dispatch({
type: 'NOTE_FILE_WATCHER_REMOVE',
id: note.id,
});
this.logger().info('ExternalEditWatcher: Stopped watching ' + filePath);
}
async stopWatchingAll() {
const filePaths = this.watchedFiles();
for (let i = 0; i < filePaths.length; i++) {
await shim.fsDriver().remove(filePaths[i]);
}
if (this.watcher_) this.watcher_.close();
this.watcher_ = null;
this.logger().info('ExternalEditWatcher: Stopped watching all files');
this.dispatch({
type: 'NOTE_FILE_WATCHER_CLEAR',
});
}
async updateNoteFile(note) {
if (!this.noteIsWatched(note)) return;
if (!note || !note.id) {
this.logger().warn('ExternalEditWatcher: Cannot update note file: ', note);
return;
}
this.logger().debug('ExternalEditWatcher: Update note file: ' + note.id);
// When the note file is updated programmatically, we skip the next change event to
// avoid update loops. We only want to listen to file changes made by the user.
this.skipNextChangeEvent_[note.id] = true;
this.writeNoteToFile_(note);
}
async writeNoteToFile_(note) {
if (!note || !note.id) {
this.logger().warn('ExternalEditWatcher: Cannot update note file: ', note);
return;
}
const filePath = this.noteFilePath(note);
const noteContent = await Note.serializeForEdit(note);
await shim.fsDriver().writeFile(filePath, noteContent, 'utf-8');
return filePath;
}
}
module.exports = ExternalEditWatcher;