Skip to content

Commit

Permalink
Migrate to CodeMirror 6
Browse files Browse the repository at this point in the history
  • Loading branch information
personalizedrefrigerator committed Jan 19, 2024
1 parent 74f526a commit 17a63ee
Show file tree
Hide file tree
Showing 4 changed files with 392 additions and 156 deletions.
33 changes: 19 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,32 @@
"name": "joplin-plugin-quick-links",
"version": "1.2.4",
"scripts": {
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive",
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
"prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --update"
"updateVersion": "webpack --env joplin-plugin-config=updateVersion",
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force"
},
"license": "MIT",
"keywords": [
"joplin-plugin"
],
"files": [
"publish"
],
"devDependencies": {
"@types/node": "^14.0.14",
"@types/node": "^18.7.13",
"@types/codemirror": "^5.60.7",
"@codemirror/autocomplete": "^6.12.0",
"@codemirror/lang-markdown": "^6.2.4",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^6.1.0",
"fs-extra": "^9.0.1",
"glob": "^7.1.6",
"on-build-webpack": "^0.1.0",
"tar": "^6.0.5",
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11",
"yargs": "^16.2.0"
"copy-webpack-plugin": "^11.0.0",
"fs-extra": "^10.1.0",
"glob": "^8.0.3",
"tar": "^6.1.11",
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"@joplin/lib": "~2.9"
}
}
}
308 changes: 208 additions & 100 deletions src/QuickLinksPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
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;
Expand All @@ -7,125 +12,228 @@ interface Hint {
render?: Function;
}

module.exports = {
default: function(context: any) {
interface PluginContext {
postMessage(message: any): Promise<any>;
}

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 = `
<div style="display:table-cell; padding-right: 5px">${prefix}</div>
<div style="display:table-cell; text-align: right;"><small><em>${description}</em></small></div>
`
};
return newNoteHint;
}

function NewNoteHint(prefix: string, todo: boolean) {
let description = "New Note";
const buildHints = async (prefix: string) =>{
const response = await context.postMessage({ command: 'getNotes', prefix: prefix });

if(todo)
description = "New Task";
let hints: Hint[] = [];

const newNoteHint: Hint = {
text: prefix,
hint: async (cm, data, completion) => {
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;

const response = await context.postMessage({command: 'createNote', title: prefix, todo: todo});
cm.replaceRange(`[${prefix}](:/${response.newNote.id})`, from, cm.getCursor(), "complete");
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 = `
<div style="display:table-cell; padding-right: 5px">${note.title}</div>
<div style="display:table-cell; text-align: right;"><small><em>In ${folder}</em></small></div>
`
};
} else {
hint.displayText = note.title;
}
hints.push(hint);
}

newNoteHint.render = (elem, _data, _completion) => {
const p = elem.ownerDocument.createElement('div');
p.setAttribute('style', 'width: 100%; display:table;');
elem.appendChild(p);
p.innerHTML = `
<div style="display:table-cell; padding-right: 5px">${prefix}</div>
<div style="display:table-cell; text-align: right;"><small><em>${description}</em></small></div>
`
};
return newNoteHint;
if(response.allowNewNotes && prefix) {
hints.push(NewNoteHint(prefix, false));
hints.push(NewNoteHint(prefix, true));
}

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)
}
},
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},
});
});
};
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 = `
<div style="display:table-cell; padding-right: 5px">${note.title}</div>
<div style="display:table-cell; text-align: right;"><small><em>In ${folder}</em></small></div>
`
};
} else {
hint.displayText = note.title;
}
hints.push(hint);
}

if(response.allowNewNotes && prefix) {
hints.push(NewNoteHint(prefix, false));
hints.push(NewNoteHint(prefix, true));
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;

return hints;
const completeMarkdown = async (completionContext: CompletionContext): Promise<CompletionResult> => {
const prefix = completionContext.matchBefore(/[@][@]\w+/);
if (!prefix || (prefix.from === prefix.to && !completionContext.explicit)) {
return null;
}

const plugin = function(CodeMirror) {
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);
}
});
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 {
plugin: plugin,
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,
},
Expand All @@ -134,6 +242,6 @@ module.exports = {
{ name: './show-hint.css'},
]
}
}
}
}
};
},
};
Loading

0 comments on commit 17a63ee

Please sign in to comment.