Skip to content

Commit

Permalink
Desktop: Fixes #996: Allow editing multiple notes in external editor
Browse files Browse the repository at this point in the history
  • Loading branch information
laurent22 committed Nov 21, 2018
1 parent 897f53b commit 19252af
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 53 deletions.
5 changes: 4 additions & 1 deletion ElectronClient/app/app.js
Expand Up @@ -24,7 +24,7 @@ const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer');

const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
Expand Down Expand Up @@ -802,6 +802,9 @@ class Application extends BaseApplication {
if (Setting.value('clipperServer.autoStart')) {
ClipperServer.instance().start();
}

ExternalEditWatcher.instance().setLogger(reg.logger());
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
}

}
Expand Down
23 changes: 21 additions & 2 deletions ElectronClient/app/gui/NoteList.jsx
Expand Up @@ -16,6 +16,12 @@ const Mark = require('mark.js/dist/mark.min.js');

class NoteListComponent extends React.Component {

constructor() {
super();

this.itemRenderer = this.itemRenderer.bind(this);
}

style() {
const theme = themeStyle(this.props.theme);

Expand Down Expand Up @@ -169,7 +175,10 @@ class NoteListComponent extends React.Component {
menu.popup(bridge().window());
}

itemRenderer(item, theme, width) {
itemRenderer(item) {
const theme = themeStyle(this.props.theme);
const width = this.props.style.width;

const onTitleClick = async (event, item) => {
if (event.ctrlKey) {
event.preventDefault();
Expand Down Expand Up @@ -269,6 +278,14 @@ class NoteListComponent extends React.Component {
titleComp = <span>{displayTitle}</span>
}

const watchedIconStyle = {
paddingRight: 4,
color: theme.color,
};
const watchedIcon = this.props.watchedNoteFiles.indexOf(item.id) < 0 ? null : (
<i style={watchedIconStyle} className={"fa fa-external-link"}></i>
);

// Need to include "todo_completed" in key so that checkbox is updated when
// item is changed via sync.
return <div key={item.id + '_' + item.todo_completed} style={style}>
Expand All @@ -283,6 +300,7 @@ class NoteListComponent extends React.Component {
onDragStart={(event) => onDragStart(event) }
data-id={item.id}
>
{watchedIcon}
{titleComp}
</a>
</div>
Expand Down Expand Up @@ -313,7 +331,7 @@ class NoteListComponent extends React.Component {
style={style}
className={"note-list"}
items={notes}
itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } }
itemRenderer={this.itemRenderer}
></ItemList>
);
}
Expand All @@ -329,6 +347,7 @@ const mapStateToProps = (state) => {
notesParentType: state.notesParentType,
searches: state.searches,
selectedSearchId: state.selectedSearchId,
watchedNoteFiles: state.watchedNoteFiles,
};
};

Expand Down
39 changes: 5 additions & 34 deletions ElectronClient/app/gui/NoteText.jsx
Expand Up @@ -305,6 +305,7 @@ class NoteTextComponent extends React.Component {
eventManager.on('todoToggle', this.onTodoToggle_);

ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ExternalEditWatcher.instance().on('noteChange', this.externalEditWatcher_noteChange);
}

componentWillUnmount() {
Expand All @@ -318,8 +319,7 @@ class NoteTextComponent extends React.Component {
eventManager.removeListener('todoToggle', this.onTodoToggle_);

ResourceFetcher.instance().off('downloadComplete', this.resourceFetcher_downloadComplete);

this.destroyExternalEditWatcher();
ExternalEditWatcher.instance().off('noteChange', this.externalEditWatcher_noteChange);
}

async saveIfNeeded(saveIfNewNote = false) {
Expand All @@ -332,7 +332,7 @@ class NoteTextComponent extends React.Component {
}
await shared.saveNoteButton_press(this);

this.externalEditWatcherUpdateNoteFile(this.state.note);
ExternalEditWatcher.instance().updateNoteFile(this.state.note);
}

async saveOneProperty(name, value) {
Expand Down Expand Up @@ -371,7 +371,6 @@ 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;
Expand All @@ -398,8 +397,6 @@ 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
Expand Down Expand Up @@ -962,42 +959,16 @@ class NoteTextComponent extends React.Component {
return splitStyle.join(marker);
}

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;

this.externalEditWatcher_.off('noteChange', this.externalEditWatcher_noteChange);
this.externalEditWatcher_.stopWatchingAll();
this.externalEditWatcher_ = null;
}

async commandStartExternalEditing() {
try {
await this.externalEditWatcher().openAndWatch(this.state.note);
await ExternalEditWatcher.instance().openAndWatch(this.state.note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}
}

async commandStopExternalEditing() {
this.externalEditWatcherStopWatchingAll();
ExternalEditWatcher.instance().stopWatching(this.state.note.id);
}

async commandSetTags() {
Expand Down
36 changes: 23 additions & 13 deletions ReactNativeClient/lib/services/ExternalEditWatcher.js
Expand Up @@ -9,14 +9,20 @@ const spawn = require('child_process').spawn;

class ExternalEditWatcher {

constructor(dispatch = null) {
constructor() {
this.logger_ = new Logger();
this.dispatch_ = dispatch ? dispatch : (action) => {};
this.dispatch = (action) => {};
this.watcher_ = null;
this.eventEmitter_ = new EventEmitter();
this.skipNextChangeEvent_ = {};
}

static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ExternalEditWatcher();
return this.instance_;
}

on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
Expand All @@ -33,10 +39,6 @@ class ExternalEditWatcher {
return this.logger_;
}

dispatch(action) {
this.dispatch_(action);
}

watch(fileToWatch) {
if (!this.watcher_) {
this.watcher_ = chokidar.watch(fileToWatch);
Expand All @@ -56,9 +58,17 @@ class ExternalEditWatcher {

if (!this.skipNextChangeEvent_[id]) {
const note = await Note.load(id);

if (!note) {
this.logger().warn('Watched note has been deleted: ' + id);
this.stopWatching(id);
return;
}

const noteContent = await shim.fsDriver().readFile(path, 'utf-8');
const updatedNote = await Note.unserializeForEdit(noteContent);
updatedNote.id = id;
updatedNote.parent_id = note.parent_id;
await Note.save(updatedNote);
this.eventEmitter_.emit('noteChange', { id: updatedNote.id });
}
Expand All @@ -82,8 +92,8 @@ class ExternalEditWatcher {
return this.instance_;
}

noteFilePath(note) {
return Setting.value('tempDir') + '/' + note.id + '.md';
noteFilePath(noteId) {
return Setting.value('tempDir') + '/' + noteId + '.md';
}

watchedFiles() {
Expand Down Expand Up @@ -181,15 +191,15 @@ class ExternalEditWatcher {
this.logger().info('ExternalEditWatcher: Started watching ' + filePath);
}

async stopWatching(note) {
if (!note || !note.id) return;
async stopWatching(noteId) {
if (!noteId) return;

const filePath = this.noteFilePath(note);
const filePath = this.noteFilePath(noteId);
if (this.watcher_) this.watcher_.unwatch(filePath);
await shim.fsDriver().remove(filePath);
this.dispatch({
type: 'NOTE_FILE_WATCHER_REMOVE',
id: note.id,
id: noteId,
});
this.logger().info('ExternalEditWatcher: Stopped watching ' + filePath);
}
Expand Down Expand Up @@ -231,7 +241,7 @@ class ExternalEditWatcher {
return;
}

const filePath = this.noteFilePath(note);
const filePath = this.noteFilePath(note.id);
const noteContent = await Note.serializeForEdit(note);
await shim.fsDriver().writeFile(filePath, noteContent, 'utf-8');
return filePath;
Expand Down
15 changes: 12 additions & 3 deletions ReactNativeClient/lib/services/ResourceService.js
Expand Up @@ -36,16 +36,25 @@ class ResourceService extends BaseService {
for (let i = 0; i < notes.length; i++) {
if (notes[i].id === noteId) return notes[i];
}
throw new Error('Invalid note ID: ' + noteId);
// The note may have been deleted since the change was recorded. For example in this case:
// - Note created (Some Change object is recorded)
// - Note is deleted
// - ResourceService indexer runs.
// In that case, there will be a change for the note, but the note will be gone.
return null;
}

for (let i = 0; i < changes.length; i++) {
const change = changes[i];

if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
const note = noteById(change.item_id);
const resourceIds = await Note.linkedResourceIds(note.body);
await NoteResource.setAssociatedResources(note.id, resourceIds);
if (note) {
const resourceIds = await Note.linkedResourceIds(note.body);
await NoteResource.setAssociatedResources(note.id, resourceIds);
} else {
this.logger().warn('ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ' + change.item_id);
}
} else if (change.type === ItemChange.TYPE_DELETE) {
await NoteResource.remove(change.item_id);
} else {
Expand Down

0 comments on commit 19252af

Please sign in to comment.