You've already forked joplin
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:
@@ -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'],
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
2088
ElectronClient/app/package-lock.json
generated
2088
ElectronClient/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
|
194
ReactNativeClient/lib/services/ExternalEditWatcher.js
Normal file
194
ReactNativeClient/lib/services/ExternalEditWatcher.js
Normal 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;
|
Reference in New Issue
Block a user