You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
19 Commits
release-1.
...
v1.0.243
Author | SHA1 | Date | |
---|---|---|---|
|
96b7ce9d50 | ||
|
2bbc1e7ecd | ||
|
83619b279d | ||
|
8b5a99d494 | ||
|
67d4123608 | ||
|
a424e3c899 | ||
|
08d4b5a714 | ||
|
68aefd5e4c | ||
|
57d750bc9a | ||
|
fbe966903b | ||
|
652748f969 | ||
|
e108fdb1d8 | ||
|
a8296e2e37 | ||
|
0998fc0ad7 | ||
|
d5f3e860b9 | ||
|
4e624f7db5 | ||
|
5b697b7e16 | ||
|
30e0d69a74 | ||
|
652816fd26 |
@@ -62,6 +62,7 @@ Modules/TinyMCE/langs/
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
CliClient/app/LinkSelector.js
|
||||
CliClient/build/LinkSelector.js
|
||||
CliClient/tests/synchronizer_LockHandler.js
|
||||
CliClient/tests/synchronizer_MigrationHandler.js
|
||||
ElectronClient/commands/focusElement.js
|
||||
@@ -70,6 +71,12 @@ ElectronClient/commands/stopExternalEditing.js
|
||||
ElectronClient/global.d.js
|
||||
ElectronClient/gui/ErrorBoundary.js
|
||||
ElectronClient/gui/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
|
||||
ElectronClient/gui/KeymapConfig/styles/index.js
|
||||
ElectronClient/gui/KeymapConfig/utils/getLabel.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
|
||||
ElectronClient/gui/MainScreen/commands/editAlarm.js
|
||||
ElectronClient/gui/MainScreen/commands/exportPdf.js
|
||||
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
|
||||
@@ -97,12 +104,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
|
15
.gitignore
vendored
15
.gitignore
vendored
@@ -50,9 +50,12 @@ joplin-webclipper-source.zip
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
*.map
|
||||
ReactNativeClient/lib/sql-extensions/
|
||||
!ReactNativeClient/lib/sql-extensions/spellfix.dll
|
||||
|
||||
# AUTO-GENERATED - EXCLUDED TYPESCRIPT BUILD
|
||||
CliClient/app/LinkSelector.js
|
||||
CliClient/build/LinkSelector.js
|
||||
CliClient/tests/synchronizer_LockHandler.js
|
||||
CliClient/tests/synchronizer_MigrationHandler.js
|
||||
ElectronClient/commands/focusElement.js
|
||||
@@ -61,6 +64,12 @@ ElectronClient/commands/stopExternalEditing.js
|
||||
ElectronClient/global.d.js
|
||||
ElectronClient/gui/ErrorBoundary.js
|
||||
ElectronClient/gui/Header/commands/focusSearch.js
|
||||
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.js
|
||||
ElectronClient/gui/KeymapConfig/ShortcutRecorder.js
|
||||
ElectronClient/gui/KeymapConfig/styles/index.js
|
||||
ElectronClient/gui/KeymapConfig/utils/getLabel.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.js
|
||||
ElectronClient/gui/KeymapConfig/utils/useKeymap.js
|
||||
ElectronClient/gui/MainScreen/commands/editAlarm.js
|
||||
ElectronClient/gui/MainScreen/commands/exportPdf.js
|
||||
ElectronClient/gui/MainScreen/commands/hideModalMessage.js
|
||||
@@ -88,12 +97,6 @@ ElectronClient/gui/NoteEditor/commands/focusElementNoteBody.js
|
||||
ElectronClient/gui/NoteEditor/commands/focusElementNoteTitle.js
|
||||
ElectronClient/gui/NoteEditor/commands/showLocalSearch.js
|
||||
ElectronClient/gui/NoteEditor/commands/showRevisions.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/AceEditor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/styles/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/Toolbar.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/index.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/types.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/AceEditor/utils/useListIdent.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/CodeMirror.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/Editor.js
|
||||
ElectronClient/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js
|
||||
|
@@ -2,6 +2,9 @@ const gulp = require('gulp');
|
||||
const fs = require('fs-extra');
|
||||
const utils = require('../Tools/gulp/utils');
|
||||
const tasks = {
|
||||
compileExtensions: {
|
||||
fn: require('../Tools/gulp/tasks/compileExtensions.js'),
|
||||
},
|
||||
copyLib: require('../Tools/gulp/tasks/copyLib'),
|
||||
tsc: require('../Tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
@@ -53,10 +56,12 @@ utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
gulp.task('build', gulp.series([
|
||||
'prepareBuild',
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
]));
|
||||
|
||||
gulp.task('buildTests', gulp.series([
|
||||
'prepareTestBuild',
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
]));
|
||||
|
@@ -4,7 +4,10 @@ require('app-module-path').addPath(__dirname);
|
||||
const filterParser = require('lib/services/searchengine/filterParser.js').default;
|
||||
// import filterParser from 'lib/services/searchengine/filterParser.js';
|
||||
|
||||
const makeTerm = (name, value, negated) => { return { name, value, negated }; };
|
||||
const makeTerm = (name, value, negated, quoted = false) => {
|
||||
if (name !== 'text') { return { name, value, negated }; } else { return { name, value, negated, quoted }; }
|
||||
};
|
||||
|
||||
describe('filterParser should be correct filter for keyword', () => {
|
||||
it('title', () => {
|
||||
const searchString = 'title: something';
|
||||
@@ -65,7 +68,7 @@ describe('filterParser should be correct filter for keyword', () => {
|
||||
|
||||
it('phrase text search', () => {
|
||||
const searchString = '"scott joplin"';
|
||||
expect(filterParser(searchString)).toContain(makeTerm('text', '"scott joplin"', false));
|
||||
expect(filterParser(searchString)).toContain(makeTerm('text', '"scott joplin"', false, true));
|
||||
});
|
||||
|
||||
it('multi word body', () => {
|
||||
|
@@ -24,7 +24,7 @@ describe('pathUtils', function() {
|
||||
['no space at the end ', 'no space at the end'],
|
||||
['nor dots...', 'nor dots'],
|
||||
[' no space before either', 'no space before either'],
|
||||
['thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong', 'thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong'],
|
||||
['thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong', 'thatsreallylongthatsreallylongthatsreallylongthats'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
|
@@ -3,7 +3,7 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const { asyncTest, fileContentEqual, expectNotThrow, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const InteropService = require('lib/services/InteropService.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
@@ -442,4 +442,26 @@ describe('services_InteropService', function() {
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/testexportfolder/textexportnote2.md`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not try to export folders with a non-existing parent', asyncTest(async () => {
|
||||
// Handles and edge case where user has a folder but this folder with a parent
|
||||
// that doesn't exist. Can happen for example in this case:
|
||||
//
|
||||
// - folder1/folder2
|
||||
// - Client 1 sync folder2, but not folder1
|
||||
// - Client 2 sync and get folder2 only
|
||||
// - Client 2 export data
|
||||
// => Crash if we don't handle this case
|
||||
|
||||
await Folder.save({ title: 'orphan', parent_id: '0c5bbd8a1b5a48189484a412a7e534cc' });
|
||||
|
||||
const service = new InteropService();
|
||||
|
||||
const result = await service.export({
|
||||
path: exportDir(),
|
||||
format: 'md',
|
||||
});
|
||||
|
||||
expect(result.warnings.length).toBe(0);
|
||||
}));
|
||||
|
||||
});
|
||||
|
@@ -115,6 +115,7 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+T' },
|
||||
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Option+Shift+Cmd+B' },
|
||||
];
|
||||
|
||||
testCases_Darwin.forEach(({ command, accelerator }) => {
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
@@ -131,6 +132,7 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
|
||||
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
|
||||
];
|
||||
|
||||
testCases_Linux.forEach(({ command, accelerator }) => {
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
@@ -138,10 +140,10 @@ describe('services_KeymapService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAccelerator', () => {
|
||||
describe('getDefaultAccelerator', () => {
|
||||
beforeEach(() => keymapService.initialize());
|
||||
|
||||
it('should reset the Accelerator', () => {
|
||||
it('should return the default accelerator', () => {
|
||||
const testCases = [
|
||||
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
|
||||
{ command: 'synchronize', accelerator: null /* Disabled */ },
|
||||
@@ -154,26 +156,22 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
testCases.forEach(({ command, accelerator }) => {
|
||||
// Remember the default Accelerator value
|
||||
const prevAccelerator = keymapService.getAccelerator(command);
|
||||
// Remember the real default Accelerator value
|
||||
const defaultAccelerator = keymapService.getAccelerator(command);
|
||||
|
||||
// Update the Accelerator,
|
||||
// Update the Accelerator and then retrieve the default accelerator
|
||||
keymapService.setAccelerator(command, accelerator);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
|
||||
// and then reset it..
|
||||
keymapService.resetAccelerator(command);
|
||||
expect(keymapService.getAccelerator(command)).toEqual(prevAccelerator);
|
||||
expect(keymapService.getDefaultAccelerator(command)).toEqual(defaultAccelerator);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setKeymap', () => {
|
||||
describe('overrideKeymap', () => {
|
||||
beforeEach(() => keymapService.initialize());
|
||||
|
||||
it('should update the keymap', () => {
|
||||
keymapService.initialize('darwin');
|
||||
const customKeymap_Darwin = [
|
||||
const customKeymapItems_Darwin = [
|
||||
{ command: 'newNote', accelerator: 'Option+Shift+Cmd+N' },
|
||||
{ command: 'synchronize', accelerator: 'F11' },
|
||||
{ command: 'textBold', accelerator: 'Shift+F5' },
|
||||
@@ -187,14 +185,14 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteList', accelerator: 'Shift+Cmd+S' /* Default of focusElementSideBar */ },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap_Darwin)).not.toThrow();
|
||||
customKeymap_Darwin.forEach(({ command, accelerator }) => {
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems_Darwin)).not.toThrow();
|
||||
customKeymapItems_Darwin.forEach(({ command, accelerator }) => {
|
||||
// Also check if keymap is updated or not
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
});
|
||||
|
||||
keymapService.initialize('win32');
|
||||
const customKeymap_Win32 = [
|
||||
const customKeymapItems_Win32 = [
|
||||
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
|
||||
{ command: 'synchronize', accelerator: 'F11' },
|
||||
{ command: 'textBold', accelerator: 'Shift+F5' },
|
||||
@@ -208,8 +206,8 @@ describe('services_KeymapService', () => {
|
||||
{ command: 'focusElementNoteList', accelerator: 'Ctrl+Shift+S' /* Default of focusElementSideBar */ },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap_Win32)).not.toThrow();
|
||||
customKeymap_Win32.forEach(({ command, accelerator }) => {
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems_Win32)).not.toThrow();
|
||||
customKeymapItems_Win32.forEach(({ command, accelerator }) => {
|
||||
// Also check if keymap is updated or not
|
||||
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
|
||||
});
|
||||
@@ -240,30 +238,30 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps.length; i++) {
|
||||
const customKeymap = customKeymaps[i];
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
const customKeymapItems = customKeymaps[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw when the provided Accelerators are invalid', () => {
|
||||
// Only one test case is provided since KeymapService.validateAccelerator() is already tested
|
||||
const customKeymap = [
|
||||
const customKeymapItems = [
|
||||
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
|
||||
{ command: 'print', accelerator: 'Alt+P' },
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J+O+P+L+I+N' },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when the provided commands are invalid', () => {
|
||||
const customKeymap = [
|
||||
const customKeymapItems = [
|
||||
{ command: 'totallyInvalidCommand', accelerator: 'Ctrl+Shift+G' },
|
||||
{ command: 'print', accelerator: 'Alt+P' },
|
||||
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
|
||||
];
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when duplicate accelerators are provided', () => {
|
||||
@@ -281,14 +279,8 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps_Darwin.length; i++) {
|
||||
const customKeymap = customKeymaps_Darwin[i];
|
||||
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
// All items should be reset to default values
|
||||
for (let j = 0; j < customKeymap.length; j++) {
|
||||
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
|
||||
}
|
||||
const customKeymapItems = customKeymaps_Darwin[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
|
||||
const customKeymaps_Linux = [
|
||||
@@ -305,14 +297,8 @@ describe('services_KeymapService', () => {
|
||||
];
|
||||
|
||||
for (let i = 0; i < customKeymaps_Linux.length; i++) {
|
||||
const customKeymap = customKeymaps_Linux[i];
|
||||
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
|
||||
|
||||
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
|
||||
|
||||
for (let j = 0; j < customKeymap.length; j++) {
|
||||
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
|
||||
}
|
||||
const customKeymapItems = customKeymaps_Linux[i];
|
||||
expect(() => keymapService.overrideKeymap(customKeymapItems)).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -426,7 +426,7 @@ describe('services_SearchEngine', function() {
|
||||
const t = testCases[i];
|
||||
const input = t[0];
|
||||
const expected = t[1];
|
||||
const actual = engine.parseQuery(input);
|
||||
const actual = await engine.parseQuery(input);
|
||||
|
||||
const _Values = actual.terms._ ? actual.terms._.map(v => v.value) : undefined;
|
||||
const titleValues = actual.terms.title ? actual.terms.title.map(v => v.value) : undefined;
|
||||
|
145
CliClient/tests/services_SearchFuzzy.js
Normal file
145
CliClient/tests/services_SearchFuzzy.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint prefer-const: 0*/
|
||||
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, asyncTest, db, synchronizer, fileApi, sleep, createNTestNotes, switchClient, createNTestFolders } = require('test-utils.js');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const ItemChange = require('lib/models/ItemChange');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const ResourceService = require('lib/services/ResourceService.js');
|
||||
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
let engine = null;
|
||||
|
||||
const ids = (array) => array.map(a => a.id);
|
||||
|
||||
describe('services_SearchFuzzy', function() {
|
||||
beforeEach(async (done) => {
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
|
||||
engine = new SearchEngine();
|
||||
engine.setDb(db());
|
||||
|
||||
Setting.setValue('db.fuzzySearchEnabled', 1);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('should return note almost matching title', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'If It Ain\'t Baroque, Don\'t Fix It' });
|
||||
const n2 = await Note.save({ title: 'Important note' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('Broke', { fuzzy: false });
|
||||
expect(rows.length).toBe(0);
|
||||
|
||||
rows = await engine.search('Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('title:Broke', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('title:"Broke"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
rows = await engine.search('Imprtant', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n2.id);
|
||||
}));
|
||||
|
||||
|
||||
it('should order results by min fuzziness', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'I demand you take me to him' });
|
||||
const n2 = await Note.save({ title: 'He demanded an answer' });
|
||||
const n3 = await Note.save({ title: 'Don\'t you make demands of me' });
|
||||
const n4 = await Note.save({ title: 'No drama for me' });
|
||||
const n5 = await Note.save({ title: 'Just minding my own business' });
|
||||
|
||||
await engine.syncTables();
|
||||
rows = await engine.search('demand', { fuzzy: false });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
|
||||
|
||||
rows = await engine.search('demand', { fuzzy: true });
|
||||
expect(rows.length).toBe(3);
|
||||
expect(rows[0].id).toBe(n1.id);
|
||||
expect(rows[1].id).toBe(n3.id);
|
||||
expect(rows[2].id).toBe(n2.id);
|
||||
}));
|
||||
|
||||
it('should consider any:1', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'cat' });
|
||||
const n2 = await Note.save({ title: 'cats' });
|
||||
const n3 = await Note.save({ title: 'cot' });
|
||||
|
||||
const n4 = await Note.save({ title: 'defenestrate' });
|
||||
const n5 = await Note.save({ title: 'defenstrate' });
|
||||
const n6 = await Note.save({ title: 'defenestrated' });
|
||||
|
||||
const n7 = await Note.save({ title: 'he defenestrated the cat' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
|
||||
rows = await engine.search('any:1 defenestrated cat', { fuzzy: true });
|
||||
expect(rows.length).toBe(7);
|
||||
}));
|
||||
|
||||
it('should leave phrase searches alone', asyncTest(async () => {
|
||||
let rows;
|
||||
const n1 = await Note.save({ title: 'abc def' });
|
||||
const n2 = await Note.save({ title: 'def ghi' });
|
||||
const n3 = await Note.save({ title: 'ghi jkl' });
|
||||
const n4 = await Note.save({ title: 'def abc' });
|
||||
const n5 = await Note.save({ title: 'mno pqr ghi jkl' });
|
||||
|
||||
await engine.syncTables();
|
||||
|
||||
rows = await engine.search('abc def', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n4.id);
|
||||
|
||||
rows = await engine.search('"abc def"', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n1.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl"', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('"ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(1);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
|
||||
rows = await engine.search('any:1 "ghi jkl" mno', { fuzzy: true });
|
||||
expect(rows.length).toBe(2);
|
||||
expect(rows.map(r=>r.id)).toContain(n3.id);
|
||||
expect(rows.map(r=>r.id)).toContain(n5.id);
|
||||
}));
|
||||
|
||||
|
||||
});
|
@@ -30,7 +30,7 @@ const PluginManager = require('lib/services/PluginManager');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const KeymapService = require('lib/services/KeymapService.js').default;
|
||||
const KeymapService = require('lib/services/KeymapService').default;
|
||||
const TemplateUtils = require('lib/TemplateUtils');
|
||||
const CssUtils = require('lib/CssUtils');
|
||||
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
|
||||
@@ -110,6 +110,8 @@ class Application extends BaseApplication {
|
||||
|
||||
this.commandService_commandsEnabledStateChange = this.commandService_commandsEnabledStateChange.bind(this);
|
||||
CommandService.instance().on('commandsEnabledStateChange', this.commandService_commandsEnabledStateChange);
|
||||
|
||||
KeymapService.instance().on('keymapChange', this.refreshMenu.bind(this));
|
||||
}
|
||||
|
||||
commandService_commandsEnabledStateChange() {
|
||||
@@ -569,7 +571,7 @@ class Application extends BaseApplication {
|
||||
const toolsItemsWindowsLinux = toolsItemsFirst.concat([{
|
||||
label: _('Options'),
|
||||
visible: !shim.isMac(),
|
||||
accelerator: shim.isMac() ? null : keymapService.getAccelerator('config'),
|
||||
accelerator: !shim.isMac() && keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -631,7 +633,7 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Preferences...'),
|
||||
visible: shim.isMac() ? true : false,
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('config') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('config'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
@@ -663,7 +665,7 @@ class Application extends BaseApplication {
|
||||
visible: shim.isMac() ? false : true,
|
||||
submenu: importItems,
|
||||
}, {
|
||||
label: _('Export'),
|
||||
label: _('Export all'),
|
||||
visible: shim.isMac() ? false : true,
|
||||
submenu: exportItems,
|
||||
}, {
|
||||
@@ -680,7 +682,7 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Hide %s', 'Joplin'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('hideApp') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('hideApp'),
|
||||
click: () => { bridge().electronApp().hide(); },
|
||||
}, {
|
||||
type: 'separator',
|
||||
@@ -700,7 +702,7 @@ class Application extends BaseApplication {
|
||||
newNotebookItem, {
|
||||
label: _('Close Window'),
|
||||
platforms: ['darwin'],
|
||||
accelerator: shim.isMac() ? keymapService.getAccelerator('closeWindow') : null,
|
||||
accelerator: shim.isMac() && keymapService.getAccelerator('closeWindow'),
|
||||
selector: 'performClose:',
|
||||
}, {
|
||||
type: 'separator',
|
||||
@@ -1040,11 +1042,9 @@ class Application extends BaseApplication {
|
||||
// https://github.com/laurent22/joplin/issues/155
|
||||
|
||||
const css = `.CodeMirror * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const ace_css = `.ace_editor * { font-family: ${fontFamilies.join(', ')} !important; }`;
|
||||
const styleTag = document.createElement('style');
|
||||
styleTag.type = 'text/css';
|
||||
styleTag.appendChild(document.createTextNode(css));
|
||||
styleTag.appendChild(document.createTextNode(ace_css));
|
||||
document.head.appendChild(styleTag);
|
||||
}
|
||||
|
||||
@@ -1093,7 +1093,7 @@ class Application extends BaseApplication {
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
try {
|
||||
await KeymapService.instance().loadKeymap(`${dir}/keymap-desktop.json`);
|
||||
await keymapService.loadCustomKeymap(`${dir}/keymap-desktop.json`);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ const shared = require('lib/components/shared/config-shared.js');
|
||||
const ConfigMenuBar = require('./ConfigMenuBar.min.js');
|
||||
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min');
|
||||
const { ClipperConfigScreen } = require('./ClipperConfigScreen.min');
|
||||
const { KeymapConfigScreen } = require('./KeymapConfig/KeymapConfigScreen');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
constructor() {
|
||||
@@ -68,6 +69,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
screenFromName(screenName) {
|
||||
if (screenName === 'encryption') return <EncryptionConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'server') return <ClipperConfigScreen theme={this.props.theme}/>;
|
||||
if (screenName === 'keymap') return <KeymapConfigScreen themeId={this.props.theme}/>;
|
||||
|
||||
throw new Error(`Invalid screen name: ${screenName}`);
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ const { themeStyle } = require('lib/theme');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const CommandService = require('lib/services/CommandService').default;
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const commands = [
|
||||
require('./commands/focusSearch'),
|
||||
@@ -28,7 +29,7 @@ class HeaderComponent extends React.Component {
|
||||
|
||||
const triggerOnQuery = query => {
|
||||
clearTimeout(this.scheduleSearchChangeEventIid_);
|
||||
if (this.searchOnQuery_) this.searchOnQuery_(query);
|
||||
if (this.searchOnQuery_) this.searchOnQuery_(query, Setting.value('db.fuzzySearchEnabled'));
|
||||
this.scheduleSearchChangeEventIid_ = null;
|
||||
};
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
192
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx
Normal file
192
ElectronClient/gui/KeymapConfig/KeymapConfigScreen.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
|
||||
import KeymapService, { KeymapItem } from '../../lib/services/KeymapService';
|
||||
import { ShortcutRecorder } from './ShortcutRecorder';
|
||||
import getLabel from './utils/getLabel';
|
||||
import useKeymap from './utils/useKeymap';
|
||||
import useCommandStatus from './utils/useCommandStatus';
|
||||
import styles_ from './styles';
|
||||
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { shim } = require('lib/shim');
|
||||
const { _ } = require('lib/locale');
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
export interface KeymapConfigScreenProps {
|
||||
themeId: number
|
||||
}
|
||||
|
||||
export const KeymapConfigScreen = ({ themeId }: KeymapConfigScreenProps) => {
|
||||
const styles = styles_(themeId);
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
const [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator] = useKeymap();
|
||||
const [recorderError, setRecorderError] = useState<Error>(null);
|
||||
const [editing, enableEditing, disableEditing] = useCommandStatus();
|
||||
const [hovering, enableHovering, disableHovering] = useCommandStatus();
|
||||
|
||||
const handleSave = (event: { commandName: string, accelerator: string }) => {
|
||||
const { commandName, accelerator } = event;
|
||||
setAccelerator(commandName, accelerator);
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleReset = (event: { commandName: string }) => {
|
||||
const { commandName } = event;
|
||||
resetAccelerator(commandName);
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleCancel = (event: { commandName: string }) => {
|
||||
const { commandName } = event;
|
||||
disableEditing(commandName);
|
||||
};
|
||||
|
||||
const handleError = (event: { recorderError: Error }) => {
|
||||
const { recorderError } = event;
|
||||
setRecorderError(recorderError);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
const filePath = bridge().showOpenDialog({
|
||||
properties: ['openFile'],
|
||||
defaultPath: 'keymap-desktop',
|
||||
filters: [{ name: 'Joplin Keymaps (keymap-desktop.json)', extensions: ['json'] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
const actualFilePath = filePath[0];
|
||||
try {
|
||||
const keymapFile = await shim.fsDriver().readFile(actualFilePath, 'utf-8');
|
||||
overrideKeymapItems(JSON.parse(keymapFile));
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(`${_('An unexpected error occured while importing the keymap!')}\n${err.message}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const filePath = bridge().showSaveDialog({
|
||||
defaultPath: 'keymap-desktop',
|
||||
filters: [{ name: 'Joplin Keymaps (keymap-desktop.json)', extensions: ['json'] }],
|
||||
});
|
||||
|
||||
if (filePath) {
|
||||
try {
|
||||
// KeymapService is already synchronized with the in-state keymap
|
||||
await keymapService.saveCustomKeymap(filePath);
|
||||
} catch (err) {
|
||||
bridge().showErrorMessageBox(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccelerator = (accelerator: string) => {
|
||||
return (
|
||||
<div>
|
||||
{accelerator.split('+').map(part => <kbd style={styles.kbd} key={part}>{part}</kbd>).reduce(
|
||||
(accumulator, part) => (accumulator.length ? [...accumulator, ' + ', part] : [part]),
|
||||
[]
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatus = (commandName: string) => {
|
||||
if (editing[commandName]) {
|
||||
return (recorderError && <i className="fa fa-exclamation-triangle" title={recorderError.message} />);
|
||||
} else if (hovering[commandName]) {
|
||||
return (<i className="fa fa-pen" />);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderError = (error: Error) => {
|
||||
return (
|
||||
<div style={styles.warning}>
|
||||
<p style={styles.text}>
|
||||
<span>
|
||||
{error.message}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderKeymapRow = ({ command, accelerator }: KeymapItem) => {
|
||||
const handleClick = () => enableEditing(command);
|
||||
const handleMouseEnter = () => enableHovering(command);
|
||||
const handleMouseLeave = () => disableHovering(command);
|
||||
const cellContent =
|
||||
<div style={styles.tableCell} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{editing[command] ?
|
||||
<ShortcutRecorder
|
||||
onSave={handleSave}
|
||||
onReset={handleReset}
|
||||
onCancel={handleCancel}
|
||||
onError={handleError}
|
||||
initialAccelerator={accelerator || '' /* Because accelerator is null if disabled */}
|
||||
commandName={command}
|
||||
themeId={themeId}
|
||||
/> :
|
||||
<div style={styles.tableCellContent} onClick={handleClick}>
|
||||
{accelerator
|
||||
? renderAccelerator(accelerator)
|
||||
: <div style={styles.disabled}>{_('Disabled')}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div style={styles.tableCellStatus} onClick={handleClick}>
|
||||
{renderStatus(command)}
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
return (
|
||||
<tr key={command}>
|
||||
<td style={styles.tableCommandColumn}>
|
||||
{getLabel(command)}
|
||||
</td>
|
||||
<td style={styles.tableShortcutColumn}>
|
||||
{cellContent}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{keymapError && renderError(keymapError)}
|
||||
<div style={styles.container}>
|
||||
<div style={styles.actionsContainer}>
|
||||
<input
|
||||
value={filter}
|
||||
onChange={event => setFilter(event.target.value)}
|
||||
placeholder={_('Search...')}
|
||||
style={styles.filterInput}
|
||||
/>
|
||||
<button style={styles.inlineButton} onClick={handleImport}>{_('Import')}</button>
|
||||
<button style={styles.inlineButton} onClick={handleExport}>{_('Export')}</button>
|
||||
</div>
|
||||
|
||||
<table style={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={styles.tableCommandColumn}>{_('Command')}</th>
|
||||
<th style={styles.tableShortcutColumn}>{_('Keyboard Shortcut')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keymapItems.filter(({ command }) => {
|
||||
const filterLowerCase = filter.toLowerCase();
|
||||
return (command.toLowerCase().includes(filterLowerCase) || getLabel(command).toLowerCase().includes(filterLowerCase));
|
||||
}).map(item => renderKeymapRow(item))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -1,51 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const React = require('react');
|
||||
const react_1 = require('react');
|
||||
const KeymapService_1 = require('../../lib/services/KeymapService');
|
||||
const styles_1 = require('./styles');
|
||||
const { _ } = require('lib/locale');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
exports.ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId }) => {
|
||||
const styles = styles_1.default(themeId);
|
||||
const [accelerator, setAccelerator] = react_1.useState(initialAccelerator);
|
||||
const [saveAllowed, setSaveAllowed] = react_1.useState(true);
|
||||
react_1.useEffect(() => {
|
||||
try {
|
||||
// Only perform validations if there's an accelerator provided
|
||||
// Otherwise performing a save means that it's going to be disabled
|
||||
if (accelerator) {
|
||||
keymapService.validateAccelerator(accelerator);
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
// Discard previous errors
|
||||
onError({ recorderError: null });
|
||||
setSaveAllowed(true);
|
||||
} catch (recorderError) {
|
||||
onError({ recorderError });
|
||||
setSaveAllowed(false);
|
||||
}
|
||||
}, [accelerator]);
|
||||
const handleKeydown = (event) => {
|
||||
event.preventDefault();
|
||||
const newAccelerator = keymapService.domToElectronAccelerator(event);
|
||||
switch (newAccelerator) {
|
||||
case 'Enter':
|
||||
if (saveAllowed) { return onSave({ commandName, accelerator }); }
|
||||
break;
|
||||
case 'Escape':
|
||||
return onCancel({ commandName });
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
return setAccelerator('');
|
||||
default:
|
||||
setAccelerator(newAccelerator);
|
||||
}
|
||||
};
|
||||
return (React.createElement('div', { style: styles.recorderContainer },
|
||||
React.createElement('input', { value: accelerator, placeholder: _('Press the shortcut'), onKeyDown: handleKeydown, style: styles.recorderInput, readOnly: true, autoFocus: true }),
|
||||
React.createElement('button', { style: styles.inlineButton, disabled: !saveAllowed, onClick: () => onSave({ commandName, accelerator }) }, _('Save')),
|
||||
React.createElement('button', { style: styles.inlineButton, onClick: () => onReset({ commandName }) }, _('Restore')),
|
||||
React.createElement('button', { style: styles.inlineButton, onClick: () => onCancel({ commandName }) }, _('Cancel'))));
|
||||
};
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiU2hvcnRjdXRSZWNvcmRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIlNob3J0Y3V0UmVjb3JkZXIudHN4Il0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsK0JBQStCO0FBQy9CLGlDQUEyRDtBQUUzRCxvRUFBNkQ7QUFDN0QscUNBQStCO0FBRS9CLE1BQU0sRUFBRSxDQUFDLEVBQUUsR0FBRyxPQUFPLENBQUMsWUFBWSxDQUFDLENBQUM7QUFDcEMsTUFBTSxhQUFhLEdBQUcsdUJBQWEsQ0FBQyxRQUFRLEVBQUUsQ0FBQztBQVlsQyxRQUFBLGdCQUFnQixHQUFHLENBQUMsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsV0FBVyxFQUFFLE9BQU8sRUFBeUIsRUFBRSxFQUFFO0lBQzNJLE1BQU0sTUFBTSxHQUFHLGdCQUFPLENBQUMsT0FBTyxDQUFDLENBQUM7SUFFaEMsTUFBTSxDQUFDLFdBQVcsRUFBRSxjQUFjLENBQUMsR0FBRyxnQkFBUSxDQUFDLGtCQUFrQixDQUFDLENBQUM7SUFDbkUsTUFBTSxDQUFDLFdBQVcsRUFBRSxjQUFjLENBQUMsR0FBRyxnQkFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBRXJELGlCQUFTLENBQUMsR0FBRyxFQUFFO1FBQ2QsSUFBSTtZQUNILDhEQUE4RDtZQUM5RCxtRUFBbUU7WUFDbkUsSUFBSSxXQUFXLEVBQUU7Z0JBQ2hCLGFBQWEsQ0FBQyxtQkFBbUIsQ0FBQyxXQUFXLENBQUMsQ0FBQztnQkFDL0MsYUFBYSxDQUFDLGNBQWMsQ0FBQyxFQUFFLFdBQVcsRUFBRSxPQUFPLEVBQUUsV0FBVyxFQUFFLENBQUMsQ0FBQzthQUNwRTtZQUVELDBCQUEwQjtZQUMxQixPQUFPLENBQUMsRUFBRSxhQUFhLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztZQUNqQyxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDckI7UUFBQyxPQUFPLGFBQWEsRUFBRTtZQUN2QixPQUFPLENBQUMsRUFBRSxhQUFhLEVBQUUsQ0FBQyxDQUFDO1lBQzNCLGNBQWMsQ0FBQyxLQUFLLENBQUMsQ0FBQztTQUN0QjtJQUNGLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUM7SUFFbEIsTUFBTSxhQUFhLEdBQUcsQ0FBQyxLQUFvQyxFQUFFLEVBQUU7UUFDOUQsS0FBSyxDQUFDLGNBQWMsRUFBRSxDQUFDO1FBQ3ZCLE1BQU0sY0FBYyxHQUFHLGFBQWEsQ0FBQyx3QkFBd0IsQ0FBQyxLQUFLLENBQUMsQ0FBQztRQUVyRSxRQUFRLGNBQWMsRUFBRTtZQUN4QixLQUFLLE9BQU87Z0JBQ1gsSUFBSSxXQUFXO29CQUFFLE9BQU8sTUFBTSxDQUFDLEVBQUUsV0FBVyxFQUFFLFdBQVcsRUFBRSxDQUFDLENBQUM7Z0JBQzdELE1BQU07WUFDUCxLQUFLLFFBQVE7Z0JBQ1osT0FBTyxRQUFRLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxDQUFDO1lBQ2xDLEtBQUssV0FBVyxDQUFDO1lBQ2pCLEtBQUssUUFBUTtnQkFDWixPQUFPLGNBQWMsQ0FBQyxFQUFFLENBQUMsQ0FBQztZQUMzQjtnQkFDQyxjQUFjLENBQUMsY0FBYyxDQUFDLENBQUM7U0FDL0I7SUFDRixDQUFDLENBQUM7SUFFRixPQUFPLENBQ04sNkJBQUssS0FBSyxFQUFFLE1BQU0sQ0FBQyxpQkFBaUI7UUFDbkMsK0JBQ0MsS0FBSyxFQUFFLFdBQVcsRUFDbEIsV0FBVyxFQUFFLENBQUMsQ0FBQyxvQkFBb0IsQ0FBQyxFQUNwQyxTQUFTLEVBQUUsYUFBYSxFQUN4QixLQUFLLEVBQUUsTUFBTSxDQUFDLGFBQWEsRUFDM0IsUUFBUSxRQUNSLFNBQVMsU0FDUjtRQUVGLGdDQUFRLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWSxFQUFFLFFBQVEsRUFBRSxDQUFDLFdBQVcsRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsTUFBTSxDQUFDLEVBQUUsV0FBVyxFQUFFLFdBQVcsRUFBRSxDQUFDLElBQzdHLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FDRjtRQUNULGdDQUFRLEtBQUssRUFBRSxNQUFNLENBQUMsWUFBWSxFQUFFLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxPQUFPLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxJQUN6RSxDQUFDLENBQUMsU0FBUyxDQUFDLENBQ0w7UUFDVCxnQ0FBUSxLQUFLLEVBQUUsTUFBTSxDQUFDLFlBQVksRUFBRSxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsV0FBVyxFQUFFLENBQUMsSUFDMUUsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUNKLENBQ0osQ0FDTixDQUFDO0FBQ0gsQ0FBQyxDQUFDIn0=
|
85
ElectronClient/gui/KeymapConfig/ShortcutRecorder.tsx
Normal file
85
ElectronClient/gui/KeymapConfig/ShortcutRecorder.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, KeyboardEvent } from 'react';
|
||||
|
||||
import KeymapService from '../../lib/services/KeymapService';
|
||||
import styles_ from './styles';
|
||||
|
||||
const { _ } = require('lib/locale');
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
export interface ShortcutRecorderProps {
|
||||
onSave: (event: { commandName: string, accelerator: string }) => void,
|
||||
onReset: (event: { commandName: string }) => void,
|
||||
onCancel: (event: { commandName: string }) => void,
|
||||
onError: (event: { recorderError: Error }) => void,
|
||||
initialAccelerator: string
|
||||
commandName: string,
|
||||
themeId: number
|
||||
}
|
||||
|
||||
export const ShortcutRecorder = ({ onSave, onReset, onCancel, onError, initialAccelerator, commandName, themeId }: ShortcutRecorderProps) => {
|
||||
const styles = styles_(themeId);
|
||||
|
||||
const [accelerator, setAccelerator] = useState(initialAccelerator);
|
||||
const [saveAllowed, setSaveAllowed] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Only perform validations if there's an accelerator provided
|
||||
// Otherwise performing a save means that it's going to be disabled
|
||||
if (accelerator) {
|
||||
keymapService.validateAccelerator(accelerator);
|
||||
keymapService.validateKeymap({ accelerator, command: commandName });
|
||||
}
|
||||
|
||||
// Discard previous errors
|
||||
onError({ recorderError: null });
|
||||
setSaveAllowed(true);
|
||||
} catch (recorderError) {
|
||||
onError({ recorderError });
|
||||
setSaveAllowed(false);
|
||||
}
|
||||
}, [accelerator]);
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
const newAccelerator = keymapService.domToElectronAccelerator(event);
|
||||
|
||||
switch (newAccelerator) {
|
||||
case 'Enter':
|
||||
if (saveAllowed) return onSave({ commandName, accelerator });
|
||||
break;
|
||||
case 'Escape':
|
||||
return onCancel({ commandName });
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
return setAccelerator('');
|
||||
default:
|
||||
setAccelerator(newAccelerator);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.recorderContainer}>
|
||||
<input
|
||||
value={accelerator}
|
||||
placeholder={_('Press the shortcut')}
|
||||
onKeyDown={handleKeydown}
|
||||
style={styles.recorderInput}
|
||||
title={_('Press the shortcut and then press ENTER. Or, press BACKSPACE to clear the shortcut.')}
|
||||
readOnly
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<button style={styles.inlineButton} disabled={!saveAllowed} onClick={() => onSave({ commandName, accelerator })}>
|
||||
{_('Save')}
|
||||
</button>
|
||||
<button style={styles.inlineButton} onClick={() => onReset({ commandName })}>
|
||||
{_('Restore')}
|
||||
</button>
|
||||
<button style={styles.inlineButton} onClick={() => onCancel({ commandName })}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,53 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const { buildStyle } = require('lib/theme');
|
||||
function styles(themeId) {
|
||||
return buildStyle('KeymapConfigScreen', themeId, (theme) => {
|
||||
return {
|
||||
container: Object.assign(Object.assign({}, theme.containerStyle), { padding: 16 }),
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
recorderContainer: {
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
},
|
||||
filterInput: Object.assign(Object.assign({}, theme.inputStyle), { flexGrow: 1, minHeight: 29, alignSelf: 'center' }),
|
||||
recorderInput: Object.assign(Object.assign({}, theme.inputStyle), { minHeight: 29 }),
|
||||
label: Object.assign(Object.assign({}, theme.textStyle), { alignSelf: 'center', marginRight: 10 }),
|
||||
table: Object.assign(Object.assign({}, theme.containerStyle), { marginTop: 16, overflow: 'auto', width: '100%' }),
|
||||
tableShortcutColumn: Object.assign(Object.assign({}, theme.textStyle), { width: '60%' }),
|
||||
tableCommandColumn: Object.assign(Object.assign({}, theme.textStyle), { width: 'auto' }),
|
||||
tableCell: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableCellContent: {
|
||||
flexGrow: 1,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tableCellStatus: {
|
||||
height: '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
kbd: {
|
||||
fontFamily: 'sans-serif',
|
||||
border: '1px solid',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
padding: 2,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
},
|
||||
disabled: {
|
||||
color: theme.colorFaded,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
inlineButton: Object.assign(Object.assign({}, theme.buttonStyle), { marginLeft: 12 }),
|
||||
warning: Object.assign(Object.assign({}, theme.textStyle), { backgroundColor: theme.warningBackgroundColor, paddingLeft: 16, paddingRight: 16, paddingTop: 2, paddingBottom: 2 }),
|
||||
};
|
||||
});
|
||||
}
|
||||
exports.default = styles;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLE1BQU0sRUFBRSxVQUFVLEVBQUUsR0FBRyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUM7QUFFNUMsU0FBd0IsTUFBTSxDQUFDLE9BQWU7SUFDN0MsT0FBTyxVQUFVLENBQUMsb0JBQW9CLEVBQUUsT0FBTyxFQUFFLENBQUMsS0FBVSxFQUFFLEVBQUU7UUFDL0QsT0FBTztZQUNOLFNBQVMsa0NBQ0wsS0FBSyxDQUFDLGNBQWMsS0FDdkIsT0FBTyxFQUFFLEVBQUUsR0FDWDtZQUNELGdCQUFnQixFQUFFO2dCQUNqQixPQUFPLEVBQUUsTUFBTTtnQkFDZixhQUFhLEVBQUUsS0FBSzthQUNwQjtZQUNELGlCQUFpQixFQUFFO2dCQUNsQixPQUFPLEVBQUUsQ0FBQztnQkFDVixRQUFRLEVBQUUsQ0FBQzthQUNYO1lBQ0QsV0FBVyxrQ0FDUCxLQUFLLENBQUMsVUFBVSxLQUNuQixRQUFRLEVBQUUsQ0FBQyxFQUNYLFNBQVMsRUFBRSxFQUFFLEVBQ2IsU0FBUyxFQUFFLFFBQVEsR0FDbkI7WUFDRCxhQUFhLGtDQUNULEtBQUssQ0FBQyxVQUFVLEtBQ25CLFNBQVMsRUFBRSxFQUFFLEdBQ2I7WUFDRCxLQUFLLGtDQUNELEtBQUssQ0FBQyxTQUFTLEtBQ2xCLFNBQVMsRUFBRSxRQUFRLEVBQ25CLFdBQVcsRUFBRSxFQUFFLEdBQ2Y7WUFDRCxLQUFLLGtDQUNELEtBQUssQ0FBQyxjQUFjLEtBQ3ZCLFNBQVMsRUFBRSxFQUFFLEVBQ2IsUUFBUSxFQUFFLE1BQU0sRUFDaEIsS0FBSyxFQUFFLE1BQU0sR0FDYjtZQUNELG1CQUFtQixrQ0FDZixLQUFLLENBQUMsU0FBUyxLQUNsQixLQUFLLEVBQUUsS0FBSyxHQUNaO1lBQ0Qsa0JBQWtCLGtDQUNkLEtBQUssQ0FBQyxTQUFTLEtBQ2xCLEtBQUssRUFBRSxNQUFNLEdBQ2I7WUFDRCxTQUFTLEVBQUU7Z0JBQ1YsT0FBTyxFQUFFLE1BQU07Z0JBQ2YsYUFBYSxFQUFFLEtBQUs7YUFDcEI7WUFDRCxnQkFBZ0IsRUFBRTtnQkFDakIsUUFBUSxFQUFFLENBQUM7Z0JBQ1gsU0FBUyxFQUFFLFFBQVE7YUFDbkI7WUFDRCxlQUFlLEVBQUU7Z0JBQ2hCLE1BQU0sRUFBRSxNQUFNO2dCQUNkLFNBQVMsRUFBRSxRQUFRO2FBQ25CO1lBQ0QsR0FBRyxFQUFFO2dCQUNKLFVBQVUsRUFBRSxZQUFZO2dCQUN4QixNQUFNLEVBQUUsV0FBVztnQkFDbkIsWUFBWSxFQUFFLENBQUM7Z0JBQ2YsZUFBZSxFQUFFLEtBQUssQ0FBQyxxQkFBcUI7Z0JBQzVDLE9BQU8sRUFBRSxDQUFDO2dCQUNWLFdBQVcsRUFBRSxDQUFDO2dCQUNkLFlBQVksRUFBRSxDQUFDO2FBQ2Y7WUFDRCxRQUFRLEVBQUU7Z0JBQ1QsS0FBSyxFQUFFLEtBQUssQ0FBQyxVQUFVO2dCQUN2QixTQUFTLEVBQUUsUUFBUTthQUNuQjtZQUNELFlBQVksa0NBQ1IsS0FBSyxDQUFDLFdBQVcsS0FDcEIsVUFBVSxFQUFFLEVBQUUsR0FDZDtZQUNELE9BQU8sa0NBQ0gsS0FBSyxDQUFDLFNBQVMsS0FDbEIsZUFBZSxFQUFFLEtBQUssQ0FBQyxzQkFBc0IsRUFDN0MsV0FBVyxFQUFFLEVBQUUsRUFDZixZQUFZLEVBQUUsRUFBRSxFQUNoQixVQUFVLEVBQUUsQ0FBQyxFQUNiLGFBQWEsRUFBRSxDQUFDLEdBQ2hCO1NBQ0QsQ0FBQztJQUNILENBQUMsQ0FBQyxDQUFDO0FBQ0osQ0FBQztBQW5GRCx5QkFtRkMifQ==
|
86
ElectronClient/gui/KeymapConfig/styles/index.ts
Normal file
86
ElectronClient/gui/KeymapConfig/styles/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
export default function styles(themeId: number) {
|
||||
return buildStyle('KeymapConfigScreen', themeId, (theme: any) => {
|
||||
return {
|
||||
container: {
|
||||
...theme.containerStyle,
|
||||
padding: 16,
|
||||
},
|
||||
actionsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
recorderContainer: {
|
||||
padding: 2,
|
||||
flexGrow: 1,
|
||||
},
|
||||
filterInput: {
|
||||
...theme.inputStyle,
|
||||
flexGrow: 1,
|
||||
minHeight: 29,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
recorderInput: {
|
||||
...theme.inputStyle,
|
||||
minHeight: 29,
|
||||
},
|
||||
label: {
|
||||
...theme.textStyle,
|
||||
alignSelf: 'center',
|
||||
marginRight: 10,
|
||||
},
|
||||
table: {
|
||||
...theme.containerStyle,
|
||||
marginTop: 16,
|
||||
overflow: 'auto',
|
||||
width: '100%',
|
||||
},
|
||||
tableShortcutColumn: {
|
||||
...theme.textStyle,
|
||||
width: '60%',
|
||||
},
|
||||
tableCommandColumn: {
|
||||
...theme.textStyle,
|
||||
width: 'auto',
|
||||
},
|
||||
tableCell: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tableCellContent: {
|
||||
flexGrow: 1,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
tableCellStatus: {
|
||||
height: '100%',
|
||||
alignSelf: 'center',
|
||||
},
|
||||
kbd: {
|
||||
fontFamily: 'sans-serif',
|
||||
border: '1px solid',
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
padding: 2,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
},
|
||||
disabled: {
|
||||
color: theme.colorFaded,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
inlineButton: {
|
||||
...theme.buttonStyle,
|
||||
marginLeft: 12,
|
||||
},
|
||||
warning: {
|
||||
...theme.textStyle,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const CommandService_1 = require('../../../lib/services/CommandService');
|
||||
const { _ } = require('lib/locale');
|
||||
const { shim } = require('lib/shim');
|
||||
const commandService = CommandService_1.default.instance();
|
||||
const getLabel = (commandName) => {
|
||||
if (commandService.exists(commandName)) { return commandService.label(commandName); }
|
||||
// Some commands are not registered in CommandService at the moment
|
||||
// Following hard-coded labels are used as a workaround
|
||||
switch (commandName) {
|
||||
case 'quit':
|
||||
return _('Quit');
|
||||
case 'insertTemplate':
|
||||
return _('Insert template');
|
||||
case 'zoomActualSize':
|
||||
return _('Actual Size');
|
||||
case 'gotoAnything':
|
||||
return _('Goto Anything...');
|
||||
case 'help':
|
||||
return _('Website and documentation');
|
||||
case 'hideApp':
|
||||
return _('Hide Joplin');
|
||||
case 'closeWindow':
|
||||
return _('Close Window');
|
||||
case 'config':
|
||||
return shim.isMac() ? _('Preferences') : _('Options');
|
||||
default:
|
||||
throw new Error(`Command: ${commandName} is unknown`);
|
||||
}
|
||||
};
|
||||
exports.default = getLabel;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2V0TGFiZWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJnZXRMYWJlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlFQUFrRTtBQUVsRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLEdBQUcsT0FBTyxDQUFDLFlBQVksQ0FBQyxDQUFDO0FBQ3BDLE1BQU0sRUFBRSxJQUFJLEVBQUUsR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUM7QUFFckMsTUFBTSxjQUFjLEdBQUcsd0JBQWMsQ0FBQyxRQUFRLEVBQUUsQ0FBQztBQUVqRCxNQUFNLFFBQVEsR0FBRyxDQUFDLFdBQW1CLEVBQUUsRUFBRTtJQUN4QyxJQUFJLGNBQWMsQ0FBQyxNQUFNLENBQUMsV0FBVyxDQUFDO1FBQUUsT0FBTyxjQUFjLENBQUMsS0FBSyxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRWpGLG1FQUFtRTtJQUNuRSx1REFBdUQ7SUFFdkQsUUFBUSxXQUFXLEVBQUU7UUFDckIsS0FBSyxNQUFNO1lBQ1YsT0FBTyxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDbEIsS0FBSyxnQkFBZ0I7WUFDcEIsT0FBTyxDQUFDLENBQUMsaUJBQWlCLENBQUMsQ0FBQztRQUM3QixLQUFLLGdCQUFnQjtZQUNwQixPQUFPLENBQUMsQ0FBQyxhQUFhLENBQUMsQ0FBQztRQUN6QixLQUFLLGNBQWM7WUFDbEIsT0FBTyxDQUFDLENBQUMsa0JBQWtCLENBQUMsQ0FBQztRQUM5QixLQUFLLE1BQU07WUFDVixPQUFPLENBQUMsQ0FBQywyQkFBMkIsQ0FBQyxDQUFDO1FBQ3ZDLEtBQUssU0FBUztZQUNiLE9BQU8sQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDO1FBQ3pCLEtBQUssYUFBYTtZQUNqQixPQUFPLENBQUMsQ0FBQyxjQUFjLENBQUMsQ0FBQztRQUMxQixLQUFLLFFBQVE7WUFDWixPQUFPLElBQUksQ0FBQyxLQUFLLEVBQUUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDdkQ7WUFDQyxNQUFNLElBQUksS0FBSyxDQUFDLFlBQVksV0FBVyxhQUFhLENBQUMsQ0FBQztLQUN0RDtBQUNGLENBQUMsQ0FBQztBQUVGLGtCQUFlLFFBQVEsQ0FBQyJ9
|
36
ElectronClient/gui/KeymapConfig/utils/getLabel.ts
Normal file
36
ElectronClient/gui/KeymapConfig/utils/getLabel.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import CommandService from '../../../lib/services/CommandService';
|
||||
|
||||
const { _ } = require('lib/locale');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
const commandService = CommandService.instance();
|
||||
|
||||
const getLabel = (commandName: string) => {
|
||||
if (commandService.exists(commandName)) return commandService.label(commandName);
|
||||
|
||||
// Some commands are not registered in CommandService at the moment
|
||||
// Following hard-coded labels are used as a workaround
|
||||
|
||||
switch (commandName) {
|
||||
case 'quit':
|
||||
return _('Quit');
|
||||
case 'insertTemplate':
|
||||
return _('Insert template');
|
||||
case 'zoomActualSize':
|
||||
return _('Actual Size');
|
||||
case 'gotoAnything':
|
||||
return _('Goto Anything...');
|
||||
case 'help':
|
||||
return _('Website and documentation');
|
||||
case 'hideApp':
|
||||
return _('Hide Joplin');
|
||||
case 'closeWindow':
|
||||
return _('Close Window');
|
||||
case 'config':
|
||||
return shim.isMac() ? _('Preferences') : _('Options');
|
||||
default:
|
||||
throw new Error(`Command: ${commandName} is unknown`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getLabel;
|
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const react_1 = require('react');
|
||||
const KeymapService_1 = require('../../../lib/services/KeymapService');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
const useCommandStatus = () => {
|
||||
const [status, setStatus] = react_1.useState(() => keymapService.getCommandNames().reduce((accumulator, command) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {}));
|
||||
const disableStatus = (commandName) => setStatus(prevStatus => (Object.assign(Object.assign({}, prevStatus), { [commandName]: false })));
|
||||
const enableStatus = (commandName) => setStatus(prevStatus => {
|
||||
// Falsify all the commands; Only one command should be truthy at any given time
|
||||
const newStatus = Object.keys(prevStatus).reduce((accumulator, command) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {});
|
||||
// Make the appropriate command truthful
|
||||
newStatus[commandName] = true;
|
||||
return newStatus;
|
||||
});
|
||||
return [status, enableStatus, disableStatus];
|
||||
};
|
||||
exports.default = useCommandStatus;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNlQ29tbWFuZFN0YXR1cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInVzZUNvbW1hbmRTdGF0dXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxpQ0FBaUM7QUFDakMsdUVBQWdFO0FBRWhFLE1BQU0sYUFBYSxHQUFHLHVCQUFhLENBQUMsUUFBUSxFQUFFLENBQUM7QUFNL0MsTUFBTSxnQkFBZ0IsR0FBRyxHQUFrRixFQUFFO0lBQzVHLE1BQU0sQ0FBQyxNQUFNLEVBQUUsU0FBUyxDQUFDLEdBQUcsZ0JBQVEsQ0FBZ0IsR0FBRyxFQUFFLENBQ3hELGFBQWEsQ0FBQyxlQUFlLEVBQUUsQ0FBQyxNQUFNLENBQUMsQ0FBQyxXQUEwQixFQUFFLE9BQWUsRUFBRSxFQUFFO1FBQ3RGLFdBQVcsQ0FBQyxPQUFPLENBQUMsR0FBRyxLQUFLLENBQUM7UUFDN0IsT0FBTyxXQUFXLENBQUM7SUFDcEIsQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUNOLENBQUM7SUFFRixNQUFNLGFBQWEsR0FBRyxDQUFDLFdBQW1CLEVBQUUsRUFBRSxDQUFDLFNBQVMsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDLGlDQUFNLFVBQVUsS0FBRSxDQUFDLFdBQVcsQ0FBQyxFQUFFLEtBQUssSUFBRyxDQUFDLENBQUM7SUFDbEgsTUFBTSxZQUFZLEdBQUcsQ0FBQyxXQUFtQixFQUFFLEVBQUUsQ0FBQyxTQUFTLENBQUMsVUFBVSxDQUFDLEVBQUU7UUFDcEUsZ0ZBQWdGO1FBQ2hGLE1BQU0sU0FBUyxHQUFHLE1BQU0sQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLENBQUMsTUFBTSxDQUFDLENBQUMsV0FBMEIsRUFBRSxPQUFlLEVBQUUsRUFBRTtZQUNoRyxXQUFXLENBQUMsT0FBTyxDQUFDLEdBQUcsS0FBSyxDQUFDO1lBQzdCLE9BQU8sV0FBVyxDQUFDO1FBQ3BCLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVQLHdDQUF3QztRQUN4QyxTQUFTLENBQUMsV0FBVyxDQUFDLEdBQUcsSUFBSSxDQUFDO1FBQzlCLE9BQU8sU0FBUyxDQUFDO0lBQ2xCLENBQUMsQ0FBQyxDQUFDO0lBRUgsT0FBTyxDQUFDLE1BQU0sRUFBRSxZQUFZLEVBQUUsYUFBYSxDQUFDLENBQUM7QUFDOUMsQ0FBQyxDQUFDO0FBRUYsa0JBQWUsZ0JBQWdCLENBQUMifQ==
|
34
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.ts
Normal file
34
ElectronClient/gui/KeymapConfig/utils/useCommandStatus.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useState } from 'react';
|
||||
import KeymapService from '../../../lib/services/KeymapService';
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
interface CommandStatus {
|
||||
[commandName: string]: boolean
|
||||
}
|
||||
|
||||
const useCommandStatus = (): [CommandStatus, (commandName: string) => void, (commandName: string) => void] => {
|
||||
const [status, setStatus] = useState<CommandStatus>(() =>
|
||||
keymapService.getCommandNames().reduce((accumulator: CommandStatus, command: string) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const disableStatus = (commandName: string) => setStatus(prevStatus => ({ ...prevStatus, [commandName]: false }));
|
||||
const enableStatus = (commandName: string) => setStatus(prevStatus => {
|
||||
// Falsify all the commands; Only one command should be truthy at any given time
|
||||
const newStatus = Object.keys(prevStatus).reduce((accumulator: CommandStatus, command: string) => {
|
||||
accumulator[command] = false;
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
// Make the appropriate command truthful
|
||||
newStatus[commandName] = true;
|
||||
return newStatus;
|
||||
});
|
||||
|
||||
return [status, enableStatus, disableStatus];
|
||||
};
|
||||
|
||||
export default useCommandStatus;
|
@@ -1,68 +0,0 @@
|
||||
'use strict';
|
||||
const __awaiter = (this && this.__awaiter) || function(thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function(resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function(resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
const react_1 = require('react');
|
||||
const KeymapService_1 = require('../../../lib/services/KeymapService');
|
||||
const keymapService = KeymapService_1.default.instance();
|
||||
// This custom hook provides a synchronized snapshot of the keymap residing at KeymapService
|
||||
// All the logic regarding altering and interacting with the keymap is isolated from the components
|
||||
const useKeymap = () => {
|
||||
const [keymapItems, setKeymapItems] = react_1.useState(() => keymapService.getKeymapItems());
|
||||
const [keymapError, setKeymapError] = react_1.useState(null);
|
||||
const setAccelerator = (commandName, accelerator) => {
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
newKeymap.find(item => item.command === commandName).accelerator = accelerator || null /* Disabled */;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
const resetAccelerator = (commandName) => {
|
||||
const defaultAccelerator = keymapService.getDefaultAccelerator(commandName);
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
newKeymap.find(item => item.command === commandName).accelerator = defaultAccelerator;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
const overrideKeymapItems = (customKeymapItems) => {
|
||||
const oldKeymapItems = [...customKeymapItems];
|
||||
keymapService.initialize(); // Start with a fresh keymap
|
||||
try {
|
||||
// First, try to update the in-memory keymap of KeymapService
|
||||
// This function will throw if there are any issues with the new custom keymap
|
||||
keymapService.overrideKeymap(customKeymapItems);
|
||||
// Then, update the state with the data from KeymapService
|
||||
// Side-effect: Changes will also be saved to the disk
|
||||
setKeymapItems(keymapService.getKeymapItems());
|
||||
} catch (err) {
|
||||
// oldKeymapItems includes even the unchanged keymap items
|
||||
// However, it is not an issue because the logic accounts for such scenarios
|
||||
keymapService.overrideKeymap(oldKeymapItems);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
const exportCustomKeymap = (customKeymapPath) => __awaiter(void 0, void 0, void 0, function* () {
|
||||
// KeymapService is already synchronized automatically with the in-state keymap
|
||||
yield keymapService.saveCustomKeymap(customKeymapPath);
|
||||
});
|
||||
react_1.useEffect(() => {
|
||||
try {
|
||||
keymapService.overrideKeymap(keymapItems);
|
||||
keymapService.saveCustomKeymap();
|
||||
setKeymapError(null);
|
||||
} catch (err) {
|
||||
setKeymapError(err);
|
||||
}
|
||||
}, [keymapItems]);
|
||||
return [keymapItems, keymapError, overrideKeymapItems, exportCustomKeymap, setAccelerator, resetAccelerator];
|
||||
};
|
||||
exports.default = useKeymap;
|
||||
// # sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXNlS2V5bWFwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsidXNlS2V5bWFwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7O0FBQUEsaUNBQTRDO0FBQzVDLHVFQUFnRjtBQUVoRixNQUFNLGFBQWEsR0FBRyx1QkFBYSxDQUFDLFFBQVEsRUFBRSxDQUFDO0FBRS9DLDRGQUE0RjtBQUM1RixtR0FBbUc7QUFFbkcsTUFBTSxTQUFTLEdBQUcsR0FPaEIsRUFBRTtJQUNILE1BQU0sQ0FBQyxXQUFXLEVBQUUsY0FBYyxDQUFDLEdBQUcsZ0JBQVEsQ0FBZSxHQUFHLEVBQUUsQ0FBQyxhQUFhLENBQUMsY0FBYyxFQUFFLENBQUMsQ0FBQztJQUNuRyxNQUFNLENBQUMsV0FBVyxFQUFFLGNBQWMsQ0FBQyxHQUFHLGdCQUFRLENBQVEsSUFBSSxDQUFDLENBQUM7SUFFNUQsTUFBTSxjQUFjLEdBQUcsQ0FBQyxXQUFtQixFQUFFLFdBQW1CLEVBQUUsRUFBRTtRQUNuRSxjQUFjLENBQUMsVUFBVSxDQUFDLEVBQUU7WUFDM0IsTUFBTSxTQUFTLEdBQUcsQ0FBQyxHQUFHLFVBQVUsQ0FBQyxDQUFDO1lBRWxDLFNBQVMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsT0FBTyxLQUFLLFdBQVcsQ0FBQyxDQUFDLFdBQVcsR0FBRyxXQUFXLElBQUksSUFBSSxDQUFDLGNBQWMsQ0FBQztZQUN0RyxPQUFPLFNBQVMsQ0FBQztRQUNsQixDQUFDLENBQUMsQ0FBQztJQUNKLENBQUMsQ0FBQztJQUVGLE1BQU0sZ0JBQWdCLEdBQUcsQ0FBQyxXQUFtQixFQUFFLEVBQUU7UUFDaEQsTUFBTSxrQkFBa0IsR0FBRyxhQUFhLENBQUMscUJBQXFCLENBQUMsV0FBVyxDQUFDLENBQUM7UUFDNUUsY0FBYyxDQUFDLFVBQVUsQ0FBQyxFQUFFO1lBQzNCLE1BQU0sU0FBUyxHQUFHLENBQUMsR0FBRyxVQUFVLENBQUMsQ0FBQztZQUVsQyxTQUFTLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLE9BQU8sS0FBSyxXQUFXLENBQUMsQ0FBQyxXQUFXLEdBQUcsa0JBQWtCLENBQUM7WUFDdEYsT0FBTyxTQUFTLENBQUM7UUFDbEIsQ0FBQyxDQUFDLENBQUM7SUFDSixDQUFDLENBQUM7SUFFRixNQUFNLG1CQUFtQixHQUFHLENBQUMsaUJBQStCLEVBQUUsRUFBRTtRQUMvRCxNQUFNLGNBQWMsR0FBRyxDQUFDLEdBQUcsaUJBQWlCLENBQUMsQ0FBQztRQUM5QyxhQUFhLENBQUMsVUFBVSxFQUFFLENBQUMsQ0FBQyw0QkFBNEI7UUFFeEQsSUFBSTtZQUNILDZEQUE2RDtZQUM3RCw4RUFBOEU7WUFDOUUsYUFBYSxDQUFDLGNBQWMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO1lBQ2hELDBEQUEwRDtZQUMxRCxzREFBc0Q7WUFDdEQsY0FBYyxDQUFDLGFBQWEsQ0FBQyxjQUFjLEVBQUUsQ0FBQyxDQUFDO1NBQy9DO1FBQUMsT0FBTyxHQUFHLEVBQUU7WUFDYiwwREFBMEQ7WUFDMUQsNEVBQTRFO1lBQzVFLGFBQWEsQ0FBQyxjQUFjLENBQUMsY0FBYyxDQUFDLENBQUM7WUFDN0MsTUFBTSxHQUFHLENBQUM7U0FDVjtJQUNGLENBQUMsQ0FBQztJQUVGLE1BQU0sa0JBQWtCLEdBQUcsQ0FBTyxnQkFBd0IsRUFBRSxFQUFFO1FBQzdELCtFQUErRTtRQUMvRSxNQUFNLGFBQWEsQ0FBQyxnQkFBZ0IsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBQ3hELENBQUMsQ0FBQSxDQUFDO0lBRUYsaUJBQVMsQ0FBQyxHQUFHLEVBQUU7UUFDZCxJQUFJO1lBQ0gsYUFBYSxDQUFDLGNBQWMsQ0FBQyxXQUFXLENBQUMsQ0FBQztZQUMxQyxhQUFhLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztZQUNqQyxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7U0FDckI7UUFBQyxPQUFPLEdBQUcsRUFBRTtZQUNiLGNBQWMsQ0FBQyxHQUFHLENBQUMsQ0FBQztTQUNwQjtJQUNGLENBQUMsRUFBRSxDQUFDLFdBQVcsQ0FBQyxDQUFDLENBQUM7SUFFbEIsT0FBTyxDQUFDLFdBQVcsRUFBRSxXQUFXLEVBQUUsbUJBQW1CLEVBQUUsa0JBQWtCLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixDQUFDLENBQUM7QUFDOUcsQ0FBQyxDQUFDO0FBRUYsa0JBQWUsU0FBUyxDQUFDIn0=
|
70
ElectronClient/gui/KeymapConfig/utils/useKeymap.ts
Normal file
70
ElectronClient/gui/KeymapConfig/utils/useKeymap.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import KeymapService, { KeymapItem } from '../../../lib/services/KeymapService';
|
||||
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
// This custom hook provides a synchronized snapshot of the keymap residing at KeymapService
|
||||
// All the logic regarding altering and interacting with the keymap is isolated from the components
|
||||
|
||||
const useKeymap = (): [
|
||||
KeymapItem[],
|
||||
Error,
|
||||
(keymapItems: KeymapItem[]) => void,
|
||||
(commandName: string, accelerator: string) => void,
|
||||
(commandName: string) => void
|
||||
] => {
|
||||
const [keymapItems, setKeymapItems] = useState<KeymapItem[]>(() => keymapService.getKeymapItems());
|
||||
const [keymapError, setKeymapError] = useState<Error>(null);
|
||||
|
||||
const setAccelerator = (commandName: string, accelerator: string) => {
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
|
||||
newKeymap.find(item => item.command === commandName).accelerator = accelerator || null /* Disabled */;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
|
||||
const resetAccelerator = (commandName: string) => {
|
||||
const defaultAccelerator = keymapService.getDefaultAccelerator(commandName);
|
||||
setKeymapItems(prevKeymap => {
|
||||
const newKeymap = [...prevKeymap];
|
||||
|
||||
newKeymap.find(item => item.command === commandName).accelerator = defaultAccelerator;
|
||||
return newKeymap;
|
||||
});
|
||||
};
|
||||
|
||||
const overrideKeymapItems = (customKeymapItems: KeymapItem[]) => {
|
||||
const oldKeymapItems = [...customKeymapItems];
|
||||
keymapService.initialize(); // Start with a fresh keymap
|
||||
|
||||
try {
|
||||
// First, try to update the in-memory keymap of KeymapService
|
||||
// This function will throw if there are any issues with the new custom keymap
|
||||
keymapService.overrideKeymap(customKeymapItems);
|
||||
// Then, update the state with the data from KeymapService
|
||||
// Side-effect: Changes will also be saved to the disk
|
||||
setKeymapItems(keymapService.getKeymapItems());
|
||||
} catch (err) {
|
||||
// oldKeymapItems includes even the unchanged keymap items
|
||||
// However, it is not an issue because the logic accounts for such scenarios
|
||||
keymapService.overrideKeymap(oldKeymapItems);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
keymapService.overrideKeymap(keymapItems);
|
||||
keymapService.saveCustomKeymap();
|
||||
setKeymapError(null);
|
||||
} catch (err) {
|
||||
setKeymapError(err);
|
||||
}
|
||||
}, [keymapItems]);
|
||||
|
||||
return [keymapItems, keymapError, overrideKeymapItems, setAccelerator, resetAccelerator];
|
||||
};
|
||||
|
||||
export default useKeymap;
|
@@ -434,7 +434,7 @@ class MainScreenComponent extends React.Component {
|
||||
// A bit of a hack, but for now don't allow changing code view
|
||||
// while a note is being saved as it will cause a problem with
|
||||
// TinyMCE because it won't have time to send its content before
|
||||
// being switch to Ace Editor.
|
||||
// being switch to the Code Editor.
|
||||
if (this.props.hasNotesBeingSaved) return;
|
||||
Setting.toggle('editor.codeView');
|
||||
},
|
||||
@@ -445,8 +445,8 @@ class MainScreenComponent extends React.Component {
|
||||
headerItems.push({
|
||||
title: _('Search...'),
|
||||
iconName: 'fa-search',
|
||||
onQuery: query => {
|
||||
CommandService.instance().execute('search', { query });
|
||||
onQuery: (query, fuzzy = false) => {
|
||||
CommandService.instance().execute('search', { query, fuzzy });
|
||||
},
|
||||
type: 'search',
|
||||
});
|
||||
@@ -468,8 +468,7 @@ class MainScreenComponent extends React.Component {
|
||||
const noteContentPropertiesDialogOptions = this.state.noteContentPropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
|
||||
const codeEditor = Setting.value('editor.betaCodeMirror') ? 'CodeMirror' : 'AceEditor';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? codeEditor : 'TinyMCE';
|
||||
const bodyEditor = this.props.settingEditorCodeView ? 'CodeMirror' : 'TinyMCE';
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
|
@@ -10,7 +10,7 @@ export const declaration:CommandDeclaration = {
|
||||
|
||||
export const runtime = (comp:any):CommandRuntime => {
|
||||
return {
|
||||
execute: async ({ query }:any) => {
|
||||
execute: async ({ query, fuzzy }:any) => {
|
||||
console.info('RUNTIME', query);
|
||||
|
||||
if (!comp.searchId_) comp.searchId_ = uuid.create();
|
||||
@@ -23,6 +23,7 @@ export const runtime = (comp:any):CommandRuntime => {
|
||||
query_pattern: query,
|
||||
query_folder_id: null,
|
||||
type_: BaseModel.TYPE_SEARCH,
|
||||
fuzzy: fuzzy,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -1,654 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect, useRef, forwardRef, useCallback, useImperativeHandle, useMemo } from 'react';
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
|
||||
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
|
||||
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
|
||||
import { textOffsetToCursorPosition, useScrollHandler, useRootWidth, usePrevious, lineLeftSpaces, selectionRange, selectionRangeCurrentLine, selectionRangePreviousLine, currentTextOffset, textOffsetSelection, selectedText } from './utils';
|
||||
import useListIdent from './utils/useListIdent';
|
||||
import Toolbar from './Toolbar';
|
||||
import styles_ from './styles';
|
||||
import { RenderedBody, defaultRenderedBody } from './utils/types';
|
||||
|
||||
const AceEditorReact = require('react-ace').default;
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { clipboard } = require('electron');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const NoteTextViewer = require('../../../NoteTextViewer.min');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const { _ } = require('lib/locale');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const dialogs = require('../../../dialogs');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
require('brace/theme/chrome');
|
||||
require('brace/theme/solarized_light');
|
||||
require('brace/theme/solarized_dark');
|
||||
require('brace/theme/twilight');
|
||||
require('brace/theme/dracula');
|
||||
require('brace/theme/chaos');
|
||||
require('brace/theme/tomorrow');
|
||||
require('brace/keybinding/vim');
|
||||
require('brace/keybinding/emacs');
|
||||
require('brace/theme/terminal');
|
||||
|
||||
// TODO: Could not get below code to work
|
||||
|
||||
// @ts-ignore Ace global variable
|
||||
// const aceGlobal = (ace as any);
|
||||
|
||||
// class CustomHighlightRules extends aceGlobal.acequire(
|
||||
// 'ace/mode/markdown_highlight_rules'
|
||||
// ).MarkdownHighlightRules {
|
||||
// constructor() {
|
||||
// super();
|
||||
// if (Setting.value('markdown.plugin.mark')) {
|
||||
// this.$rules.start.push({
|
||||
// // This is actually a highlight `mark`, but Ace has no token name for
|
||||
// // this so we made up our own. Reference for common tokens here:
|
||||
// // https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#common-tokens
|
||||
// token: 'highlight_mark',
|
||||
// regex: '==[^ ](?:.*?[^ ])?==',
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// /* eslint-disable-next-line no-undef */
|
||||
// class CustomMdMode extends aceGlobal.acequire('ace/mode/markdown').Mode {
|
||||
// constructor() {
|
||||
// super();
|
||||
// this.HighlightRules = CustomHighlightRules;
|
||||
// }
|
||||
// }
|
||||
|
||||
function markupRenderOptions(override: any = null) {
|
||||
return { ...override };
|
||||
}
|
||||
|
||||
function AceEditor(props: NoteBodyEditorProps, ref: any) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const [renderedBody, setRenderedBody] = useState<RenderedBody>(defaultRenderedBody()); // Viewer content
|
||||
const [editor, setEditor] = useState(null);
|
||||
const [webviewReady, setWebviewReady] = useState(false);
|
||||
|
||||
const previousRenderedBody = usePrevious(renderedBody);
|
||||
const previousSearchMarkers = usePrevious(props.searchMarkers);
|
||||
const previousContentKey = usePrevious(props.contentKey);
|
||||
|
||||
const editorRef = useRef(null);
|
||||
editorRef.current = editor;
|
||||
const rootRef = useRef(null);
|
||||
const webviewRef = useRef(null);
|
||||
const props_onChangeRef = useRef<Function>(null);
|
||||
props_onChangeRef.current = props.onChange;
|
||||
const contentKeyHasChangedRef = useRef(false);
|
||||
contentKeyHasChangedRef.current = previousContentKey !== props.contentKey;
|
||||
|
||||
const rootWidth = useRootWidth({ rootRef });
|
||||
|
||||
const { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll } = useScrollHandler(editor, webviewRef, props.onScroll);
|
||||
|
||||
useListIdent({ editor });
|
||||
|
||||
const aceEditor_change = useCallback((newBody: string) => {
|
||||
// Throw an error early to know what part of the code set the body to the
|
||||
// wrong value. Otherwise it will trigger an error somewhere deep in React-Ace
|
||||
// which will be hard to debug.
|
||||
if (typeof newBody !== 'string') throw new Error('Body is not a string');
|
||||
props_onChangeRef.current({ changeId: null, content: newBody });
|
||||
}, []);
|
||||
|
||||
const wrapSelectionWithStrings = useCallback((string1: string, string2 = '', defaultText = '', replacementText: string = null, byLine = false) => {
|
||||
if (!editor) return;
|
||||
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
|
||||
let newBody = props.content;
|
||||
|
||||
if (selection && selection.start !== selection.end) {
|
||||
const selectedLines = replacementText !== null ? replacementText : props.content.substr(selection.start, selection.end - selection.start);
|
||||
const selectedStrings = byLine ? selectedLines.split(/\r?\n/) : [selectedLines];
|
||||
|
||||
newBody = props.content.substr(0, selection.start);
|
||||
|
||||
let startCursorPos, endCursorPos;
|
||||
|
||||
for (let i = 0; i < selectedStrings.length; i++) {
|
||||
if (byLine == false) {
|
||||
const start = selectedStrings[i].search(/[^\s]/);
|
||||
const end = selectedStrings[i].search(/[^\s](?=[\s]*$)/);
|
||||
newBody += selectedStrings[i].substr(0, start) + string1 + selectedStrings[i].substr(start, end - start + 1) + string2 + selectedStrings[i].substr(end + 1);
|
||||
// Getting position for correcting offset in highlighted text when surrounded by white spaces
|
||||
startCursorPos = textOffsetToCursorPosition(selection.start + start, newBody);
|
||||
endCursorPos = textOffsetToCursorPosition(selection.start + end + 1, newBody);
|
||||
|
||||
} else { newBody += string1 + selectedStrings[i] + string2; }
|
||||
|
||||
}
|
||||
|
||||
newBody += props.content.substr(selection.end);
|
||||
|
||||
const r = selectionRange(editor);
|
||||
|
||||
// Because some insertion strings will have newlines, we'll need to account for them
|
||||
const str1Split = string1.split(/\r?\n/);
|
||||
|
||||
// Add the number of newlines to the row
|
||||
// and add the length of the final line to the column (for strings with no newlines this is the string length)
|
||||
|
||||
let newRange: any = {};
|
||||
if (!byLine) {
|
||||
// Correcting offset in Highlighted text when surrounded by white spaces
|
||||
newRange = {
|
||||
start: {
|
||||
row: startCursorPos.row,
|
||||
column: startCursorPos.column + string1.length,
|
||||
},
|
||||
end: {
|
||||
row: endCursorPos.row,
|
||||
column: endCursorPos.column + string1.length,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
newRange = {
|
||||
start: {
|
||||
row: r.start.row + str1Split.length - 1,
|
||||
column: r.start.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
end: {
|
||||
row: r.end.row + str1Split.length - 1,
|
||||
column: r.end.column + str1Split[str1Split.length - 1].length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (replacementText !== null) {
|
||||
const diff = replacementText.length - (selection.end - selection.start);
|
||||
newRange.end.column += diff;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
} else {
|
||||
const middleText = replacementText !== null ? replacementText : defaultText;
|
||||
const textOffset = currentTextOffset(editor, props.content);
|
||||
const s1 = props.content.substr(0, textOffset);
|
||||
const s2 = props.content.substr(textOffset);
|
||||
newBody = s1 + string1 + middleText + string2 + s2;
|
||||
|
||||
const p = textOffsetToCursorPosition(textOffset + string1.length, newBody);
|
||||
const newRange = {
|
||||
start: { row: p.row, column: p.column },
|
||||
end: { row: p.row, column: p.column + middleText.length },
|
||||
};
|
||||
|
||||
// BUG!! If replacementText contains newline characters, the logic
|
||||
// to select the new text will not work.
|
||||
|
||||
setTimeout(() => {
|
||||
if (middleText && newRange) {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(newRange.start.row, newRange.start.column);
|
||||
range.setEnd(newRange.end.row, newRange.end.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
} else {
|
||||
for (let i = 0; i < string1.length; i++) {
|
||||
editor.getSession().getSelection().moveCursorRight();
|
||||
}
|
||||
}
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
aceEditor_change(newBody);
|
||||
}, [editor, props.content, aceEditor_change]);
|
||||
|
||||
const addListItem = useCallback((string1, string2 = '', defaultText = '', byLine = false) => {
|
||||
let newLine = '\n';
|
||||
const range = selectionRange(editor);
|
||||
if (!range || (range.start.row === range.end.row && !selectionRangeCurrentLine(range, props.content))) {
|
||||
newLine = '';
|
||||
}
|
||||
wrapSelectionWithStrings(newLine + string1, string2, defaultText, null, byLine);
|
||||
}, [wrapSelectionWithStrings, props.content, editor]);
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
content: () => props.content,
|
||||
resetScroll: () => {
|
||||
resetScroll();
|
||||
},
|
||||
scrollTo: (options:ScrollOptions) => {
|
||||
if (options.type === ScrollOptionTypes.Hash) {
|
||||
if (!webviewRef.current) return;
|
||||
webviewRef.current.wrappedInstance.send('scrollToHash', options.value as string);
|
||||
} else if (options.type === ScrollOptionTypes.Percent) {
|
||||
const p = options.value as number;
|
||||
setEditorPercentScroll(p);
|
||||
setViewerPercentScroll(p);
|
||||
} else {
|
||||
throw new Error(`Unsupported scroll options: ${options.type}`);
|
||||
}
|
||||
},
|
||||
supportsCommand: (/* name:string*/) => {
|
||||
// TODO: not implemented, currently only used for "search" command
|
||||
// which is not directly supported by Ace Editor.
|
||||
return false;
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
if (!editor) return false;
|
||||
|
||||
reg.logger().debug('AceEditor: execCommand', cmd);
|
||||
|
||||
let commandProcessed = true;
|
||||
|
||||
if (cmd.name === 'dropItems') {
|
||||
if (cmd.value.type === 'notes') {
|
||||
wrapSelectionWithStrings('', '', '', cmd.value.markdownTags.join('\n'));
|
||||
} else if (cmd.value.type === 'files') {
|
||||
const newBody = await commandAttachFileToBody(props.content, cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else if (cmd.name === 'focus') {
|
||||
editor.focus();
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
}
|
||||
|
||||
if (!commandProcessed) {
|
||||
const commands: any = {
|
||||
textBold: () => wrapSelectionWithStrings('**', '**', _('strong text')),
|
||||
textItalic: () => wrapSelectionWithStrings('*', '*', _('emphasised text')),
|
||||
textLink: async () => {
|
||||
const url = await dialogs.prompt(_('Insert Hyperlink'));
|
||||
if (url) wrapSelectionWithStrings('[', `](${url})`);
|
||||
},
|
||||
textCode: () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const string = props.content.substr(selection.start, selection.end - selection.start);
|
||||
|
||||
// Look for newlines
|
||||
const match = string.match(/\r?\n/);
|
||||
|
||||
if (match && match.length > 0) {
|
||||
if (string.startsWith('```') && string.endsWith('```')) {
|
||||
wrapSelectionWithStrings('', '', '', string.substr(4, selection.end - selection.start - 8));
|
||||
} else {
|
||||
wrapSelectionWithStrings(`\`\`\`${match[0]}`, `${match[0]}\`\`\``);
|
||||
}
|
||||
} else {
|
||||
wrapSelectionWithStrings('`', '`', '');
|
||||
}
|
||||
},
|
||||
insertText: (value: any) => wrapSelectionWithStrings(value),
|
||||
attachFile: async () => {
|
||||
const selection = textOffsetSelection(selectionRange(editor), props.content);
|
||||
const newBody = await commandAttachFileToBody(props.content, null, { position: selection ? selection.start : 0 });
|
||||
if (newBody) aceEditor_change(newBody);
|
||||
},
|
||||
textNumberedList: () => {
|
||||
const selection = selectionRange(editor);
|
||||
let bulletNumber = markdownUtils.olLineNumber(selectionRangeCurrentLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(selectionRangePreviousLine(selection, props.content));
|
||||
if (!bulletNumber) bulletNumber = 0;
|
||||
addListItem(`${bulletNumber + 1}. `, '', _('List item'), true);
|
||||
},
|
||||
textBulletedList: () => addListItem('- ', '', _('List item'), true),
|
||||
textCheckbox: () => addListItem('- [ ] ', '', _('List item'), true),
|
||||
textHeading: () => addListItem('## ','','', true),
|
||||
textHorizontalRule: () => addListItem('* * *'),
|
||||
};
|
||||
|
||||
if (commands[cmd.name]) {
|
||||
commands[cmd.name](cmd.value);
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported Joplin command: ', cmd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
}, [editor, props.content, addListItem, wrapSelectionWithStrings, selectionRangeCurrentLine, aceEditor_change, setEditorPercentScroll, setViewerPercentScroll, resetScroll, renderedBody]);
|
||||
|
||||
const onEditorPaste = useCallback(async (event: any = null) => {
|
||||
const resourceMds = await handlePasteEvent(event);
|
||||
if (!resourceMds.length) return;
|
||||
wrapSelectionWithStrings('', '', resourceMds.join('\n'));
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const editorCutText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
if (!text) return;
|
||||
|
||||
clipboard.writeText(text);
|
||||
|
||||
const s = textOffsetSelection(selectionRange(editor), props.content);
|
||||
if (!s || s.start === s.end) return;
|
||||
|
||||
const s1 = props.content.substr(0, s.start);
|
||||
const s2 = props.content.substr(s.end);
|
||||
|
||||
aceEditor_change(s1 + s2);
|
||||
|
||||
setTimeout(() => {
|
||||
const range = selectionRange(editor);
|
||||
range.setStart(range.start.row, range.start.column);
|
||||
range.setEnd(range.start.row, range.start.column);
|
||||
editor.getSession().getSelection().setSelectionRange(range, false);
|
||||
editor.focus();
|
||||
}, 10);
|
||||
}, [props.content, editor, aceEditor_change]);
|
||||
|
||||
function clipboardText() {
|
||||
return clipboard.readText() ? clipboard.readText() : clipboard.readHTML();
|
||||
}
|
||||
|
||||
const editorCopyText = useCallback(() => {
|
||||
const text = selectedText(selectionRange(editor), props.content);
|
||||
clipboard.writeText(text);
|
||||
}, [props.content, editor]);
|
||||
|
||||
const editorPasteText = useCallback(() => {
|
||||
wrapSelectionWithStrings(clipboardText(), '', '', '');
|
||||
}, [wrapSelectionWithStrings]);
|
||||
|
||||
const onEditorContextMenu = useCallback(() => {
|
||||
const menu = new Menu();
|
||||
|
||||
const hasSelectedText = !!selectedText(selectionRange(editor), props.content);
|
||||
const currentClipboardText = clipboardText();
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Cut'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCutText();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Copy'),
|
||||
enabled: hasSelectedText,
|
||||
click: async () => {
|
||||
editorCopyText();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Paste'),
|
||||
enabled: true,
|
||||
click: async () => {
|
||||
if (currentClipboardText) {
|
||||
editorPasteText();
|
||||
} else {
|
||||
// To handle pasting images
|
||||
onEditorPaste();
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}, [props.content, editorCutText, editorPasteText, editorCopyText, onEditorPaste, editor]);
|
||||
|
||||
function aceEditor_load(editor: any) {
|
||||
setEditor(editor);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
const cancelledKeys = [];
|
||||
const letters = ['F', 'T', 'P', 'Q', 'L', ',', 'G', 'K'];
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
const l = letters[i];
|
||||
cancelledKeys.push(`Ctrl+${l}`);
|
||||
cancelledKeys.push(`Command+${l}`);
|
||||
}
|
||||
cancelledKeys.push('Alt+E');
|
||||
cancelledKeys.push('Command+Shift+L');
|
||||
cancelledKeys.push('Ctrl+Shift+L');
|
||||
|
||||
for (let i = 0; i < cancelledKeys.length; i++) {
|
||||
const k = cancelledKeys[i];
|
||||
editor.commands.bindKey(k, () => {
|
||||
// HACK: Ace doesn't seem to provide a way to override its shortcuts, but throwing
|
||||
// an exception from this undocumented function seems to cancel it without any
|
||||
// side effect.
|
||||
// https://stackoverflow.com/questions/36075846
|
||||
throw new Error(`HACK: Overriding Ace Editor shortcut: ${k}`);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelector('#note-editor').addEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').addEventListener('contextmenu', onEditorContextMenu);
|
||||
|
||||
// Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash.
|
||||
// https://github.com/ajaxorg/ace/issues/2754
|
||||
// @ts-ignore: Keep the function signature as-is despite unusued arguments
|
||||
editor.getSession().getMode().getNextLineIndent = function(state: any, line: string) {
|
||||
const leftSpaces = lineLeftSpaces(line);
|
||||
const lineNoLeftSpaces = line.trimLeft();
|
||||
|
||||
if (lineNoLeftSpaces.indexOf('- [ ] ') === 0 || lineNoLeftSpaces.indexOf('- [x] ') === 0 || lineNoLeftSpaces.indexOf('- [X] ') === 0) return `${leftSpaces}- [ ] `;
|
||||
if (lineNoLeftSpaces.indexOf('- ') === 0) return `${leftSpaces}- `;
|
||||
if (lineNoLeftSpaces.indexOf('* ') === 0 && line.trim() !== '* * *') return `${leftSpaces}* `;
|
||||
|
||||
const bulletNumber = markdownUtils.olLineNumber(lineNoLeftSpaces);
|
||||
if (bulletNumber) return `${leftSpaces + (bulletNumber + 1)}. `;
|
||||
|
||||
return this.$getIndent(line);
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.querySelector('#note-editor').removeEventListener('paste', onEditorPaste, true);
|
||||
document.querySelector('#note-editor').removeEventListener('contextmenu', onEditorContextMenu);
|
||||
};
|
||||
}, [editor, onEditorPaste, onEditorContextMenu]);
|
||||
|
||||
const webview_domReady = useCallback(() => {
|
||||
setWebviewReady(true);
|
||||
}, []);
|
||||
|
||||
const webview_ipcMessage = useCallback((event: any) => {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, props.content);
|
||||
aceEditor_change(newBody);
|
||||
} else if (msg === 'percentScroll') {
|
||||
setEditorPercentScroll(arg0);
|
||||
} else {
|
||||
props.onMessage(event);
|
||||
}
|
||||
}, [props.onMessage, props.content, aceEditor_change, setEditorPercentScroll]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const interval = contentKeyHasChangedRef.current ? 0 : 500;
|
||||
|
||||
const timeoutId = setTimeout(async () => {
|
||||
let bodyToRender = props.content;
|
||||
|
||||
if (!bodyToRender.trim() && props.visiblePanes.indexOf('viewer') >= 0 && props.visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = `<i>${_('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout'))}</i>`;
|
||||
}
|
||||
|
||||
const result = await props.markupToHtml(props.contentMarkupLanguage, bodyToRender, markupRenderOptions({ resourceInfos: props.resourceInfos }));
|
||||
if (cancelled) return;
|
||||
setRenderedBody(result);
|
||||
}, interval);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [props.content, props.contentMarkupLanguage, props.visiblePanes, props.resourceInfos, props.markupToHtml]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
if (contentKeyHasChangedRef.current) {
|
||||
// editor.getSession().setMode(new CustomMdMode());
|
||||
const undoManager = editor.getSession().getUndoManager();
|
||||
undoManager.reset();
|
||||
editor.getSession().setUndoManager(undoManager);
|
||||
}
|
||||
}, [props.content, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!webviewReady) return;
|
||||
|
||||
const options: any = {
|
||||
pluginAssets: renderedBody.pluginAssets,
|
||||
downloadResources: Setting.value('sync.resourceDownloadMode'),
|
||||
};
|
||||
webviewRef.current.wrappedInstance.send('setHtml', renderedBody.html, options);
|
||||
}, [renderedBody, webviewReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
}
|
||||
}, [props.searchMarkers, renderedBody]);
|
||||
|
||||
const cellEditorStyle = useMemo(() => {
|
||||
const output = { ...styles.cellEditor };
|
||||
if (!props.visiblePanes.includes('editor')) {
|
||||
// Note: Ideally we'd set the display to "none" to take the editor out
|
||||
// of the DOM but if we do that, certain things won't work, in particular
|
||||
// things related to scroll, which are based on the editor.
|
||||
|
||||
// Note that the below hack doesn't work and causes a bug in this case:
|
||||
// - Put Ace Editor in viewer-only mode
|
||||
// - Go to WYSIWYG editor
|
||||
// - Create new to-do - set title only
|
||||
// - Switch to Code View
|
||||
// - Switch layout and type something
|
||||
// => Text editor layout is broken and text is off-screen
|
||||
|
||||
output.display = 'none'; // Seems to work fine since the refactoring
|
||||
}
|
||||
|
||||
return output;
|
||||
}, [styles.cellEditor, props.visiblePanes]);
|
||||
|
||||
const cellViewerStyle = useMemo(() => {
|
||||
const output = { ...styles.cellViewer };
|
||||
if (!props.visiblePanes.includes('viewer')) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
output.width = 1;
|
||||
output.maxWidth = 1;
|
||||
} else if (!props.visiblePanes.includes('editor')) {
|
||||
output.borderLeftStyle = 'none';
|
||||
}
|
||||
return output;
|
||||
}, [styles.cellViewer, props.visiblePanes]);
|
||||
|
||||
const editorReadOnly = props.visiblePanes.indexOf('editor') < 0;
|
||||
|
||||
function renderEditor() {
|
||||
// Need to hard-code the editor width, otherwise various bugs pops up
|
||||
let width = 0;
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
width = !props.visiblePanes.includes('viewer') ? rootWidth : Math.floor(rootWidth / 2);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={cellEditorStyle}>
|
||||
<AceEditorReact
|
||||
value={props.content}
|
||||
mode={props.contentMarkupLanguage === Note.MARKUP_LANGUAGE_HTML ? 'text' : 'markdown'}
|
||||
theme={styles.editor.aceEditorTheme}
|
||||
style={styles.editor}
|
||||
width={`${width}px`}
|
||||
fontSize={styles.editor.fontSize}
|
||||
showGutter={false}
|
||||
readOnly={editorReadOnly}
|
||||
name="note-editor"
|
||||
wrapEnabled={true}
|
||||
onScroll={editor_scroll}
|
||||
onChange={aceEditor_change}
|
||||
showPrintMargin={false}
|
||||
onLoad={aceEditor_load}
|
||||
// Enable/Disable the autoclosing braces
|
||||
setOptions={
|
||||
{
|
||||
behavioursEnabled: Setting.value('editor.autoMatchingBraces'),
|
||||
useSoftTabs: false,
|
||||
}
|
||||
}
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
editorProps={{ $blockScrolling: Infinity }}
|
||||
// This is buggy (gets outside the container)
|
||||
highlightActiveLine={false}
|
||||
keyboardHandler={props.keyboardMode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderViewer() {
|
||||
return (
|
||||
<div style={cellViewerStyle}>
|
||||
<NoteTextViewer
|
||||
ref={webviewRef}
|
||||
viewerStyle={styles.viewer}
|
||||
onIpcMessage={webview_ipcMessage}
|
||||
onDomReady={webview_domReady}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.root} ref={rootRef}>
|
||||
<div style={styles.rowToolbar}>
|
||||
<Toolbar
|
||||
theme={props.theme}
|
||||
dispatch={props.dispatch}
|
||||
disabled={editorReadOnly}
|
||||
/>
|
||||
{props.noteToolbar}
|
||||
</div>
|
||||
<div style={styles.rowEditorViewer}>
|
||||
{renderEditor()}
|
||||
{renderViewer()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(AceEditor);
|
||||
|
@@ -1,48 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import CommandService from '../../../../lib/services/CommandService';
|
||||
|
||||
const ToolbarBase = require('../../../Toolbar.min.js');
|
||||
const { buildStyle, themeStyle } = require('lib/theme');
|
||||
|
||||
interface ToolbarProps {
|
||||
theme: number,
|
||||
dispatch: Function,
|
||||
disabled: boolean,
|
||||
}
|
||||
|
||||
function styles_(props:ToolbarProps) {
|
||||
return buildStyle('AceEditorToolbar', props.theme, (/* theme:any*/) => {
|
||||
const theme = themeStyle(props.theme);
|
||||
return {
|
||||
root: {
|
||||
flex: 1,
|
||||
marginBottom: 0,
|
||||
borderTop: `1px solid ${theme.dividerColor}`,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function Toolbar(props:ToolbarProps) {
|
||||
const styles = styles_(props);
|
||||
|
||||
const cmdService = CommandService.instance();
|
||||
|
||||
const toolbarItems = [
|
||||
cmdService.commandToToolbarButton('textBold'),
|
||||
cmdService.commandToToolbarButton('textItalic'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textLink'),
|
||||
cmdService.commandToToolbarButton('textCode'),
|
||||
cmdService.commandToToolbarButton('attachFile'),
|
||||
{ type: 'separator' },
|
||||
cmdService.commandToToolbarButton('textNumberedList'),
|
||||
cmdService.commandToToolbarButton('textBulletedList'),
|
||||
cmdService.commandToToolbarButton('textCheckbox'),
|
||||
cmdService.commandToToolbarButton('textHeading'),
|
||||
cmdService.commandToToolbarButton('textHorizontalRule'),
|
||||
cmdService.commandToToolbarButton('insertDateTime'),
|
||||
];
|
||||
|
||||
return <ToolbarBase disabled={props.disabled} style={styles.root} items={toolbarItems} />;
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
import { NoteBodyEditorProps } from '../../../utils/types';
|
||||
const { buildStyle } = require('lib/theme');
|
||||
|
||||
export default function styles(props: NoteBodyEditorProps) {
|
||||
return buildStyle('AceEditor', props.theme, (theme: any) => {
|
||||
return {
|
||||
root: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...props.style,
|
||||
},
|
||||
rowToolbar: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
rowEditorViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
},
|
||||
cellEditor: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
},
|
||||
cellViewer: {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
borderLeftWidth: 1,
|
||||
borderLeftColor: theme.dividerColor,
|
||||
borderLeftStyle: 'solid',
|
||||
},
|
||||
viewer: {
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
},
|
||||
editor: {
|
||||
display: 'flex',
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
flex: 1,
|
||||
overflowY: 'hidden',
|
||||
paddingTop: 0,
|
||||
lineHeight: `${theme.textAreaLineHeight}px`,
|
||||
fontSize: `${theme.editorFontSize}px`,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
aceEditorTheme: theme.aceEditorTheme, // Defined in theme.js
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
@@ -1,219 +0,0 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
|
||||
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
|
||||
if (!body) return 0;
|
||||
|
||||
const noteLines = body.split('\n');
|
||||
|
||||
let pos = 0;
|
||||
for (let i = 0; i < noteLines.length; i++) {
|
||||
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
|
||||
|
||||
if (i === cursorPos.row) {
|
||||
pos += cursorPos.column;
|
||||
break;
|
||||
} else {
|
||||
pos += noteLines[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
export function currentTextOffset(editor: any, body: string) {
|
||||
return cursorPositionToTextOffset(editor.getCursorPosition(), body);
|
||||
}
|
||||
|
||||
export function rangeToTextOffsets(range: any, body: string) {
|
||||
return {
|
||||
start: cursorPositionToTextOffset(range.start, body),
|
||||
end: cursorPositionToTextOffset(range.end, body),
|
||||
};
|
||||
}
|
||||
|
||||
export function textOffsetSelection(selectionRange: any, body: string) {
|
||||
return selectionRange && body ? rangeToTextOffsets(selectionRange, body) : null;
|
||||
}
|
||||
|
||||
export function selectedText(selectionRange: any, body: string) {
|
||||
const selection = textOffsetSelection(selectionRange, body);
|
||||
if (!selection || selection.start === selection.end) return '';
|
||||
|
||||
return body.substr(selection.start, selection.end - selection.start);
|
||||
}
|
||||
|
||||
export function selectionRange(editor:any) {
|
||||
const ranges = editor.getSelection().getAllRanges();
|
||||
return ranges && ranges.length ? ranges[0] : null;
|
||||
}
|
||||
|
||||
export function textOffsetToCursorPosition(offset: number, body: string) {
|
||||
const lines = body.split('\n');
|
||||
let row = 0;
|
||||
let currentOffset = 0;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (currentOffset + line.length >= offset) {
|
||||
return {
|
||||
row: row,
|
||||
column: offset - currentOffset,
|
||||
};
|
||||
}
|
||||
|
||||
row++;
|
||||
currentOffset += line.length + 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function lineAtRow(body: string, row: number) {
|
||||
if (!body) return '';
|
||||
const lines = body.split('\n');
|
||||
if (row < 0 || row >= lines.length) return '';
|
||||
return lines[row];
|
||||
}
|
||||
|
||||
export function selectionRangeCurrentLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row);
|
||||
}
|
||||
|
||||
export function selectionRangePreviousLine(selectionRange: any, body: string) {
|
||||
if (!selectionRange) return '';
|
||||
return lineAtRow(body, selectionRange.start.row - 1);
|
||||
}
|
||||
|
||||
export function lineLeftSpaces(line: string) {
|
||||
let output = '';
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
if ([' ', '\t'].indexOf(line[i]) >= 0) {
|
||||
output += line[i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export function usePrevious(value: any): any {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export function useScrollHandler(editor: any, webviewRef: any, onScroll: Function) {
|
||||
const editorMaxScrollTop_ = useRef(0);
|
||||
const restoreScrollTop_ = useRef<any>(null);
|
||||
const ignoreNextEditorScrollEvent_ = useRef(false);
|
||||
const scrollTimeoutId_ = useRef<any>(null);
|
||||
|
||||
// TODO: Below is not needed anymore????
|
||||
//
|
||||
// this.editorMaxScrollTop_ = 0;
|
||||
// // HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// // and then (in the renderer callback) to the value we actually need. The first
|
||||
// // operation helps clear the scroll position cache. See:
|
||||
// //
|
||||
// this.editorSetScrollTop(1);
|
||||
// this.restoreScrollTop_ = 0;
|
||||
|
||||
const editorSetScrollTop = useCallback((v) => {
|
||||
if (!editor) return;
|
||||
editor.getSession().setScrollTop(v);
|
||||
}, [editor]);
|
||||
|
||||
// Complicated but reliable method to get editor content height
|
||||
// https://github.com/ajaxorg/ace/issues/2046
|
||||
const onAfterEditorRender = useCallback(() => {
|
||||
const r = editor.renderer;
|
||||
editorMaxScrollTop_.current = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight);
|
||||
|
||||
if (restoreScrollTop_.current !== null) {
|
||||
editorSetScrollTop(restoreScrollTop_.current);
|
||||
restoreScrollTop_.current = null;
|
||||
}
|
||||
}, [editor, editorSetScrollTop]);
|
||||
|
||||
const scheduleOnScroll = useCallback((event: any) => {
|
||||
if (scrollTimeoutId_.current) {
|
||||
clearTimeout(scrollTimeoutId_.current);
|
||||
scrollTimeoutId_.current = null;
|
||||
}
|
||||
|
||||
scrollTimeoutId_.current = setTimeout(() => {
|
||||
scrollTimeoutId_.current = null;
|
||||
onScroll(event);
|
||||
}, 10);
|
||||
}, [onScroll]);
|
||||
|
||||
const setEditorPercentScroll = useCallback((p: number) => {
|
||||
ignoreNextEditorScrollEvent_.current = true;
|
||||
editorSetScrollTop(p * editorMaxScrollTop_.current);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}, [editorSetScrollTop, scheduleOnScroll]);
|
||||
|
||||
const setViewerPercentScroll = useCallback((p: number) => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.wrappedInstance.send('setPercentScroll', p);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}
|
||||
}, [scheduleOnScroll]);
|
||||
|
||||
const editor_scroll = useCallback(() => {
|
||||
if (ignoreNextEditorScrollEvent_.current) {
|
||||
ignoreNextEditorScrollEvent_.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const m = editorMaxScrollTop_.current;
|
||||
const percent = m ? editor.getSession().getScrollTop() / m : 0;
|
||||
|
||||
setViewerPercentScroll(percent);
|
||||
}, [editor, setViewerPercentScroll]);
|
||||
|
||||
const resetScroll = useCallback(() => {
|
||||
if (!editor) return;
|
||||
|
||||
// Ace Editor caches scroll values, which makes
|
||||
// it hard to reset the scroll position, so we
|
||||
// need to use this hack.
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
editor.session.$scrollTop = -1;
|
||||
editor.session.$scrollLeft = -1;
|
||||
editor.renderer.scrollTop = -1;
|
||||
editor.renderer.scrollLeft = -1;
|
||||
editor.renderer.scrollBarV.scrollTop = -1;
|
||||
editor.renderer.scrollBarH.scrollLeft = -1;
|
||||
editor.session.setScrollTop(0);
|
||||
editor.session.setScrollLeft(0);
|
||||
}, [editorSetScrollTop, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
editor.renderer.on('afterRender', onAfterEditorRender);
|
||||
|
||||
return () => {
|
||||
editor.renderer.off('afterRender', onAfterEditorRender);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
|
||||
export function useRootWidth(dependencies:any) {
|
||||
const { rootRef } = dependencies;
|
||||
|
||||
const [rootWidth, setRootWidth] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
|
||||
if (rootWidth !== rootRef.current.offsetWidth) setRootWidth(rootRef.current.offsetWidth);
|
||||
});
|
||||
|
||||
return rootWidth;
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
export interface RenderedBody {
|
||||
html: string;
|
||||
pluginAssets: any[];
|
||||
}
|
||||
|
||||
export function defaultRenderedBody(): RenderedBody {
|
||||
return {
|
||||
html: '',
|
||||
pluginAssets: [],
|
||||
};
|
||||
}
|
@@ -1,178 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { selectionRange } from './index';
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
|
||||
// The line that contains only `- ` is
|
||||
// recognized as a heading in Ace.
|
||||
function hyphenEmptyListItem(tokens: any[]) {
|
||||
return (
|
||||
tokens.length === 2 &&
|
||||
tokens[0].type === 'markup.heading.2' &&
|
||||
tokens[0].value === '-' &&
|
||||
tokens[1].type === 'text.xml' &&
|
||||
tokens[1].value === ' '
|
||||
);
|
||||
}
|
||||
|
||||
// Returns tokens of the line if it starts with a 'markup.list' token.
|
||||
function listTokens(editor: any, row: number) {
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (
|
||||
!(tokens.length > 0 && tokens[0].type === 'markup.list') &&
|
||||
!hyphenEmptyListItem(tokens)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
function countIndent(line: string): number {
|
||||
return line.match(/\t| {4}/g)?.length || 0;
|
||||
}
|
||||
|
||||
// Finds the list item with indent level `prevIndent`.
|
||||
function findPrevListNum(editor: any, row: number, indent: number) {
|
||||
while (row > 0) {
|
||||
row--;
|
||||
const line = editor.session.getLine(row);
|
||||
|
||||
if (countIndent(line) === indent) {
|
||||
const num = markdownUtils.olLineNumber(line.trimLeft());
|
||||
if (num) {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
interface HookDependencies {
|
||||
editor: any;
|
||||
}
|
||||
|
||||
export default function useListIdent(dependencies: HookDependencies) {
|
||||
const { editor } = dependencies;
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return () => {};
|
||||
|
||||
// Markdown list indentation. (https://github.com/laurent22/joplin/pull/2713)
|
||||
// If the current line starts with `markup.list` token,
|
||||
// hitting `Tab` key indents the line instead of inserting tab at cursor.
|
||||
const originalEditorIndent = editor.indent;
|
||||
|
||||
editor.indent = function() {
|
||||
const range = selectionRange(editor);
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
const tokens = listTokens(this, row);
|
||||
|
||||
if (tokens.length > 0) {
|
||||
if (tokens[0].value.search(/\d+\./) != -1) {
|
||||
const line = this.session.getLine(row);
|
||||
const n = findPrevListNum(this, row, countIndent(line) + 1) + 1;
|
||||
this.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
|
||||
this.session.indentRows(row, row, '\t');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (originalEditorIndent) originalEditorIndent.call(this);
|
||||
};
|
||||
|
||||
// Correct the number of numbered list item when outdenting.
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownOutdent',
|
||||
bindKey: { win: 'Shift+Tab', mac: 'Shift+Tab' },
|
||||
multiSelectAction: 'forEachLine',
|
||||
exec: function(editor: any) {
|
||||
const range = selectionRange(editor);
|
||||
|
||||
if (range.isEmpty()) {
|
||||
const row = range.start.row;
|
||||
|
||||
const tokens = editor.session.getTokens(row);
|
||||
if (tokens.length && tokens[0].type === 'markup.list') {
|
||||
const matches = tokens[0].value.match(/^(\t+)\d+\./);
|
||||
if (matches && matches.length) {
|
||||
const indent = countIndent(matches[1]);
|
||||
const n = findPrevListNum(editor, row, indent - 1) + 1;
|
||||
console.log(n);
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: tokens[0].value.length },
|
||||
},
|
||||
tokens[0].value.replace(/\d+\./, `${n}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
editor.blockOutdent();
|
||||
},
|
||||
readonly: false,
|
||||
});
|
||||
|
||||
// Delete a list markup (e.g. `- `) from an empty list item on hitting Enter.
|
||||
// (https://github.com/laurent22/joplin/pull/2772)
|
||||
editor.commands.addCommand({
|
||||
name: 'markdownEnter',
|
||||
bindKey: 'Enter',
|
||||
multiSelectAction: 'forEach',
|
||||
exec: function(editor: any) {
|
||||
const range = editor.getSelectionRange();
|
||||
const tokens = listTokens(editor, range.start.row);
|
||||
|
||||
const emptyListItem =
|
||||
tokens.length === 1 || hyphenEmptyListItem(tokens);
|
||||
const emptyCheckboxItem =
|
||||
tokens.length === 3 &&
|
||||
['[ ]', '[x]'].includes(tokens[1].value) &&
|
||||
tokens[2].value === ' ';
|
||||
|
||||
if (!range.isEmpty() || !(emptyListItem || emptyCheckboxItem)) {
|
||||
editor.insert('\n');
|
||||
// Cursor can go out of the view after inserting '\n'.
|
||||
editor.renderer.scrollCursorIntoView();
|
||||
return;
|
||||
}
|
||||
|
||||
const row = range.start.row;
|
||||
const line = editor.session.getLine(row);
|
||||
let indent = editor
|
||||
.getSession()
|
||||
.getMode()
|
||||
.getNextLineIndent(null, line);
|
||||
if (indent.startsWith('\t')) {
|
||||
indent = indent.slice(1);
|
||||
} else {
|
||||
indent = '';
|
||||
}
|
||||
|
||||
editor.session.replace(
|
||||
{
|
||||
start: { row, column: 0 },
|
||||
end: { row, column: line.length },
|
||||
},
|
||||
indent
|
||||
);
|
||||
},
|
||||
readOnly: false,
|
||||
});
|
||||
|
||||
return () => {
|
||||
editor.indent = originalEditorIndent;
|
||||
editor.commands.removeCommand('markdownOutdent');
|
||||
editor.commands.removeCommand('markdownEnter');
|
||||
};
|
||||
}, [editor]);
|
||||
}
|
@@ -113,7 +113,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
},
|
||||
supportsCommand: (/* name:string*/) => {
|
||||
// TODO: not implemented, currently only used for "search" command
|
||||
// which is not directly supported by Ace Editor.
|
||||
// which is not directly supported by this Editor.
|
||||
return false;
|
||||
},
|
||||
execCommand: async (cmd: EditorCommand) => {
|
||||
@@ -308,11 +308,6 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
async function loadScripts() {
|
||||
const scriptsToLoad:{src: string, id:string, loaded: boolean}[] = [
|
||||
{
|
||||
src: 'node_modules/codemirror/lib/codemirror.css',
|
||||
id: 'codemirrorBaseStyle',
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
src: 'node_modules/codemirror/addon/dialog/dialog.css',
|
||||
id: 'codemirrorDialogStyle',
|
||||
@@ -484,36 +479,12 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchMarkers !== previousSearchMarkers || renderedBody !== previousRenderedBody) {
|
||||
// SEARCHHACK
|
||||
// TODO: remove this options hack when aceeditor is removed
|
||||
// Currently the webviewRef will send out an ipcMessage to set the results count
|
||||
// Also setting it here will start an infinite loop of repeating the search
|
||||
// Unfortunately we can't remove the function in the webview setMarkers
|
||||
// until the aceeditor is remove.
|
||||
// The below search is more accurate than the webview based one as it searches
|
||||
// the text and not rendered html (rendered html fails if there is a match
|
||||
// in a katex block)
|
||||
// Once AceEditor is removed the options definition below can be removed and
|
||||
// props.searchMarkers.options can be directly passed to as the 3rd argument below
|
||||
// (replacing options)
|
||||
let options = { notFromAce: true };
|
||||
if (props.searchMarkers.options) {
|
||||
options = Object.assign({}, props.searchMarkers.options, options);
|
||||
}
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, options);
|
||||
// SEARCHHACK
|
||||
webviewRef.current.wrappedInstance.send('setMarkers', props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
if (editorRef.current) {
|
||||
const matches = editorRef.current.setMarkers(props.searchMarkers.keywords, props.searchMarkers.options);
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: when aceeditor is removed then this check will be performed in the NoteSearchbar
|
||||
// End the if statement can be removed in favor of simply returning matches
|
||||
if (props.visiblePanes.includes('editor')) {
|
||||
props.setLocalSearchResultCount(matches);
|
||||
} else {
|
||||
props.setLocalSearchResultCount(-1);
|
||||
}
|
||||
// end SEARCHHACK
|
||||
props.setLocalSearchResultCount(matches);
|
||||
}
|
||||
}
|
||||
}, [props.searchMarkers, props.setLocalSearchResultCount, renderedBody]);
|
||||
|
@@ -139,8 +139,8 @@ function Editor(props: EditorProps, ref: any) {
|
||||
'Right': 'goCharRight',
|
||||
'Up': 'goLineUp',
|
||||
'Down': 'goLineDown',
|
||||
'End': 'goLineEnd',
|
||||
'Home': 'goLineStartSmart',
|
||||
'End': 'goLineRight',
|
||||
'Home': 'goLineLeftSmart',
|
||||
'PageUp': 'goPageUp',
|
||||
'PageDown': 'goPageDown',
|
||||
'Delete': 'delCharAfter',
|
||||
|
@@ -267,7 +267,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
} else if (cmd.value.type === 'files') {
|
||||
insertResourcesIntoContentRef.current(cmd.value.paths, { createFileURL: !!cmd.value.createFileURL });
|
||||
} else {
|
||||
reg.logger().warn('AceEditor: unsupported drop item: ', cmd);
|
||||
reg.logger().warn('TinyMCE: unsupported drop item: ', cmd);
|
||||
}
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
@@ -626,6 +626,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
update: function(element:any) {
|
||||
let itemType:ContextMenuItemType = ContextMenuItemType.None;
|
||||
let resourceId = '';
|
||||
let linkToCopy = null;
|
||||
|
||||
if (element.nodeName === 'IMG') {
|
||||
itemType = ContextMenuItemType.Image;
|
||||
@@ -633,6 +634,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
} else if (element.nodeName === 'A') {
|
||||
resourceId = Resource.pathToId(element.href);
|
||||
itemType = resourceId ? ContextMenuItemType.Resource : ContextMenuItemType.Link;
|
||||
linkToCopy = element.getAttribute('href') || '';
|
||||
} else {
|
||||
itemType = ContextMenuItemType.Text;
|
||||
}
|
||||
@@ -640,6 +642,7 @@ const TinyMCE = (props:NoteBodyEditorProps, ref:any) => {
|
||||
contextMenuActionOptions.current = {
|
||||
itemType,
|
||||
resourceId,
|
||||
linkToCopy,
|
||||
textToCopy: null,
|
||||
htmlToCopy: editor.selection ? editor.selection.getContent() : '',
|
||||
insertContent: (content:string) => {
|
||||
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import TinyMCE from './NoteBody/TinyMCE/TinyMCE';
|
||||
import AceEditor from './NoteBody/AceEditor/AceEditor';
|
||||
import CodeMirror from './NoteBody/CodeMirror/CodeMirror';
|
||||
import { connect } from 'react-redux';
|
||||
import MultiNoteActions from '../MultiNoteActions';
|
||||
@@ -365,7 +364,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId);
|
||||
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
|
||||
|
||||
const editorProps:NoteBodyEditorProps = {
|
||||
ref: editorRef,
|
||||
@@ -398,8 +397,6 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
|
||||
if (props.bodyEditor === 'TinyMCE') {
|
||||
editor = <TinyMCE {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'AceEditor') {
|
||||
editor = <AceEditor {...editorProps}/>;
|
||||
} else if (props.bodyEditor === 'CodeMirror') {
|
||||
editor = <CodeMirror {...editorProps}/>;
|
||||
} else {
|
||||
@@ -466,6 +463,7 @@ function NoteEditor(props: NoteEditorProps) {
|
||||
onNext={localSearch_next}
|
||||
onPrevious={localSearch_previous}
|
||||
onClose={localSearch_close}
|
||||
visiblePanes={props.noteVisiblePanes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -531,6 +529,7 @@ const mapStateToProps = (state: any) => {
|
||||
customCss: state.customCss,
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
watchedResources: state.watchedResources,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -20,6 +20,7 @@ export enum ContextMenuItemType {
|
||||
export interface ContextMenuOptions {
|
||||
itemType: ContextMenuItemType,
|
||||
resourceId: string,
|
||||
linkToCopy: string,
|
||||
textToCopy: string,
|
||||
htmlToCopy: string,
|
||||
insertContent: Function,
|
||||
@@ -114,7 +115,7 @@ export function menuItems():ContextMenuItems {
|
||||
copyLinkUrl: {
|
||||
label: _('Copy Link Address'),
|
||||
onAction: async (options:ContextMenuOptions) => {
|
||||
clipboard.writeText(options.textToCopy);
|
||||
clipboard.writeText(options.linkToCopy !== null ? options.linkToCopy : options.textToCopy);
|
||||
},
|
||||
isActive: (itemType:ContextMenuItemType) => itemType === ContextMenuItemType.Link,
|
||||
},
|
||||
|
@@ -23,6 +23,7 @@ export interface NoteEditorProps {
|
||||
customCss: string,
|
||||
noteVisiblePanes: string[],
|
||||
watchedResources: any,
|
||||
highlightedWords: any[],
|
||||
}
|
||||
|
||||
export interface NoteBodyEditorProps {
|
||||
|
@@ -41,6 +41,7 @@ export default function useMessageHandler(scrollWhenReady:any, setScrollWhenRead
|
||||
itemType: arg0 && arg0.type,
|
||||
resourceId: arg0.resourceId,
|
||||
textToCopy: arg0.textToCopy,
|
||||
linkToCopy: null,
|
||||
htmlToCopy: '',
|
||||
insertContent: () => { console.warn('insertContent() not implemented'); },
|
||||
});
|
||||
|
@@ -1,8 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
|
||||
interface SearchMarkersOptions {
|
||||
searchTimestamp: number,
|
||||
selectedIndex: number,
|
||||
@@ -25,18 +22,14 @@ function defaultSearchMarkers():SearchMarkers {
|
||||
};
|
||||
}
|
||||
|
||||
export default function useSearchMarkers(showLocalSearch:boolean, localSearchMarkerOptions:Function, searches:any[], selectedSearchId:string) {
|
||||
|
||||
export default function useSearchMarkers(showLocalSearch:boolean, localSearchMarkerOptions:Function, searches:any[], selectedSearchId:string, highlightedWords: any[] = []) {
|
||||
return useMemo(():SearchMarkers => {
|
||||
if (showLocalSearch) return localSearchMarkerOptions();
|
||||
|
||||
const output = defaultSearchMarkers();
|
||||
|
||||
const search = BaseModel.byId(searches, selectedSearchId);
|
||||
if (search) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(search.query_pattern);
|
||||
output.keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
output.keywords = highlightedWords;
|
||||
|
||||
return output;
|
||||
}, [showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
|
||||
}, [highlightedWords, showLocalSearch, localSearchMarkerOptions, searches, selectedSearchId]);
|
||||
}
|
||||
|
@@ -7,7 +7,6 @@ const BaseModel = require('lib/BaseModel');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const eventManager = require('lib/eventManager');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const Note = require('lib/models/Note');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const NoteListUtils = require('../utils/NoteListUtils');
|
||||
@@ -229,8 +228,7 @@ class NoteListComponent extends React.Component {
|
||||
if (this.props.notesParentType === 'Search') {
|
||||
const query = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
if (query) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(query.query_pattern);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
return this.props.highlightedWords;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
@@ -460,6 +458,7 @@ const mapStateToProps = state => {
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
isInsertingNotes: state.isInsertingNotes,
|
||||
noteSortOrder: state.settings['notes.sortOrder.field'],
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -148,14 +148,7 @@ class NoteSearchBarComponent extends React.Component {
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// Currently searching in the viewer does not support jumping between matches
|
||||
// So we explicitly disable those commands when only the viewer is open (this is
|
||||
// currently signaled by results count being set to -1, but once Ace editor is removed
|
||||
// we can observe the visible panes directly).
|
||||
// SEARCHHACK
|
||||
// TODO: remove the props.resultCount check here and replace it by checking visible panes directly
|
||||
const allowScrolling = this.props.resultCount !== -1;
|
||||
// end SEARCHHACK
|
||||
const allowScrolling = this.props.visiblePanes.indexOf('editor') >= 0;
|
||||
|
||||
const viewerWarning = (
|
||||
<div style={textStyle}>
|
||||
|
@@ -277,25 +277,7 @@
|
||||
let selectedElement = null;
|
||||
let elementIndex = 0;
|
||||
|
||||
const onEachElement = (element) => {
|
||||
// SEARCHHACK
|
||||
// TODO: remove notFromAce hack when removing aceeditor
|
||||
// when removing just remove the 'notFromAce' part and leave the rest alone
|
||||
if (!('selectedIndex' in options) || 'notFromAce' in options) return;
|
||||
// SEARCHHACK
|
||||
|
||||
if (('selectedIndex' in options) && elementIndex === options.selectedIndex) {
|
||||
markSelectedElement_ = element;
|
||||
element.classList.add('mark-selected');
|
||||
selectedElement = element;
|
||||
}
|
||||
|
||||
elementIndex++;
|
||||
}
|
||||
|
||||
const markKeywordOptions = {
|
||||
each: onEachElement,
|
||||
};
|
||||
const markKeywordOptions = {};
|
||||
|
||||
if ('separateWordSearch' in options) markKeywordOptions.separateWordSearch = options.separateWordSearch;
|
||||
|
||||
@@ -307,22 +289,6 @@
|
||||
replaceRegexDiacritics: replaceRegexDiacritics,
|
||||
}, markKeywordOptions);
|
||||
}
|
||||
|
||||
// SEARCHHACK
|
||||
// TODO: Remove this block (until the other SEARCHHACK marker) when removing Ace
|
||||
// HACK: Aceeditor uses this view to handle all the searching
|
||||
// The newer editor wont and this needs to be disabled in order to
|
||||
// prevent an infinite loop
|
||||
if (!('notFromAce' in options)) {
|
||||
ipcProxySendToHost('setMarkerCount', elementIndex);
|
||||
|
||||
// We only scroll the element into view if the search just happened. So when the user type the search
|
||||
// or select the next/previous result, we scroll into view. However for other actions that trigger a
|
||||
// re-render, we don't scroll as this is normally not wanted.
|
||||
// This is to go around this issue: https://github.com/laurent22/joplin/issues/1833
|
||||
if (selectedElement && Date.now() - options.searchTimestamp <= 1000) selectedElement.scrollIntoView();
|
||||
}
|
||||
// SEARCHHACK
|
||||
}
|
||||
|
||||
let markLoader_ = { state: 'idle', whenDone: null };
|
||||
|
@@ -25,7 +25,7 @@ class NoteListUtils {
|
||||
|
||||
if (!hasEncrypted) {
|
||||
menu.append(
|
||||
new MenuItem(cmdService.commandToMenuItem('setTags'))
|
||||
new MenuItem(cmdService.commandToMenuItem('setTags', { noteIds }))
|
||||
);
|
||||
|
||||
menu.append(
|
||||
|
@@ -17,6 +17,9 @@ const tasks = {
|
||||
electronRebuild: {
|
||||
fn: require('./tools/electronRebuild.js'),
|
||||
},
|
||||
compileExtensions: {
|
||||
fn: require('../Tools/gulp/tasks/compileExtensions.js'),
|
||||
},
|
||||
copyLib: require('../Tools/gulp/tasks/copyLib'),
|
||||
tsc: require('../Tools/gulp/tasks/tsc'),
|
||||
updateIgnoredTypeScriptBuild: require('../Tools/gulp/tasks/updateIgnoredTypeScriptBuild'),
|
||||
@@ -25,6 +28,7 @@ const tasks = {
|
||||
utils.registerGulpTasks(gulp, tasks);
|
||||
|
||||
const buildSeries = [
|
||||
'compileExtensions',
|
||||
'copyLib',
|
||||
];
|
||||
|
||||
|
@@ -12,6 +12,7 @@
|
||||
<link rel="stylesheet" href="node_modules/@fortawesome/fontawesome-free/css/all.min.css">
|
||||
<link rel="stylesheet" href="node_modules/react-datetime/css/react-datetime.css">
|
||||
<link rel="stylesheet" href="node_modules/smalltalk/css/smalltalk.css">
|
||||
<link rel="stylesheet" href="node_modules/codemirror/lib/codemirror.css">
|
||||
|
||||
<style>
|
||||
.smalltalk {
|
||||
|
268
ElectronClient/package-lock.json
generated
268
ElectronClient/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.242",
|
||||
"version": "1.0.243",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -51,6 +51,10 @@
|
||||
{
|
||||
"from": "build-win/Joplin.VisualElementsManifest.xml",
|
||||
"to": "."
|
||||
},
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.dll",
|
||||
"to": "usr/lib/spellfix.dll"
|
||||
}
|
||||
],
|
||||
"extraResources": [
|
||||
@@ -65,7 +69,13 @@
|
||||
"artifactName": "${productName}Portable.${ext}"
|
||||
},
|
||||
"mac": {
|
||||
"icon": "../../Assets/macOs.icns"
|
||||
"icon": "../../Assets/macOs.icns",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.dylib",
|
||||
"to": "usr/lib/spellfix.dylib"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"icon": "../Assets/LinuxIcons/256x256.png",
|
||||
@@ -73,7 +83,13 @@
|
||||
"desktop": {
|
||||
"Icon": "joplin"
|
||||
},
|
||||
"target": "AppImage"
|
||||
"target": "AppImage",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "lib/sql-extensions/spellfix.so",
|
||||
"to": "usr/lib/spellfix.so"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"homepage": "https://github.com/laurent22/joplin#readme",
|
||||
@@ -162,7 +178,6 @@
|
||||
"promise": "^8.0.1",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.9.0",
|
||||
"react-ace": "^6.1.4",
|
||||
"react-datetime": "^2.14.0",
|
||||
"react-dom": "^16.9.0",
|
||||
"react-redux": "^5.0.7",
|
||||
|
@@ -177,9 +177,8 @@ class Dialog extends React.PureComponent {
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
keywords(searchQuery) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(searchQuery);
|
||||
return SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
keywords() {
|
||||
return this.props.highlightedWords;
|
||||
}
|
||||
|
||||
markupToHtml() {
|
||||
@@ -227,7 +226,7 @@ class Dialog extends React.PureComponent {
|
||||
}
|
||||
} else {
|
||||
const limit = 20;
|
||||
const searchKeywords = this.keywords(searchQuery);
|
||||
const searchKeywords = this.keywords();
|
||||
const notes = await Note.byIds(results.map(result => result.id).slice(0, limit), { fields: ['id', 'body', 'markup_language', 'is_todo', 'todo_completed'] });
|
||||
const notesById = notes.reduce((obj, { id, body, markup_language }) => ((obj[[id]] = { id, body, markup_language }), obj), {});
|
||||
|
||||
@@ -283,7 +282,7 @@ class Dialog extends React.PureComponent {
|
||||
this.setState({
|
||||
listType: listType,
|
||||
results: results,
|
||||
keywords: this.keywords(searchQuery),
|
||||
keywords: this.keywords(),
|
||||
selectedItemId: results.length === 0 ? null : results[0].id,
|
||||
resultsInBody: resultsInBody,
|
||||
});
|
||||
@@ -455,6 +454,7 @@ const mapStateToProps = (state) => {
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -63,13 +63,6 @@ a {
|
||||
transition: 0.3s;
|
||||
opacity: 1;
|
||||
}
|
||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||
as red boxes, but since those are actually valid characters and common in imported
|
||||
Evernote data, we hide them here. */
|
||||
.ace-chrome .ace_invisible_space {
|
||||
background-color: transparent !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.note-list .list-item-container:hover {
|
||||
background-color: rgba(0,160,255,0.1) !important;
|
||||
|
@@ -64,6 +64,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
| :---: | :---: | :---: |
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/6979755?s=96&v=4"/></br>[Devon Zuegel](https://github.com/devonzuegel) | <img width="50" src="https://avatars2.githubusercontent.com/u/24908652?s=96&v=4"/></br>[小西 孝宗](https://github.com/konishi-t) | <img width="50" src="https://avatars2.githubusercontent.com/u/215668?s=96&v=4"/></br>[Alexander van der Berg](https://github.com/avanderberg)
|
||||
| <img width="50" src="https://avatars0.githubusercontent.com/u/1168659?s=96&v=4"/></br>[Nicholas Head](https://github.com/nicholashead) | <img width="50" src="https://avatars2.githubusercontent.com/u/1439535?s=96&v=4"/></br>[Frank Bloise](https://github.com/fbloise) | <img width="50" src="https://avatars2.githubusercontent.com/u/15859362?s=96&v=4"/></br>[Thomas Broussard](https://github.com/thomasbroussard)
|
||||
| <img width="50" src="https://avatars2.githubusercontent.com/u/1307332?s=96&v=4"/></br>[Brandon Johnson](https://github.com/dbrandonjohnson) | |
|
||||
|
||||
<!-- TOC -->
|
||||
# Table of contents
|
||||
@@ -87,7 +88,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
|
||||
|
||||
- Development
|
||||
|
||||
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec/e2ee.md)
|
||||
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md)
|
||||
- [Note History spec](https://github.com/laurent22/joplin/blob/master/readme/spec/history.md)
|
||||
- [Sync Lock spec](https://github.com/laurent22/joplin/blob/master/readme/spec/sync_lock.md)
|
||||
|
||||
|
@@ -280,6 +280,7 @@ class BaseApplication {
|
||||
});
|
||||
|
||||
let notes = [];
|
||||
let highlightedWords = [];
|
||||
|
||||
if (parentId) {
|
||||
if (parentType === Folder.modelType()) {
|
||||
@@ -288,12 +289,21 @@ class BaseApplication {
|
||||
notes = await Tag.notes(parentId, options);
|
||||
} else if (parentType === BaseModel.TYPE_SEARCH) {
|
||||
const search = BaseModel.byId(state.searches, parentId);
|
||||
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
|
||||
notes = await SearchEngineUtils.notesForQuery(search.query_pattern, { fuzzy: search.fuzzy });
|
||||
const parsedQuery = await SearchEngine.instance().parseQuery(search.query_pattern, search.fuzzy);
|
||||
highlightedWords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
} else if (parentType === BaseModel.TYPE_SMART_FILTER) {
|
||||
notes = await Note.previews(parentId, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightedWords.length) {
|
||||
this.store().dispatch({
|
||||
type: 'SET_HIGHLIGHTED',
|
||||
words: highlightedWords,
|
||||
});
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_UPDATE_ALL',
|
||||
notes: notes,
|
||||
@@ -681,6 +691,23 @@ class BaseApplication {
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
this.database_.setLogger(this.dbLogger_);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
if (shim.isElectron()) {
|
||||
this.database_.extensionToLoad = './lib/sql-extensions/spellfix';
|
||||
}
|
||||
} else {
|
||||
if (shim.isElectron()) {
|
||||
if (shim.isWindows()) {
|
||||
const appDir = process.execPath.substring(0, process.execPath.lastIndexOf('\\'));
|
||||
this.database_.extensionToLoad = `${appDir}/usr/lib/spellfix`;
|
||||
} else {
|
||||
const appDir = process.execPath.substring(0, process.execPath.lastIndexOf('/'));
|
||||
this.database_.extensionToLoad = `${appDir}/usr/lib/spellfix`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.database_.open({ name: `${profileDir}/database.sqlite` });
|
||||
|
||||
// if (Setting.value('env') === 'dev') await this.database_.clearForTesting();
|
||||
@@ -707,6 +734,11 @@ class BaseApplication {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
if (Setting.value('db.fuzzySearchEnabled') === -1) {
|
||||
const fuzzySearchEnabled = await this.database_.fuzzySearchEnabled();
|
||||
Setting.setValue('db.fuzzySearchEnabled', fuzzySearchEnabled ? 1 : 0);
|
||||
}
|
||||
|
||||
if (Setting.value('encryption.shouldReencrypt') < 0) {
|
||||
// We suggest re-encryption if the user has at least one notebook
|
||||
// and if encryption is enabled. This code runs only when shouldReencrypt = -1
|
||||
|
@@ -93,7 +93,7 @@ class ActionButtonComponent extends React.Component {
|
||||
}
|
||||
|
||||
if (!buttonComps.length && !this.props.mainButton) {
|
||||
return <ReactNativeActionButton style={{ display: 'none' }} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
const mainButton = this.props.mainButton ? this.props.mainButton : {};
|
||||
|
@@ -40,7 +40,6 @@ const ImagePicker = require('react-native-image-picker');
|
||||
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
|
||||
const ShareExtension = require('lib/ShareExtension.js').default;
|
||||
const CameraView = require('lib/components/CameraView');
|
||||
const SearchEngine = require('lib/services/searchengine/SearchEngine');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
@@ -975,8 +974,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
|
||||
@@ -1016,8 +1014,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
keywords = this.props.highlightedWords;
|
||||
}
|
||||
|
||||
const onCheckboxChange = newBody => {
|
||||
@@ -1202,6 +1199,7 @@ const NoteScreen = connect(state => {
|
||||
sharedData: state.sharedData,
|
||||
showSideMenu: state.showSideMenu,
|
||||
provisionalNoteIds: state.provisionalNoteIds,
|
||||
highlightedWords: state.highlightedWords,
|
||||
};
|
||||
})(NoteScreenComponent);
|
||||
|
||||
|
@@ -227,9 +227,12 @@ class NotesScreenComponent extends BaseScreenComponent {
|
||||
);
|
||||
}
|
||||
|
||||
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
|
||||
let buttonFolderId = this.props.selectedFolderId != Folder.conflictFolderId() ? this.props.selectedFolderId : null;
|
||||
if (!buttonFolderId) buttonFolderId = this.props.activeFolderId;
|
||||
|
||||
const addFolderNoteButtons = !!buttonFolderId;
|
||||
const thisComp = this;
|
||||
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>;
|
||||
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={buttonFolderId}></ActionButton>;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
@@ -250,6 +253,7 @@ const NotesScreen = connect(state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
activeFolderId: state.settings.activeFolderId,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedTagId: state.selectedTagId,
|
||||
|
@@ -156,6 +156,12 @@ shared.settingsSections = createSelector(
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
output.push({
|
||||
name: 'keymap',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
@@ -36,6 +36,10 @@ class DatabaseDriverNode {
|
||||
});
|
||||
}
|
||||
|
||||
loadExtension(path) {
|
||||
return this.db_.loadExtension(path);
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@@ -50,6 +50,10 @@ class DatabaseDriverReactNative {
|
||||
});
|
||||
}
|
||||
|
||||
loadExtension(path) {
|
||||
throw new Error(`No extension support for ${path} in react-native-sqlite-storage`);
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(
|
||||
|
@@ -97,6 +97,16 @@ class Database {
|
||||
return this.tryCall('selectOne', sql, params);
|
||||
}
|
||||
|
||||
async loadExtension(path) {
|
||||
let result = null;
|
||||
try {
|
||||
result = await this.driver().loadExtension(path);
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw new Error(`Could not load extension ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
async selectAll(sql, params = null) {
|
||||
return this.tryCall('selectAll', sql, params);
|
||||
}
|
||||
|
@@ -126,6 +126,7 @@ class JoplinDatabase extends Database {
|
||||
this.tableFields_ = null;
|
||||
this.version_ = null;
|
||||
this.tableFieldNames_ = {};
|
||||
this.extensionToLoad = './build/lib/sql-extensions/spellfix';
|
||||
this.eventEmitter_ = new EventEmitter();
|
||||
}
|
||||
|
||||
@@ -283,6 +284,8 @@ class JoplinDatabase extends Database {
|
||||
if (tableName == 'table_fields') continue;
|
||||
if (tableName == 'sqlite_sequence') continue;
|
||||
if (tableName.indexOf('notes_fts') === 0) continue;
|
||||
if (tableName == 'notes_spellfix') continue;
|
||||
if (tableName == 'search_aux') continue;
|
||||
chain.push(() => {
|
||||
return this.selectAll(`PRAGMA table_info("${tableName}")`).then(pragmas => {
|
||||
for (let i = 0; i < pragmas.length; i++) {
|
||||
@@ -332,7 +335,7 @@ class JoplinDatabase extends Database {
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
// Note: v16 and v17 don't do anything. They were used to debug an issue.
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
|
||||
@@ -848,13 +851,20 @@ class JoplinDatabase extends Database {
|
||||
queries.push(this.addMigrationFile(33));
|
||||
}
|
||||
|
||||
if (targetVersion == 34) {
|
||||
queries.push('CREATE VIRTUAL TABLE search_aux USING fts4aux(notes_fts)');
|
||||
queries.push('CREATE VIRTUAL TABLE notes_spellfix USING spellfix1');
|
||||
}
|
||||
|
||||
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
|
||||
|
||||
try {
|
||||
await this.transactionExecBatch(queries);
|
||||
} catch (error) {
|
||||
if (targetVersion === 15 || targetVersion === 18 || targetVersion === 33) {
|
||||
this.logger().warn('Could not upgrade to database v15 or v18 or v33- FTS feature will not be used', error);
|
||||
this.logger().warn('Could not upgrade to database v15 or v18 or v33 - FTS feature will not be used', error);
|
||||
} else if (targetVersion === 34) {
|
||||
this.logger().warn('Could not upgrade to database v34 - fuzzy search will not be used', error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -881,6 +891,17 @@ class JoplinDatabase extends Database {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fuzzySearchEnabled() {
|
||||
try {
|
||||
await this.selectOne('SELECT count(*) FROM notes_spellfix');
|
||||
} catch (error) {
|
||||
this.logger().warn('Fuzzy search check failed', error);
|
||||
return false;
|
||||
}
|
||||
this.logger().info('Fuzzy search check succeeded');
|
||||
return true;
|
||||
}
|
||||
|
||||
version() {
|
||||
return this.version_;
|
||||
}
|
||||
@@ -888,6 +909,12 @@ class JoplinDatabase extends Database {
|
||||
async initialize() {
|
||||
this.logger().info('Checking for database schema update...');
|
||||
|
||||
try {
|
||||
await this.loadExtension(this.extensionToLoad);
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
}
|
||||
|
||||
let versionRow = null;
|
||||
try {
|
||||
// Will throw if the database has not been created yet, but this is handled below
|
||||
|
@@ -91,7 +91,7 @@ class HtmlUtils {
|
||||
// that can break several plugins, such as Katex (which needs to load CSS
|
||||
// files using a relative URL). For that reason it is disabled.
|
||||
// More info: https://github.com/laurent22/joplin/issues/3021
|
||||
const disallowedTags = ['script', 'iframe', 'frameset', 'frame', 'object', 'base'];
|
||||
const disallowedTags = ['script', 'iframe', 'frameset', 'frame', 'object', 'base', 'embed'];
|
||||
|
||||
const parser = new htmlparser2.Parser({
|
||||
|
||||
|
@@ -19,7 +19,7 @@ module.exports = function(theme) {
|
||||
b,strong{font-weight:bolder}small{font-size:80%}img{border-style:none}
|
||||
|
||||
body {
|
||||
font-size: ${theme.noteViewerFontSize};
|
||||
font-size: ${formatCssSize(theme.noteViewerFontSize)};
|
||||
color: ${theme.color};
|
||||
word-wrap: break-word;
|
||||
line-height: ${theme.lineHeight};
|
||||
|
@@ -252,7 +252,12 @@ class Setting extends BaseModel {
|
||||
|
||||
'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
|
||||
// The active folder ID is guaranteed to be valid as long as there's at least one
|
||||
// existing folder, so it is a good default in contexts where there's no currently
|
||||
// selected folder. It corresponds in general to the currently selected folder or
|
||||
// to the last folder that was selected.
|
||||
activeFolderId: { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
|
||||
firstStart: { value: true, type: Setting.TYPE_BOOL, public: false },
|
||||
locale: {
|
||||
value: defaultLocale(),
|
||||
@@ -392,14 +397,6 @@ class Setting extends BaseModel {
|
||||
appTypes: ['desktop'],
|
||||
label: () => _('Auto-pair braces, parenthesis, quotations, etc.'),
|
||||
},
|
||||
'editor.betaCodeMirror': {
|
||||
value: false,
|
||||
type: Setting.TYPE_BOOL,
|
||||
public: true,
|
||||
section: 'note',
|
||||
appTypes: ['desktop'],
|
||||
label: () => _('Use CodeMirror as the code editor (WARNING: BETA).'),
|
||||
},
|
||||
'notes.sortOrder.reverse': { value: true, type: Setting.TYPE_BOOL, section: 'note', public: true, label: () => _('Reverse sort order'), appTypes: ['cli'] },
|
||||
'folders.sortOrder.field': {
|
||||
value: 'title',
|
||||
@@ -506,6 +503,7 @@ class Setting extends BaseModel {
|
||||
|
||||
'keychain.supported': { value: -1, type: Setting.TYPE_INT, public: false },
|
||||
'db.ftsEnabled': { value: -1, type: Setting.TYPE_INT, public: false },
|
||||
'db.fuzzySearchEnabled': { value: -1, type: Setting.TYPE_INT, public: false },
|
||||
'encryption.enabled': { value: false, type: Setting.TYPE_BOOL, public: false },
|
||||
'encryption.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'encryption.passwordCache': { value: {}, type: Setting.TYPE_OBJECT, public: false, secure: true },
|
||||
@@ -1258,6 +1256,7 @@ class Setting extends BaseModel {
|
||||
if (name === 'revisionService') return _('Note History');
|
||||
if (name === 'encryption') return _('Encryption');
|
||||
if (name === 'server') return _('Web Clipper');
|
||||
if (name === 'keymap') return _('Keyboard Shortcuts');
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -1277,6 +1276,7 @@ class Setting extends BaseModel {
|
||||
if (name === 'revisionService') return 'fas fa-history';
|
||||
if (name === 'encryption') return 'fas fa-key';
|
||||
if (name === 'server') return 'far fa-hand-scissors';
|
||||
if (name === 'keymap') return 'fa fa-keyboard';
|
||||
return name;
|
||||
}
|
||||
|
||||
|
@@ -61,7 +61,10 @@ for (let i = 0; i < 32; i++) {
|
||||
const friendlySafeFilename_blackListNames = ['.', '..', 'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'];
|
||||
|
||||
function friendlySafeFilename(e, maxLength = null) {
|
||||
if (maxLength === null) maxLength = 255;
|
||||
// Although Windows supports paths up to 255 characters, but that includes the filename and its
|
||||
// parent directory path. Also there's generally no good reason for dir or file names
|
||||
// to be so long, so keep it at 50, which should prevent various errors.
|
||||
if (maxLength === null) maxLength = 50;
|
||||
if (!e || !e.replace) return _('Untitled');
|
||||
|
||||
let output = '';
|
||||
|
@@ -12,6 +12,7 @@ const defaultState = {
|
||||
masterKeys: [],
|
||||
notLoadedMasterKeys: [],
|
||||
searches: [],
|
||||
highlightedWords: [],
|
||||
selectedNoteIds: [],
|
||||
selectedNoteHash: '',
|
||||
selectedFolderId: null,
|
||||
@@ -946,6 +947,9 @@ const reducer = (state = defaultState, action) => {
|
||||
}
|
||||
newState.selectedNoteIds = [];
|
||||
break;
|
||||
case 'SET_HIGHLIGHTED':
|
||||
newState = Object.assign({}, state, { highlightedWords: action.words });
|
||||
break;
|
||||
|
||||
case 'APP_STATE_SET':
|
||||
newState = Object.assign({}, state);
|
||||
|
@@ -157,6 +157,7 @@ export default class CommandService extends BaseService {
|
||||
options = {
|
||||
mustExist: true,
|
||||
runtimeMustBeRegistered: false,
|
||||
...options,
|
||||
};
|
||||
|
||||
const command = this.commands_[name];
|
||||
@@ -248,6 +249,17 @@ export default class CommandService extends BaseService {
|
||||
return command.runtime.title(command.runtime.props);
|
||||
}
|
||||
|
||||
label(commandName:string):string {
|
||||
const command = this.commandByName(commandName);
|
||||
if (!command) throw new Error(`Command: ${commandName} is not declared`);
|
||||
return command.declaration.label();
|
||||
}
|
||||
|
||||
exists(commandName:string):boolean {
|
||||
const command = this.commandByName(commandName, { mustExist: false });
|
||||
return !!command;
|
||||
}
|
||||
|
||||
private extractExecuteArgs(command:Command, executeArgs:any) {
|
||||
if (executeArgs) return executeArgs;
|
||||
if (!command.runtime) throw new Error(`Command: ${command.declaration.name}: Runtime is not defined - make sure it has been registered.`);
|
||||
@@ -281,7 +293,7 @@ export default class CommandService extends BaseService {
|
||||
};
|
||||
|
||||
if (command.declaration.role) item.role = command.declaration.role;
|
||||
if (this.keymapService.hasAccelerator(commandName)) {
|
||||
if (this.keymapService.acceleratorExists(commandName)) {
|
||||
item.accelerator = this.keymapService.getAccelerator(commandName);
|
||||
}
|
||||
|
||||
|
@@ -141,6 +141,7 @@ class ExternalEditWatcher {
|
||||
// Hack to support external watcher on some linux applications (gedit, gvim, etc)
|
||||
// taken from https://github.com/paulmillr/chokidar/issues/591
|
||||
this.watcher_.on('raw', async (event, path, { watchedPath }) => {
|
||||
/* was_debug */ this.logger().info(`ExternalEditWatcher: Raw event: ${event}: ${watchedPath}`);
|
||||
if (event === 'rename') {
|
||||
this.watcher_.unwatch(watchedPath);
|
||||
this.watcher_.add(watchedPath);
|
||||
|
@@ -276,7 +276,9 @@ class InteropService {
|
||||
|
||||
const exportedNoteIds = [];
|
||||
let resourceIds = [];
|
||||
const folderIds = await Folder.allIds();
|
||||
|
||||
// Recursively get all the folders that have valid parents
|
||||
const folderIds = await Folder.childrenIds('', true);
|
||||
|
||||
let fullSourceFolderIds = sourceFolderIds.slice();
|
||||
for (let i = 0; i < sourceFolderIds.length; i++) {
|
||||
@@ -327,7 +329,7 @@ class InteropService {
|
||||
await queueExportItem(BaseModel.TYPE_TAG, exportedTagIds[i]);
|
||||
}
|
||||
|
||||
const exporter = this.newModuleFromPath_('exporter', options);// this.newModuleByFormat_('exporter', exportFormat);
|
||||
const exporter = this.newModuleFromPath_('exporter', options);
|
||||
await exporter.init(exportPath, options);
|
||||
|
||||
const typeOrder = [BaseModel.TYPE_FOLDER, BaseModel.TYPE_RESOURCE, BaseModel.TYPE_NOTE, BaseModel.TYPE_TAG, BaseModel.TYPE_NOTE_TAG];
|
||||
|
@@ -1,6 +1,9 @@
|
||||
const fs = require('fs-extra');
|
||||
import { KeyboardEvent } from 'react';
|
||||
|
||||
const BaseService = require('lib/services/BaseService');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const eventManager = require('lib/eventManager');
|
||||
const { shim } = require('lib/shim');
|
||||
const { _ } = require('lib/locale');
|
||||
|
||||
const keysRegExp = /^([0-9A-Z)!@#$%^&*(:+<_>?~{|}";=,\-./`[\\\]']|F1*[1-9]|F10|F2[0-4]|Plus|Space|Tab|Backspace|Delete|Insert|Return|Enter|Up|Down|Left|Right|Home|End|PageUp|PageDown|Escape|Esc|VolumeUp|VolumeDown|VolumeMute|MediaNextTrack|MediaPreviousTrack|MediaStop|MediaPlayPause|PrintScreen)$/;
|
||||
const modifiersRegExp = {
|
||||
@@ -8,7 +11,7 @@ const modifiersRegExp = {
|
||||
default: /^(Ctrl|Alt|AltGr|Shift|Super)$/,
|
||||
};
|
||||
|
||||
const defaultKeymap = {
|
||||
const defaultKeymapItems = {
|
||||
darwin: [
|
||||
{ accelerator: 'Cmd+N', command: 'newNote' },
|
||||
{ accelerator: 'Cmd+T', command: 'newTodo' },
|
||||
@@ -86,135 +89,192 @@ interface Keymap {
|
||||
|
||||
export default class KeymapService extends BaseService {
|
||||
private keymap: Keymap;
|
||||
private defaultKeymap: KeymapItem[];
|
||||
private platform: string;
|
||||
private customKeymapPath: string;
|
||||
private defaultKeymapItems: KeymapItem[];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Automatically initialized for the current platform
|
||||
// By default, initialize for the current platform
|
||||
// Manual initialization allows testing for other platforms
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize(platform: string = shim.platformName()) {
|
||||
this.keysRegExp = keysRegExp;
|
||||
this.platform = platform;
|
||||
|
||||
switch (platform) {
|
||||
case 'darwin':
|
||||
this.defaultKeymap = defaultKeymap.darwin;
|
||||
this.defaultKeymapItems = defaultKeymapItems.darwin;
|
||||
this.modifiersRegExp = modifiersRegExp.darwin;
|
||||
break;
|
||||
default:
|
||||
this.defaultKeymap = defaultKeymap.default;
|
||||
this.defaultKeymapItems = defaultKeymapItems.default;
|
||||
this.modifiersRegExp = modifiersRegExp.default;
|
||||
}
|
||||
|
||||
this.keymap = {};
|
||||
for (let i = 0; i < this.defaultKeymap.length; i++) {
|
||||
// Make a copy of the KeymapItem before assigning it
|
||||
// Otherwise we're going to mess up the defaultKeymap array
|
||||
this.keymap[this.defaultKeymap[i].command] = { ...this.defaultKeymap[i] };
|
||||
for (let i = 0; i < this.defaultKeymapItems.length; i++) {
|
||||
// Keep the original defaultKeymapItems array untouched
|
||||
// Makes it possible to retrieve the original accelerator later, if needed
|
||||
this.keymap[this.defaultKeymapItems[i].command] = { ...this.defaultKeymapItems[i] };
|
||||
}
|
||||
}
|
||||
|
||||
async loadKeymap(keymapPath: string) {
|
||||
this.keymapPath = keymapPath; // Used for saving changes later..
|
||||
async loadCustomKeymap(customKeymapPath: string) {
|
||||
this.customKeymapPath = customKeymapPath; // Useful for saving the changes later
|
||||
|
||||
if (await fs.exists(keymapPath)) {
|
||||
this.logger().info(`Loading keymap: ${keymapPath}`);
|
||||
if (await shim.fsDriver().exists(customKeymapPath)) {
|
||||
this.logger().info(`KeymapService: Loading keymap from file: ${customKeymapPath}`);
|
||||
|
||||
try {
|
||||
const keymapFile = await fs.readFile(keymapPath, 'utf-8');
|
||||
this.setKeymap(JSON.parse(keymapFile));
|
||||
const customKeymapFile = await shim.fsDriver().readFile(customKeymapPath, 'utf-8');
|
||||
// Custom keymaps are supposed to contain an array of keymap items
|
||||
this.overrideKeymap(JSON.parse(customKeymapFile));
|
||||
} catch (err) {
|
||||
const msg = err.message ? err.message : '';
|
||||
throw new Error(`Failed to load keymap: ${keymapPath}\n${msg}`);
|
||||
const message = err.message || '';
|
||||
throw new Error(`${_('Error loading the keymap from file: %s', customKeymapPath)}\n${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasAccelerator(command: string) {
|
||||
return !!this.keymap[command];
|
||||
async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) {
|
||||
this.logger().info(`KeymapService: Saving keymap to file: ${customKeymapPath}`);
|
||||
|
||||
try {
|
||||
// Only the customized keymap items should be saved to the disk
|
||||
const customKeymapItems = this.getCustomKeymapItems();
|
||||
await shim.fsDriver().writeFile(customKeymapPath, JSON.stringify(customKeymapItems, null, 2), 'utf-8');
|
||||
|
||||
// Refresh the menu items so that the changes are reflected
|
||||
eventManager.emit('keymapChange');
|
||||
} catch (err) {
|
||||
const message = err.message || '';
|
||||
throw new Error(`${_('Error saving the keymap to file: %s', customKeymapPath)}\n${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
getAccelerator(command: string) {
|
||||
const item = this.keymap[command];
|
||||
|
||||
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
else return item.accelerator;
|
||||
acceleratorExists(command: string) {
|
||||
return !!this.keymap[command];
|
||||
}
|
||||
|
||||
setAccelerator(command: string, accelerator: string) {
|
||||
this.keymap[command].accelerator = accelerator;
|
||||
}
|
||||
|
||||
resetAccelerator(command: string) {
|
||||
const defaultItem = this.defaultKeymap.find((item => item.command === command));
|
||||
getAccelerator(command: string) {
|
||||
const item = this.keymap[command];
|
||||
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
else this.setAccelerator(command, defaultItem.accelerator);
|
||||
return item.accelerator;
|
||||
}
|
||||
|
||||
setKeymap(customKeymap: KeymapItem[]) {
|
||||
for (let i = 0; i < customKeymap.length; i++) {
|
||||
const item = customKeymap[i];
|
||||
getDefaultAccelerator(command: string) {
|
||||
const defaultItem = this.defaultKeymapItems.find((item => item.command === command));
|
||||
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
||||
|
||||
try {
|
||||
this.validateKeymapItem(item); // Throws if there are any issues in the keymap item
|
||||
this.setAccelerator(item.command, item.accelerator);
|
||||
} catch (err) {
|
||||
throw new Error(`Keymap item ${JSON.stringify(item)} is invalid: ${err.message}`);
|
||||
return defaultItem.accelerator;
|
||||
}
|
||||
|
||||
getCommandNames() {
|
||||
return Object.keys(this.keymap);
|
||||
}
|
||||
|
||||
getKeymapItems() {
|
||||
return Object.values(this.keymap);
|
||||
}
|
||||
|
||||
getCustomKeymapItems() {
|
||||
const customkeymapItems: KeymapItem[] = [];
|
||||
this.defaultKeymapItems.forEach(({ command, accelerator }) => {
|
||||
const currentAccelerator = this.getAccelerator(command);
|
||||
|
||||
// Only the customized/changed keymap items are neccessary for the custom keymap
|
||||
// Customizations can be merged with the original keymap at the runtime
|
||||
if (this.getAccelerator(command) !== accelerator) {
|
||||
customkeymapItems.push({ command, accelerator: currentAccelerator });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return customkeymapItems;
|
||||
}
|
||||
|
||||
getDefaultKeymapItems() {
|
||||
return [...this.defaultKeymapItems];
|
||||
}
|
||||
|
||||
overrideKeymap(customKeymapItems: KeymapItem[]) {
|
||||
try {
|
||||
this.validateKeymap(); // Throws whenever there are duplicate Accelerators used in the keymap
|
||||
for (let i = 0; i < customKeymapItems.length; i++) {
|
||||
const item = customKeymapItems[i];
|
||||
// Validate individual custom keymap items
|
||||
// Throws if there are any issues in the keymap item
|
||||
this.validateKeymapItem(item);
|
||||
this.setAccelerator(item.command, item.accelerator);
|
||||
}
|
||||
|
||||
// Validate the entire keymap for duplicates
|
||||
// Throws whenever there are duplicate Accelerators used in the keymap
|
||||
this.validateKeymap();
|
||||
} catch (err) {
|
||||
this.initialize();
|
||||
throw new Error(`Keymap configuration contains duplicates\n${err.message}`);
|
||||
this.initialize(); // Discard all the changes if there are any issues
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private validateKeymapItem(item: KeymapItem) {
|
||||
if (!item.hasOwnProperty('command')) {
|
||||
throw new Error('"command" property is missing');
|
||||
throw new Error(_('Keymap item %s is missing the required "command" property.', JSON.stringify(item)));
|
||||
} else if (!this.keymap.hasOwnProperty(item.command)) {
|
||||
throw new Error(`"${item.command}" is not a valid command`);
|
||||
throw new Error(_('Keymap item %s is invalid because %s is not a valid command.', JSON.stringify(item), item.command));
|
||||
}
|
||||
|
||||
if (!item.hasOwnProperty('accelerator')) {
|
||||
throw new Error('"accelerator" property is missing');
|
||||
throw new Error(_('Keymap item %s is missing the required "accelerator" property.', JSON.stringify(item)));
|
||||
} else if (item.accelerator !== null) {
|
||||
try {
|
||||
this.validateAccelerator(item.accelerator);
|
||||
} catch (err) {
|
||||
throw new Error(`"${item.accelerator}" is not a valid accelerator`);
|
||||
} catch {
|
||||
throw new Error(_('Keymap item %s is invalid because %s is not a valid accelerator.', JSON.stringify(item), item.accelerator));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateKeymap() {
|
||||
validateKeymap(proposedKeymapItem: KeymapItem = null) {
|
||||
const usedAccelerators = new Set();
|
||||
|
||||
// Validate as if the proposed change is already present in the current keymap
|
||||
// Helpful for detecting any errors that'll occur, when the proposed change is performed on the keymap
|
||||
if (proposedKeymapItem) usedAccelerators.add(proposedKeymapItem.accelerator);
|
||||
|
||||
for (const item of Object.values(this.keymap)) {
|
||||
const itemAccelerator = item.accelerator;
|
||||
const [itemAccelerator, itemCommand] = [item.accelerator, item.command];
|
||||
if (proposedKeymapItem && itemCommand === proposedKeymapItem.command) continue; // Ignore the original accelerator
|
||||
|
||||
if (usedAccelerators.has(itemAccelerator)) {
|
||||
const originalItem = Object.values(this.keymap).find(_item => _item.accelerator == item.accelerator);
|
||||
throw new Error(
|
||||
`Accelerator "${itemAccelerator}" can't be used for both "${item.command}" and "${originalItem.command}" commands\n` +
|
||||
'You have to change the accelerator for any of above commands'
|
||||
);
|
||||
} else if (itemAccelerator !== null) {
|
||||
const originalItem = (proposedKeymapItem && proposedKeymapItem.accelerator === itemAccelerator)
|
||||
? proposedKeymapItem
|
||||
: Object.values(this.keymap).find(_item => _item.accelerator == itemAccelerator);
|
||||
|
||||
throw new Error(_(
|
||||
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
|
||||
itemAccelerator,
|
||||
originalItem.command,
|
||||
itemCommand
|
||||
));
|
||||
} else if (itemAccelerator) {
|
||||
usedAccelerators.add(itemAccelerator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private validateAccelerator(accelerator: string) {
|
||||
validateAccelerator(accelerator: string) {
|
||||
let keyFound = false;
|
||||
|
||||
const parts = accelerator.split('+');
|
||||
const isValid = parts.every((part, index) => {
|
||||
const isKey = this.keysRegExp.test(part);
|
||||
const isKey = keysRegExp.test(part);
|
||||
const isModifier = this.modifiersRegExp.test(part);
|
||||
|
||||
if (isKey) {
|
||||
@@ -228,7 +288,69 @@ export default class KeymapService extends BaseService {
|
||||
return isKey || isModifier;
|
||||
});
|
||||
|
||||
if (!isValid) throw new Error(`Accelerator invalid: ${accelerator}`);
|
||||
if (!isValid) throw new Error(_('Accelerator "%s" is not valid.', accelerator));
|
||||
}
|
||||
|
||||
domToElectronAccelerator(event: KeyboardEvent<HTMLDivElement>) {
|
||||
const parts = [];
|
||||
const { key, ctrlKey, metaKey, altKey, shiftKey } = event;
|
||||
|
||||
// First, the modifiers
|
||||
if (ctrlKey) parts.push('Ctrl');
|
||||
switch (this.platform) {
|
||||
case 'darwin':
|
||||
if (altKey) parts.push('Option');
|
||||
if (shiftKey) parts.push('Shift');
|
||||
if (metaKey) parts.push('Cmd');
|
||||
break;
|
||||
default:
|
||||
if (altKey) parts.push('Alt');
|
||||
if (shiftKey) parts.push('Shift');
|
||||
}
|
||||
|
||||
// Finally, the key
|
||||
const electronKey = KeymapService.domToElectronKey(key);
|
||||
if (electronKey) parts.push(electronKey);
|
||||
|
||||
return parts.join('+');
|
||||
}
|
||||
|
||||
static domToElectronKey(domKey: string) {
|
||||
let electronKey;
|
||||
|
||||
if (/^([a-z])$/.test(domKey)) {
|
||||
electronKey = domKey.toUpperCase();
|
||||
} else if (/^Arrow(Up|Down|Left|Right)|Audio(VolumeUp|VolumeDown|VolumeMute)$/.test(domKey)) {
|
||||
electronKey = domKey.slice(5);
|
||||
} else {
|
||||
switch (domKey) {
|
||||
case ' ':
|
||||
electronKey = 'Space';
|
||||
break;
|
||||
case '+':
|
||||
electronKey = 'Plus';
|
||||
break;
|
||||
case 'MediaTrackNext':
|
||||
electronKey = 'MediaNextTrack';
|
||||
break;
|
||||
case 'MediaTrackPrevious':
|
||||
electronKey = 'MediaPreviousTrack';
|
||||
break;
|
||||
default:
|
||||
electronKey = domKey;
|
||||
}
|
||||
}
|
||||
|
||||
if (keysRegExp.test(electronKey)) return electronKey;
|
||||
else return null;
|
||||
}
|
||||
|
||||
public on(eventName: string, callback: Function) {
|
||||
eventManager.on(eventName, callback);
|
||||
}
|
||||
|
||||
public off(eventName: string, callback: Function) {
|
||||
eventManager.off(eventName, callback);
|
||||
}
|
||||
|
||||
static instance() {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const KeymapService = require('lib/services/KeymapService').default;
|
||||
|
||||
class PluginManager {
|
||||
constructor() {
|
||||
@@ -80,6 +81,8 @@ class PluginManager {
|
||||
|
||||
menuItems() {
|
||||
let output = [];
|
||||
const keymapService = KeymapService.instance();
|
||||
|
||||
for (const name in this.plugins_) {
|
||||
const menuItems = this.plugins_[name].Class.manifest.menuItems;
|
||||
if (!menuItems) continue;
|
||||
@@ -91,10 +94,7 @@ class PluginManager {
|
||||
itemName: item.name,
|
||||
});
|
||||
};
|
||||
|
||||
if (item.accelerator instanceof Function) {
|
||||
item.accelerator = item.accelerator();
|
||||
}
|
||||
item.accelerator = keymapService.getAccelerator(name);
|
||||
}
|
||||
|
||||
output = output.concat(menuItems);
|
||||
|
@@ -17,7 +17,7 @@ const { shim } = require('lib/shim.js');
|
||||
|
||||
let keytar:any;
|
||||
try {
|
||||
keytar = shim.isLinux() || shim.isPortable() ? null : require('keytar');
|
||||
keytar = (shim.isWindows() || shim.isMac()) && !shim.isPortable() ? require('keytar') : null;
|
||||
} catch (error) {
|
||||
console.error('Cannot load keytar - keychain support will be disabled', error);
|
||||
keytar = null;
|
||||
|
@@ -81,6 +81,11 @@ class SearchEngine {
|
||||
);
|
||||
}
|
||||
|
||||
if (!noteIds.length && (Setting.value('db.fuzzySearchEnabled') == 1)) {
|
||||
// On the last loop
|
||||
queries.push({ sql: 'INSERT INTO notes_spellfix(word,rank) SELECT term, documents FROM search_aux WHERE col=\'*\'' });
|
||||
}
|
||||
|
||||
await this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
@@ -145,7 +150,16 @@ class SearchEngine {
|
||||
[BaseModel.TYPE_NOTE, lastChangeId]
|
||||
);
|
||||
|
||||
if (!changes.length) break;
|
||||
const queries = [];
|
||||
|
||||
if (!changes.length) {
|
||||
if (Setting.value('db.fuzzySearchEnabled') === 1) {
|
||||
queries.push({ sql: 'DELETE FROM notes_spellfix' });
|
||||
queries.push({ sql: 'INSERT INTO notes_spellfix(word,rank) SELECT term, documents FROM search_aux WHERE col=\'*\'' });
|
||||
await this.db().transactionExecBatch(queries);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const noteIds = changes.map(a => a.item_id);
|
||||
const notes = await Note.modelSelectAll(`
|
||||
@@ -153,8 +167,6 @@ class SearchEngine {
|
||||
FROM notes WHERE id IN ("${noteIds.join('","')}") AND is_conflict = 0 AND encryption_applied = 0`
|
||||
);
|
||||
|
||||
const queries = [];
|
||||
|
||||
for (let i = 0; i < changes.length; i++) {
|
||||
const change = changes[i];
|
||||
|
||||
@@ -254,7 +266,7 @@ class SearchEngine {
|
||||
|
||||
|
||||
|
||||
calculateWeightBM25_(rows) {
|
||||
calculateWeightBM25_(rows, fuzzyScore) {
|
||||
// https://www.sqlite.org/fts3.html#matchinfo
|
||||
// pcnalx are the arguments passed to matchinfo
|
||||
// p - The number of matchable phrases in the query.
|
||||
@@ -318,14 +330,20 @@ class SearchEngine {
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
row.weight = 0;
|
||||
row.fuzziness = 1000;
|
||||
row.wordFound = [];
|
||||
for (let j = 0; j < numPhrases; j++) {
|
||||
let found = false;
|
||||
columns.forEach(column => {
|
||||
const rowsWithHits = docsWithHits(X[i], column, j);
|
||||
const frequencyHits = hitsThisRow(X[i], column, j);
|
||||
|
||||
const idf = IDF(rowsWithHits, numRows);
|
||||
found = found ? found : (frequencyHits > 0);
|
||||
|
||||
row.weight += BM25(idf, frequencyHits, numTokens[column][i], avgTokens[column]);
|
||||
row.fuzziness = (frequencyHits > 0) ? Math.min(row.fuzziness, fuzzyScore[j]) : row.fuzziness;
|
||||
});
|
||||
row.wordFound.push(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,23 +363,40 @@ class SearchEngine {
|
||||
|
||||
row.fields = Object.keys(matchedFields).filter(key => matchedFields[key]);
|
||||
row.weight = 0;
|
||||
row.fuzziness = 0;
|
||||
}
|
||||
}
|
||||
|
||||
processResults_(rows, parsedQuery, isBasicSearchResults = false) {
|
||||
const rowContainsAllWords = (wordsFound, numFuzzyMatches) => {
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
for (let i = 0; i < numFuzzyMatches.length; i++) {
|
||||
end = end + numFuzzyMatches[i];
|
||||
if (!(wordsFound.slice(start, end).find(x => x))) {
|
||||
// This note doesn't contain any fuzzy matches for the word
|
||||
return false;
|
||||
}
|
||||
start = end;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (isBasicSearchResults) {
|
||||
this.processBasicSearchResults_(rows, parsedQuery);
|
||||
} else {
|
||||
this.calculateWeightBM25_(rows);
|
||||
this.calculateWeightBM25_(rows, parsedQuery.fuzzyScore);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
row.include = (parsedQuery.fuzzy && !parsedQuery.any) ? rowContainsAllWords(row.wordFound, parsedQuery.numFuzzyMatches) : true;
|
||||
const offsets = row.offsets.split(' ').map(o => Number(o));
|
||||
row.fields = this.fieldNamesFromOffsets_(offsets);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
rows.sort((a, b) => {
|
||||
if (a.fuzziness < b.fuzziness) return -1;
|
||||
if (a.fuzziness > b.fuzziness) return +1;
|
||||
if (a.fields.includes('title') && !b.fields.includes('title')) return -1;
|
||||
if (!a.fields.includes('title') && b.fields.includes('title')) return +1;
|
||||
if (a.weight < b.weight) return +1;
|
||||
@@ -389,21 +424,75 @@ class SearchEngine {
|
||||
return regexString;
|
||||
}
|
||||
|
||||
parseQuery(query) {
|
||||
async fuzzifier(words) {
|
||||
const fuzzyMatches = [];
|
||||
words.forEach(word => {
|
||||
const fuzzyWords = this.db().selectAll('SELECT word, score FROM notes_spellfix WHERE word MATCH ? AND top=3', [word]);
|
||||
fuzzyMatches.push(fuzzyWords);
|
||||
});
|
||||
return await Promise.all(fuzzyMatches);
|
||||
}
|
||||
|
||||
async parseQuery(query, fuzzy = false) {
|
||||
// fuzzy = false;
|
||||
const trimQuotes = (str) => str.startsWith('"') ? str.substr(1, str.length - 2) : str;
|
||||
|
||||
let allTerms = [];
|
||||
let allFuzzyTerms = [];
|
||||
|
||||
try {
|
||||
allTerms = filterParser(query);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
const textTerms = allTerms.filter(x => x.name === 'text').map(x => trimQuotes(x.value));
|
||||
const titleTerms = allTerms.filter(x => x.name === 'title').map(x => trimQuotes(x.value));
|
||||
const bodyTerms = allTerms.filter(x => x.name === 'body').map(x => trimQuotes(x.value));
|
||||
const textTerms = allTerms.filter(x => x.name === 'text' && !x.negated);
|
||||
const titleTerms = allTerms.filter(x => x.name === 'title' && !x.negated);
|
||||
const bodyTerms = allTerms.filter(x => x.name === 'body' && !x.negated);
|
||||
|
||||
const terms = { _: textTerms, 'title': titleTerms, 'body': bodyTerms };
|
||||
const fuzzyScore = [];
|
||||
let numFuzzyMatches = [];
|
||||
let terms = null;
|
||||
if (fuzzy) {
|
||||
const fuzzyText = await this.fuzzifier(textTerms.filter(x => !x.quoted).map(x => trimQuotes(x.value)));
|
||||
const fuzzyTitle = await this.fuzzifier(titleTerms.map(x => trimQuotes(x.value)));
|
||||
const fuzzyBody = await this.fuzzifier(bodyTerms.map(x => trimQuotes(x.value)));
|
||||
const phraseSearches = textTerms.filter(x => x.quoted).map(x => x.value);
|
||||
|
||||
// Save number of matches we got for each word
|
||||
// fuzzifier() is currently set to return at most 3 matches)
|
||||
// We need to know which fuzzy words go together so that we can filter out notes that don't contain a required word.
|
||||
numFuzzyMatches = fuzzyText.concat(fuzzyTitle).concat(fuzzyBody).map(x => x.length);
|
||||
for (let i = 0; i < phraseSearches.length; i++) {
|
||||
numFuzzyMatches.push(1); // Phrase searches are preserved without fuzzification
|
||||
}
|
||||
|
||||
const mergedFuzzyText = [].concat.apply([], fuzzyText);
|
||||
const mergedFuzzyTitle = [].concat.apply([], fuzzyTitle);
|
||||
const mergedFuzzyBody = [].concat.apply([], fuzzyBody);
|
||||
|
||||
const fuzzyTextTerms = mergedFuzzyText.map(x => { return { name: 'text', value: x.word, negated: false, score: x.score }; });
|
||||
const fuzzyTitleTerms = mergedFuzzyTitle.map(x => { return { name: 'title', value: x.word, negated: false, score: x.score }; });
|
||||
const fuzzyBodyTerms = mergedFuzzyBody.map(x => { return { name: 'body', value: x.word, negated: false, score: x.score }; });
|
||||
const phraseTextTerms = phraseSearches.map(x => { return { name: 'text', value: x, negated: false, score: 0 }; });
|
||||
|
||||
allTerms = allTerms.filter(x => (x.name !== 'text' && x.name !== 'title' && x.name !== 'body'));
|
||||
|
||||
allFuzzyTerms = allTerms.concat(fuzzyTextTerms).concat(fuzzyTitleTerms).concat(fuzzyBodyTerms).concat(phraseTextTerms);
|
||||
|
||||
const allTextTerms = allFuzzyTerms.filter(x => x.name === 'title' || x.name === 'body' || x.name === 'text');
|
||||
for (let i = 0; i < allTextTerms.length; i++) {
|
||||
fuzzyScore.push(allFuzzyTerms[i].score ? allFuzzyTerms[i].score : 0);
|
||||
}
|
||||
|
||||
terms = { _: fuzzyTextTerms.concat(phraseTextTerms).map(x =>trimQuotes(x.value)), 'title': fuzzyTitleTerms.map(x =>trimQuotes(x.value)), 'body': fuzzyBodyTerms.map(x =>trimQuotes(x.value)) };
|
||||
} else {
|
||||
const nonNegatedTextTerms = textTerms.length + titleTerms.length + bodyTerms.length;
|
||||
for (let i = 0; i < nonNegatedTextTerms; i++) {
|
||||
fuzzyScore.push(0);
|
||||
}
|
||||
terms = { _: textTerms.map(x =>trimQuotes(x.value)), 'title': titleTerms.map(x =>trimQuotes(x.value)), 'body': bodyTerms.map(x =>trimQuotes(x.value)) };
|
||||
}
|
||||
|
||||
// Filter terms:
|
||||
// - Convert wildcards to regex
|
||||
@@ -445,7 +534,11 @@ class SearchEngine {
|
||||
termCount: termCount,
|
||||
keys: keys,
|
||||
terms: terms, // text terms
|
||||
allTerms: allTerms,
|
||||
allTerms: fuzzy ? allFuzzyTerms : allTerms,
|
||||
fuzzyScore: fuzzyScore,
|
||||
numFuzzyMatches: numFuzzyMatches,
|
||||
fuzzy: fuzzy,
|
||||
any: !!allTerms.find(term => term.name === 'any'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -474,7 +567,7 @@ class SearchEngine {
|
||||
|
||||
async basicSearch(query) {
|
||||
query = query.replace(/\*/, '');
|
||||
const parsedQuery = this.parseQuery(query);
|
||||
const parsedQuery = await this.parseQuery(query);
|
||||
const searchOptions = {};
|
||||
|
||||
for (const key of parsedQuery.keys) {
|
||||
@@ -489,8 +582,8 @@ class SearchEngine {
|
||||
return Note.previews(null, searchOptions);
|
||||
}
|
||||
|
||||
determineSearchType_(query, preferredSearchType) {
|
||||
if (preferredSearchType === SearchEngine.SEARCH_TYPE_BASIC) return SearchEngine.SEARCH_TYPE_BASIC;
|
||||
determineSearchType_(query, options) {
|
||||
if (options.searchType === SearchEngine.SEARCH_TYPE_BASIC) return SearchEngine.SEARCH_TYPE_BASIC;
|
||||
|
||||
// If preferredSearchType is "fts" we auto-detect anyway
|
||||
// because it's not always supported.
|
||||
@@ -499,9 +592,12 @@ class SearchEngine {
|
||||
|
||||
if (!Setting.value('db.ftsEnabled') || ['ja', 'zh', 'ko', 'th'].indexOf(st) >= 0) {
|
||||
return SearchEngine.SEARCH_TYPE_BASIC;
|
||||
} else if ((Setting.value('db.fuzzySearchEnabled') === 1) && options.fuzzy) {
|
||||
return SearchEngine.SEARCH_TYPE_FTS_FUZZY;
|
||||
} else {
|
||||
return SearchEngine.SEARCH_TYPE_FTS;
|
||||
}
|
||||
|
||||
return SearchEngine.SEARCH_TYPE_FTS;
|
||||
}
|
||||
|
||||
async search(searchString, options = null) {
|
||||
@@ -511,28 +607,31 @@ class SearchEngine {
|
||||
|
||||
searchString = this.normalizeText_(searchString);
|
||||
|
||||
const searchType = this.determineSearchType_(searchString, options.searchType);
|
||||
const searchType = this.determineSearchType_(searchString, options);
|
||||
|
||||
if (searchType === SearchEngine.SEARCH_TYPE_BASIC) {
|
||||
// Non-alphabetical languages aren't support by SQLite FTS (except with extensions which are not available in all platforms)
|
||||
const rows = await this.basicSearch(searchString);
|
||||
const parsedQuery = this.parseQuery(searchString);
|
||||
const parsedQuery = await this.parseQuery(searchString);
|
||||
this.processResults_(rows, parsedQuery, true);
|
||||
return rows;
|
||||
} else {
|
||||
// SEARCH_TYPE_FTS
|
||||
// SEARCH_TYPE_FTS or SEARCH_TYPE_FTS_FUZZY
|
||||
// FTS will ignore all special characters, like "-" in the index. So if
|
||||
// we search for "this-phrase" it won't find it because it will only
|
||||
// see "this phrase" in the index. Because of this, we remove the dashes
|
||||
// when searching.
|
||||
// https://github.com/laurent22/joplin/issues/1075#issuecomment-459258856
|
||||
|
||||
const parsedQuery = this.parseQuery(searchString);
|
||||
const parsedQuery = await this.parseQuery(searchString, options.fuzzy);
|
||||
|
||||
try {
|
||||
const { query, params } = queryBuilder(parsedQuery.allTerms);
|
||||
const { query, params } = (searchType === SearchEngine.SEARCH_TYPE_FTS_FUZZY) ? queryBuilder(parsedQuery.allTerms, true) : queryBuilder(parsedQuery.allTerms, false);
|
||||
const rows = await this.db().selectAll(query, params);
|
||||
this.processResults_(rows, parsedQuery);
|
||||
if (searchType === SearchEngine.SEARCH_TYPE_FTS_FUZZY && !parsedQuery.any) {
|
||||
return rows.filter(row => row.include);
|
||||
}
|
||||
return rows;
|
||||
} catch (error) {
|
||||
this.logger().warn(`Cannot execute MATCH query: ${searchString}: ${error.message}`);
|
||||
@@ -567,5 +666,6 @@ SearchEngine.instance_ = null;
|
||||
SearchEngine.SEARCH_TYPE_AUTO = 'auto';
|
||||
SearchEngine.SEARCH_TYPE_BASIC = 'basic';
|
||||
SearchEngine.SEARCH_TYPE_FTS = 'fts';
|
||||
SearchEngine.SEARCH_TYPE_FTS_FUZZY = 'fts_fuzzy';
|
||||
|
||||
module.exports = SearchEngine;
|
||||
|
@@ -11,7 +11,7 @@ class SearchEngineUtils {
|
||||
searchType = SearchEngine.SEARCH_TYPE_BASIC;
|
||||
}
|
||||
|
||||
const results = await SearchEngine.instance().search(query, { searchType });
|
||||
const results = await SearchEngine.instance().search(query, { searchType, fuzzy: options.fuzzy });
|
||||
const noteIds = results.map(n => n.id);
|
||||
|
||||
// We need at least the note ID to be able to sort them below so if not
|
||||
|
@@ -3,6 +3,7 @@ interface Term {
|
||||
name: string
|
||||
value: string
|
||||
negated: boolean
|
||||
quoted?: boolean
|
||||
}
|
||||
|
||||
const makeTerm = (name: string, value: string): Term => {
|
||||
@@ -10,10 +11,9 @@ const makeTerm = (name: string, value: string): Term => {
|
||||
return { name: name, value: value, negated: false };
|
||||
};
|
||||
|
||||
const quoted = (s: string) => s.startsWith('"') && s.endsWith('"');
|
||||
|
||||
const quote = (s : string) => {
|
||||
const quoted = (s: string) => s.startsWith('"') && s.endsWith('"');
|
||||
|
||||
if (!quoted(s)) {
|
||||
return `"${s}"`;
|
||||
}
|
||||
@@ -71,7 +71,6 @@ const parseQuery = (query: string): Term[] => {
|
||||
'altitude', 'resource', 'sourceurl']);
|
||||
|
||||
const terms = getTerms(query);
|
||||
// console.log(terms);
|
||||
|
||||
const result: Term[] = [];
|
||||
for (let i = 0; i < terms.length; i++) {
|
||||
@@ -98,9 +97,9 @@ const parseQuery = (query: string): Term[] => {
|
||||
// Every word is quoted if not already.
|
||||
// By quoting the word, FTS match query will take care of removing dashes and other word seperators.
|
||||
if (value.startsWith('-')) {
|
||||
result.push({ name: 'text', value: quote(value.slice(1)) , negated: true });
|
||||
result.push({ name: 'text', value: quote(value.slice(1)) , negated: true, quoted: quoted(value) });
|
||||
} else {
|
||||
result.push({ name: 'text', value: quote(value), negated: false });
|
||||
result.push({ name: 'text', value: quote(value), negated: false, quoted: quoted(value) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -300,7 +300,7 @@ const sourceUrlFilter = (terms: Term[], conditons: string[], params: string[], r
|
||||
};
|
||||
|
||||
|
||||
const textFilter = (terms: Term[], conditions: string[], params: string[], relation: Relation) => {
|
||||
const textFilter = (terms: Term[], conditions: string[], params: string[], relation: Relation, fuzzy: Boolean) => {
|
||||
const addExcludeTextConditions = (excludedTerms: Term[], conditions:string[], params: string[], relation: Relation) => {
|
||||
const type = excludedTerms[0].name === 'text' ? '' : `.${excludedTerms[0].name}`;
|
||||
|
||||
@@ -342,7 +342,7 @@ const textFilter = (terms: Term[], conditions: string[], params: string[], relat
|
||||
if (term.name === 'text') return term.value;
|
||||
else return `${term.name}:${term.value}`;
|
||||
});
|
||||
const matchQuery = (relation === 'OR') ? termsToMatch.join(' OR ') : termsToMatch.join(' ');
|
||||
const matchQuery = (fuzzy || (relation === 'OR')) ? termsToMatch.join(' OR ') : termsToMatch.join(' ');
|
||||
params.push(matchQuery);
|
||||
}
|
||||
|
||||
@@ -374,14 +374,11 @@ const getConnective = (terms: Term[], relation: Relation): string => {
|
||||
return (!notebookTerm && (relation === 'OR')) ? 'ROWID=-1' : '1'; // ROWID=-1 acts as 0 (something always false)
|
||||
};
|
||||
|
||||
export default function queryBuilder(terms: Term[]) {
|
||||
export default function queryBuilder(terms: Term[], fuzzy: boolean) {
|
||||
const queryParts: string[] = [];
|
||||
const params: string[] = [];
|
||||
const withs: string[] = [];
|
||||
|
||||
// console.log("testing beep beep boop boop")
|
||||
// console.log(terms);
|
||||
|
||||
const relation: Relation = getDefaultRelation(terms);
|
||||
|
||||
queryParts.push(`
|
||||
@@ -405,8 +402,8 @@ export default function queryBuilder(terms: Term[]) {
|
||||
|
||||
resourceFilter(terms, queryParts, params, relation, withs);
|
||||
|
||||
textFilter(terms, queryParts, params, relation, fuzzy);
|
||||
|
||||
textFilter(terms, queryParts, params, relation);
|
||||
|
||||
typeFilter(terms, queryParts, params, relation);
|
||||
|
||||
|
BIN
ReactNativeClient/lib/sql-extensions/spellfix.dll
Normal file
BIN
ReactNativeClient/lib/sql-extensions/spellfix.dll
Normal file
Binary file not shown.
@@ -28,7 +28,6 @@ const aritimStyle = {
|
||||
codeBorderColor: '#141a21', // Single line code border, and tables
|
||||
codeColor: '#005b47', // Single line code text
|
||||
|
||||
aceEditorTheme: 'chaos',
|
||||
codeMirrorTheme: 'monokai',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
|
||||
|
@@ -28,7 +28,6 @@ const darkStyle = {
|
||||
codeBackgroundColor: 'rgb(47, 48, 49)',
|
||||
codeBorderColor: 'rgb(70, 70, 70)',
|
||||
|
||||
aceEditorTheme: 'twilight',
|
||||
codeMirrorTheme: 'material-darker',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
|
||||
|
@@ -28,7 +28,6 @@ const draculaStyle = {
|
||||
codeBorderColor: '#f8f8f2',
|
||||
codeColor: '#50fa7b',
|
||||
|
||||
aceEditorTheme: 'dracula',
|
||||
codeMirrorTheme: 'dracula',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@@ -31,7 +31,6 @@ const lightStyle = {
|
||||
codeBorderColor: 'rgb(220, 220, 220)',
|
||||
codeColor: 'rgb(0,0,0)',
|
||||
|
||||
aceEditorTheme: 'chrome',
|
||||
codeMirrorTheme: 'default',
|
||||
codeThemeCss: 'atom-one-light.css',
|
||||
};
|
||||
|
@@ -74,7 +74,6 @@ const nordStyle = {
|
||||
codeBorderColor: nord[2],
|
||||
codeColor: nord[13],
|
||||
|
||||
aceEditorTheme: 'terminal',
|
||||
codeMirrorTheme: 'nord',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@@ -28,7 +28,6 @@ const solarizedDarkStyle = {
|
||||
codeBorderColor: '#696969',
|
||||
codeColor: '#fdf6e3',
|
||||
|
||||
aceEditorTheme: 'twilight',
|
||||
codeMirrorTheme: 'solarized dark',
|
||||
codeThemeCss: 'atom-one-dark-reasonable.css',
|
||||
};
|
||||
|
@@ -28,7 +28,6 @@ const solarizedLightStyle = {
|
||||
codeBorderColor: '#eee8d5',
|
||||
codeColor: '#002b36',
|
||||
|
||||
aceEditorTheme: 'tomorrow',
|
||||
codeMirrorTheme: 'solarized light',
|
||||
codeThemeCss: 'atom-one-light.css',
|
||||
};
|
||||
|
50
Tools/gulp/tasks/compileExtensions.js
Normal file
50
Tools/gulp/tasks/compileExtensions.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const fs = require('fs-extra');
|
||||
const utils = require('../utils');
|
||||
|
||||
|
||||
async function getSourceCode(dest) {
|
||||
await utils.execCommand(`curl -o ${dest}/sqlite.tar.gz "https://www.sqlite.org/src/tarball/sqlite.tar.gz?r=release"`);
|
||||
await utils.execCommand(`curl -o ${dest}/amalgamation.tar.gz "https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz"`);
|
||||
await utils.execCommand(`tar -xzvf ${dest}/sqlite.tar.gz -C ${dest}`);
|
||||
await utils.execCommand(`tar -xzvf ${dest}/amalgamation.tar.gz -C ${dest}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const rootDir = utils.rootDir();
|
||||
const dest = `${rootDir}/ReactNativeClient/lib/sql-extensions`;
|
||||
|
||||
try {
|
||||
await fs.ensureDir(dest);
|
||||
|
||||
if (utils.isLinux()) {
|
||||
try {
|
||||
await fs.promises.access(`${dest}/spellfix.so`);
|
||||
} catch (e) {
|
||||
await getSourceCode(dest);
|
||||
await utils.execCommand(`gcc -shared -fPIC -Wall -I${dest}/sqlite-autoconf-3330000/ ${dest}/sqlite/ext/misc/spellfix.c -o ${dest}/spellfix.so`);
|
||||
}
|
||||
}
|
||||
|
||||
if (utils.isMac()) {
|
||||
try {
|
||||
await fs.promises.access(`${dest}/spellfix.dylib`);
|
||||
} catch (e) {
|
||||
await getSourceCode(dest);
|
||||
await utils.execCommand(`gcc -shared -fPIC -Wall -I${dest}/sqlite-autoconf-3330000/ -dynamiclib ${dest}/sqlite/ext/misc/spellfix.c -o ${dest}/spellfix.dylib`);
|
||||
}
|
||||
}
|
||||
|
||||
// if (utils.isWindows()) {
|
||||
// try {
|
||||
// await fs.promises.access(`${dest}/spellfix.dll`);
|
||||
// } catch (e) {
|
||||
// await getSourceCode(dest);
|
||||
// await utils.execCommand(`cl /I ${dest}/sqlite-autoconf-3330000 ${dest}/sqlite/ext/misc/spellfix.c -link -dll -out:spellfix.dll `)
|
||||
// }
|
||||
// }
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = main;
|
@@ -27,4 +27,3 @@ Finally, there are other ways to support the development of Joplin:
|
||||
- Consider rating the app on [Google Play](https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) or [App Store](https://itunes.apple.com/us/app/joplin/id1315599797).
|
||||
- [Create or update a translation](https://joplinapp.org/#localisation).
|
||||
- Vote for or review the app on [alternativeTo](https://alternativeto.net/software/joplin/) or [Product Hunt](https://www.producthunt.com/posts/joplin).
|
||||
- Help improve [the Wikipedia article](https://en.wikipedia.org/wiki/Draft:Joplin_(software))
|
||||
|
Reference in New Issue
Block a user