From 234ced5570b08b136bad3e4497ea78a8295fc873 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Fri, 19 Jan 2024 11:26:22 -0800 Subject: [PATCH] Refactor content script into multiple files --- plugin.config.json | 4 +- src/QuickLinksPlugin.ts | 247 ------------------------- src/contentScript/codeMirror5Plugin.ts | 122 ++++++++++++ src/contentScript/codeMirror6Plugin.ts | 104 +++++++++++ src/contentScript/index.ts | 28 +++ src/{ => contentScript}/show-hint.css | 0 src/contentScript/types.ts | 4 + src/index.ts | 2 +- 8 files changed, 261 insertions(+), 250 deletions(-) delete mode 100644 src/QuickLinksPlugin.ts create mode 100644 src/contentScript/codeMirror5Plugin.ts create mode 100644 src/contentScript/codeMirror6Plugin.ts create mode 100644 src/contentScript/index.ts rename src/{ => contentScript}/show-hint.css (100%) create mode 100644 src/contentScript/types.ts diff --git a/plugin.config.json b/plugin.config.json index 5e1f9a4..236f488 100644 --- a/plugin.config.json +++ b/plugin.config.json @@ -1,5 +1,5 @@ { "extraScripts": [ - "QuickLinksPlugin.ts" + "contentScript/index.ts" ] -} \ No newline at end of file +} diff --git a/src/QuickLinksPlugin.ts b/src/QuickLinksPlugin.ts deleted file mode 100644 index 38c8ee5..0000000 --- a/src/QuickLinksPlugin.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { Editor } from "codemirror"; -import type * as CodeMirrorAutocompleteType from '@codemirror/autocomplete'; -import type * as CodeMirrorMarkdownType from '@codemirror/lang-markdown'; -import type * as CodeMirrorStateType from '@codemirror/state'; -import type { CompletionContext, CompletionResult, Completion } from '@codemirror/autocomplete'; -import type { EditorView } from '@codemirror/view'; - -interface Hint { - text: string; - hint: Function; - displayText?: string; - render?: Function; -} - -interface PluginContext { - postMessage(message: any): Promise; -} - -function codeMirror5Plugin(context: PluginContext, CodeMirror: any) { - function NewNoteHint(prefix: string, todo: boolean) { - let description = "New Note"; - - if(todo) - description = "New Task"; - - const newNoteHint: Hint = { - text: prefix, - hint: async (cm, data, completion) => { - const from = completion.from || data.from; - from.ch -= 2; - - const response = await context.postMessage({command: 'createNote', title: prefix, todo: todo}); - cm.replaceRange(`[${prefix}](:/${response.newNote.id})`, from, cm.getCursor(), "complete"); - }, - }; - - newNoteHint.render = (elem, _data, _completion) => { - const p = elem.ownerDocument.createElement('div'); - p.setAttribute('style', 'width: 100%; display:table;'); - elem.appendChild(p); - p.innerHTML = ` -
${prefix}
-
${description}
- ` - }; - return newNoteHint; - } - - const buildHints = async (prefix: string) =>{ - const response = await context.postMessage({ command: 'getNotes', prefix: prefix }); - - let hints: Hint[] = []; - - const notes = response.notes; - for (let i = 0; i < notes.length; i++) { - const note = notes[i]; - const hint: Hint = { - text: note.title, - hint: async (cm: Editor, data, completion) => { - const from = completion.from || data.from; - from.ch -= 2; - cm.replaceRange(`[${note.title}](:/${note.id})`, from, cm.getCursor(), "complete"); - if (response.selectText) { - const selectionStart = Object.assign({}, from); - const selectionEnd = Object.assign({}, from); - selectionStart.ch += 1; - selectionEnd.ch += 1 + note.title.length; - cm.setSelection(selectionStart, selectionEnd) - } - }, - }; - if (response.showFolders) { - const folder = !!note.folder ? note.folder : "unknown"; - hint.render = (elem, _data, _completion) => { - const p = elem.ownerDocument.createElement('div'); - p.setAttribute('style', 'width: 100%; display:table;'); - elem.appendChild(p); - p.innerHTML = ` -
${note.title}
-
In ${folder}
- ` - }; - } else { - hint.displayText = note.title; - } - hints.push(hint); - } - - if(response.allowNewNotes && prefix) { - hints.push(NewNoteHint(prefix, false)); - hints.push(NewNoteHint(prefix, true)); - } - - return hints; - } - - CodeMirror.defineOption('quickLinks', false, function(cm, value, prev) { - if (!value) return; - - cm.on('inputRead', async function (cm1, change) { - if (!cm1.state.completionActive && cm.getTokenAt(cm.getCursor()).string === '@@') { - const start = {line: change.from.line, ch: change.from.ch + 1}; - - const hint = function(cm, callback) { - const cursor = cm.getCursor(); - let prefix = cm.getRange(start, cursor) || ''; - - buildHints(prefix).then(hints => { - callback({ - list: hints, - from: {line: change.from.line, ch: change.from.ch + 1}, - to: {line: change.to.line, ch: change.to.ch + 1}, - }); - }); - }; - - setTimeout(function () { - CodeMirror.showHint(cm, hint, { - completeSingle: false, - closeOnUnfocus: true, - async: true, - closeCharacters: /[()\[\]{};:>,]/ - }); - }, 10); - } - }); - }); -} - -function codeMirror6Plugin(pluginContext: PluginContext, CodeMirror: any) { - const { autocompletion, insertCompletionText } = require('@codemirror/autocomplete') as typeof CodeMirrorAutocompleteType; - const { markdownLanguage } = require('@codemirror/lang-markdown') as typeof CodeMirrorMarkdownType; - const { EditorSelection } = require('@codemirror/state') as typeof CodeMirrorStateType; - - const completeMarkdown = async (completionContext: CompletionContext): Promise => { - const prefix = completionContext.matchBefore(/[@][@]\w+/); - if (!prefix || (prefix.from === prefix.to && !completionContext.explicit)) { - return null; - } - - const response = await pluginContext.postMessage({ - command: 'getNotes', - prefix: prefix.text, - }); - - const createApplyCompletionFn = (noteTitle: string, noteId: string) => { - return (view: EditorView, _completion: Completion, from: number, to: number) => { - const markdownLink = `[${noteTitle}](:/${noteId})`; - - view.dispatch( - insertCompletionText( - view.state, - markdownLink, - from, - to, - ), - ); - - if (response.selectText) { - const selStart = from + 1; - const selEnd = selStart + noteTitle.length; - view.dispatch({ - selection: EditorSelection.range(selStart, selEnd), - }); - } - }; - }; - - - const notes = response.notes; - const completions: Completion[] = []; - for (const note of notes) { - completions.push({ - apply: createApplyCompletionFn(note.title, note.id), - label: note.title, - detail: response.showFolders ? `In ${note.folder ?? 'unknown'}` : undefined, - }); - } - - const addNewNoteCompletion = (todo: boolean) => { - const title = prefix.text.substring(2); - const description = todo ? 'New Task' : 'New Note'; - completions.push({ - label: description, - detail: `"${title}"`, - apply: async (view, completion, from, to) => { - const response = await pluginContext.postMessage({ - command: 'createNote', - title, - todo, - }); - createApplyCompletionFn( - title, response.newNote.id - )(view, completion, from, to); - }, - }); - }; - - if (response.allowNewNotes) { - addNewNoteCompletion(true); - addNewNoteCompletion(false); - } - - return { - from: prefix.from, - options: completions, - filter: false, - }; - }; - - CodeMirror.addExtension([ - autocompletion({ - activateOnTyping: true, - override: [ completeMarkdown ], - tooltipClass: () => 'quick-links-completions', - closeOnBlur: false, - }), - markdownLanguage.data.of({ - autocomplete: completeMarkdown, - }), - ]); -} - -module.exports = { - default: function(context: PluginContext) { - return { - plugin: (CodeMirror: any) => { - if (CodeMirror.cm6) { - return codeMirror6Plugin(context, CodeMirror); - } else { - return codeMirror5Plugin(context, CodeMirror); - } - }, - codeMirrorResources: [ - 'addon/hint/show-hint', - ], - codeMirrorOptions: { - 'quickLinks': true, - }, - assets: function() { - return [ - { name: './show-hint.css'}, - ] - } - }; - }, -}; diff --git a/src/contentScript/codeMirror5Plugin.ts b/src/contentScript/codeMirror5Plugin.ts new file mode 100644 index 0000000..c36711f --- /dev/null +++ b/src/contentScript/codeMirror5Plugin.ts @@ -0,0 +1,122 @@ +import type { Editor } from "codemirror"; +import { PluginContext } from "./types"; + +interface Hint { + text: string; + hint: Function; + displayText?: string; + render?: Function; +} + +export default function codeMirror5Plugin(context: PluginContext, CodeMirror: any) { + function NewNoteHint(prefix: string, todo: boolean) { + let description = "New Note"; + + if(todo) + description = "New Task"; + + const newNoteHint: Hint = { + text: prefix, + hint: async (cm: Editor, data, completion) => { + const from = completion.from || data.from; + from.ch -= 2; + + const response = await context.postMessage({command: 'createNote', title: prefix, todo: todo}); + cm.replaceRange(`[${prefix}](:/${response.newNote.id})`, from, cm.getCursor(), "complete"); + }, + }; + + newNoteHint.render = (elem, _data, _completion) => { + const p = elem.ownerDocument.createElement('div'); + p.setAttribute('style', 'width: 100%; display:table;'); + elem.appendChild(p); + p.innerHTML = ` +
${prefix}
+
${description}
+ ` + }; + return newNoteHint; + } + + const buildHints = async (prefix: string) =>{ + const response = await context.postMessage({ command: 'getNotes', prefix: prefix }); + + let hints: Hint[] = []; + + const notes = response.notes; + for (let i = 0; i < notes.length; i++) { + const note = notes[i]; + const hint: Hint = { + text: note.title, + hint: async (cm: Editor, data, completion) => { + const from = completion.from || data.from; + from.ch -= 2; + cm.replaceRange(`[${note.title}](:/${note.id})`, from, cm.getCursor(), "complete"); + if (response.selectText) { + const selectionStart = Object.assign({}, from); + const selectionEnd = Object.assign({}, from); + selectionStart.ch += 1; + selectionEnd.ch += 1 + note.title.length; + cm.setSelection(selectionStart, selectionEnd) + } + }, + }; + if (response.showFolders) { + const folder = !!note.folder ? note.folder : "unknown"; + hint.render = (elem, _data, _completion) => { + const p = elem.ownerDocument.createElement('div'); + p.setAttribute('style', 'width: 100%; display:table;'); + elem.appendChild(p); + p.innerHTML = ` +
${note.title}
+
In ${folder}
+ ` + }; + } else { + hint.displayText = note.title; + } + hints.push(hint); + } + + if(response.allowNewNotes && prefix) { + hints.push(NewNoteHint(prefix, false)); + hints.push(NewNoteHint(prefix, true)); + } + + return hints; + } + + CodeMirror.defineOption('quickLinks', false, function(cm, value, prev) { + if (!value) return; + + cm.on('inputRead', async function (cm1, change) { + if (!cm1.state.completionActive && cm.getTokenAt(cm.getCursor()).string === '@@') { + const start = {line: change.from.line, ch: change.from.ch + 1}; + + const hint = function(cm, callback) { + const cursor = cm.getCursor(); + let prefix = cm.getRange(start, cursor) || ''; + + buildHints(prefix).then(hints => { + callback({ + list: hints, + from: {line: change.from.line, ch: change.from.ch + 1}, + to: {line: change.to.line, ch: change.to.ch + 1}, + }); + }); + }; + + setTimeout(function () { + CodeMirror.showHint(cm, hint, { + completeSingle: false, + closeOnUnfocus: true, + async: true, + closeCharacters: /[()\[\]{};:>,]/ + }); + }, 10); + } + }); + }); +} + + diff --git a/src/contentScript/codeMirror6Plugin.ts b/src/contentScript/codeMirror6Plugin.ts new file mode 100644 index 0000000..0041ec3 --- /dev/null +++ b/src/contentScript/codeMirror6Plugin.ts @@ -0,0 +1,104 @@ + +// To avoid importing @codemirror packages directly, we import CodeMirror types, then +// later, require dynamically. +// +// This allows us to continue supporting older versions of Joplin that don't depend +// on @codemirror/ packages. +import type * as CodeMirrorAutocompleteType from '@codemirror/autocomplete'; +import type * as CodeMirrorStateType from '@codemirror/state'; + +import type { CompletionContext, CompletionResult, Completion } from '@codemirror/autocomplete'; +import type { EditorView } from '@codemirror/view'; + +import { PluginContext } from './types'; + + +export default function codeMirror6Plugin(pluginContext: PluginContext, CodeMirror: any) { + const { autocompletion, insertCompletionText } = require('@codemirror/autocomplete') as typeof CodeMirrorAutocompleteType; + const { EditorSelection } = require('@codemirror/state') as typeof CodeMirrorStateType; + + const completeMarkdown = async (completionContext: CompletionContext): Promise => { + const prefix = completionContext.matchBefore(/[@][@]\w+/); + if (!prefix || (prefix.from === prefix.to && !completionContext.explicit)) { + return null; + } + + const response = await pluginContext.postMessage({ + command: 'getNotes', + prefix: prefix.text, + }); + + const createApplyCompletionFn = (noteTitle: string, noteId: string) => { + return (view: EditorView, _completion: Completion, from: number, to: number) => { + const markdownLink = `[${noteTitle}](:/${noteId})`; + + view.dispatch( + insertCompletionText( + view.state, + markdownLink, + from, + to, + ), + ); + + if (response.selectText) { + const selStart = from + 1; + const selEnd = selStart + noteTitle.length; + view.dispatch({ + selection: EditorSelection.range(selStart, selEnd), + }); + } + }; + }; + + + const notes = response.notes; + const completions: Completion[] = []; + for (const note of notes) { + completions.push({ + apply: createApplyCompletionFn(note.title, note.id), + label: note.title, + detail: response.showFolders ? `In ${note.folder ?? 'unknown'}` : undefined, + }); + } + + const addNewNoteCompletion = (todo: boolean) => { + const title = prefix.text.substring(2); + const description = todo ? 'New Task' : 'New Note'; + completions.push({ + label: description, + detail: `"${title}"`, + apply: async (view, completion, from, to) => { + const response = await pluginContext.postMessage({ + command: 'createNote', + title, + todo, + }); + + const applyCompletion = createApplyCompletionFn(title, response.newNote.id); + applyCompletion(view, completion, from, to); + }, + }); + }; + + if (response.allowNewNotes) { + addNewNoteCompletion(true); + addNewNoteCompletion(false); + } + + return { + from: prefix.from, + options: completions, + filter: false, + }; + }; + + CodeMirror.addExtension([ + autocompletion({ + activateOnTyping: true, + override: [ completeMarkdown ], + tooltipClass: () => 'quick-links-completions', + }), + ]); +} + diff --git a/src/contentScript/index.ts b/src/contentScript/index.ts new file mode 100644 index 0000000..4d040c3 --- /dev/null +++ b/src/contentScript/index.ts @@ -0,0 +1,28 @@ +import { PluginContext } from './types'; +import codeMirror5Plugin from './codeMirror5Plugin'; +import codeMirror6Plugin from './codeMirror6Plugin'; + +module.exports = { + default: function(context: PluginContext) { + return { + plugin: (CodeMirror: any) => { + if (CodeMirror.cm6) { + return codeMirror6Plugin(context, CodeMirror); + } else { + return codeMirror5Plugin(context, CodeMirror); + } + }, + codeMirrorResources: [ + 'addon/hint/show-hint', + ], + codeMirrorOptions: { + 'quickLinks': true, + }, + assets: function() { + return [ + { name: './show-hint.css'}, + ] + } + }; + }, +}; diff --git a/src/show-hint.css b/src/contentScript/show-hint.css similarity index 100% rename from src/show-hint.css rename to src/contentScript/show-hint.css diff --git a/src/contentScript/types.ts b/src/contentScript/types.ts new file mode 100644 index 0000000..7c0e0d8 --- /dev/null +++ b/src/contentScript/types.ts @@ -0,0 +1,4 @@ + +export interface PluginContext { + postMessage(message: any): Promise; +} diff --git a/src/index.ts b/src/index.ts index 15e5d47..459a1e5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -130,7 +130,7 @@ joplin.plugins.register({ await joplin.contentScripts.register( ContentScriptType.CodeMirrorPlugin, 'quickLinks', - './QuickLinksPlugin.js' + './contentScript/index.js' ); await joplin.contentScripts.onMessage('quickLinks', async (message: any) => {