Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CodeMirror 6 #7

Merged
merged 3 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"ts-loader": "^9.3.1",
"typescript": "^4.8.2",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
"webpack-cli": "^4.10.0",
"@codemirror/view": "6.24.1",
"@codemirror/state": "6.4.1"
},
"dependencies": {
"@felisdiligens/md-table-tools": "^0.1.0",
Expand Down
3 changes: 1 addition & 2 deletions plugin.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extraScripts": [
"cmPlugin.ts",
"cmUtils.ts",
"cmPlugin/index.ts",
"commands.ts",
"dialogs.ts",
"settings.ts",
Expand Down
16 changes: 16 additions & 0 deletions src/cmPlugin/cmDynamicRequire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Imports for `@codemirror/` packages can fail when Joplin's CodeMirror 5 editor is open.
* Because we want to support both CodeMirror 5 and CodeMirror 6, we require the CM6 packages
* dynamically, and only when CM6 is available.
*/

import type * as CodeMirrorStateType from '@codemirror/state';
import type * as CodeMirrorViewType from '@codemirror/view';

export function requireCodeMirrorState() {
return require('@codemirror/state') as typeof CodeMirrorStateType;
}

export function requireCodeMirrorView() {
return require('@codemirror/view') as typeof CodeMirrorViewType;
}
9 changes: 8 additions & 1 deletion src/cmUtils.ts → src/cmPlugin/cmUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

export function isCodeMirror6(cm: Editor) {
return !!(cm as any).cm6;
}

function determineColumnIndex(line: string, ch: number): number {
let row = line.substring(0, ch).trim();
if (row.startsWith("|"))
Expand Down Expand Up @@ -85,7 +89,10 @@ export function getColumnRanges(line: string, cursor: Position, overrideColIndex
*/
export function getRangesOfAllTables(cm: Editor, allowEmptyLine: boolean): Range[] {
let ranges: Range[] = [];
const doc = cm.getDoc();

// In some versions of Joplin's beta editor, .getDoc is undefined.
const doc = cm.getDoc?.() ?? cm;

let cursor = { } as Position;

let tableStartLine = -1;
Expand Down
189 changes: 41 additions & 148 deletions src/cmPlugin.ts → src/cmPlugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Editor } from "codemirror";
import type { EditorView } from "@codemirror/view";
import { Table, TextAlignment } from "@felisdiligens/md-table-tools";
import { createPosition, getColumnRanges, getRangeOfTable, isCursorInTable, replaceAllTablesFunc, replaceRange, replaceRangeFunc, replaceSelectionFunc } from "./cmUtils";
import { getCSVRenderer, getHTMLRenderer, getMarkdownParser, getMarkdownRenderer, parseTable } from "./tableUtils";
import { createPosition, getColumnRanges, isCodeMirror6, isCursorInTable, replaceAllTablesFunc, replaceRange, replaceRangeFunc, replaceSelectionFunc } from "./cmUtils";
import { getCSVRenderer, getHTMLRenderer, getMarkdownParser, getMarkdownRenderer, parseTable } from "../tableUtils";
import makeKeyCommands from "./makeTableKeyCommands";
import { requireCodeMirrorState, requireCodeMirrorView } from "./cmDynamicRequire";

const separatorRegex = /^\|?([\s\.]*:?[\-=\.]+[:\+]?[\s\.]*\|?)+\|?$/;

module.exports = {
default: function(context) {
Expand Down Expand Up @@ -299,156 +301,47 @@ module.exports = {
Hotkeys:
Tab, Shift+Tab, Enter
*/
CodeMirror.defineOption('tableToolsHotkeys', false, (cm: Editor, value: boolean) => {
cm.on('cursorActivity', async () => {
const settings = await context.postMessage({ name: 'getSettings' });
var insideTable = isCursorInTable(cm, settings.selectedFormat == "multimd");

// if the cursor is in the table and hotkeys are allowed:
if (insideTable && settings.allowHotkeys) {
const cursor = cm.getCursor();

cm.setOption("extraKeys", {
// Insert <br> instead of normal newline:
"Enter": (cm) => {
if (settings.enterBehavior == "disabled") {
cm.replaceSelection('\n');
return;
}

var line = cm.getLine(cursor.line);
var substr = line.substring(cursor.ch, line.length);

// Check if the cursor is within the table:
if (settings.enterBehavior == "insertBrTag" &&
(!line.trim().startsWith("|") || (substr.includes("|") && cursor.ch != 0))) {
cm.replaceSelection('<br>');
} else {
cm.replaceSelection('\n');
}
},
// Jump to next cell:
"Tab": (cm) => {
if (settings.tabBehavior == "disabled") {
cm.replaceSelection('\t');
return;
}

let colIndex = -1;
if (settings.formatOnTab) {
try {
const selection = getRangeOfTable(cm, settings.selectedFormat == "multimd");
if (selection !== null) {
const parsedTable = getMarkdownParser(settings.selectedFormat).parse(cm.getRange(selection.range.from, selection.range.to));
const formattedTable = getMarkdownRenderer(settings.selectedFormat, true).render(parsedTable);
if (formattedTable)
cm.replaceRange(formattedTable, selection.range.from, selection.range.to);
colIndex = selection.column;
}
} catch (err) {
console.error(`Couldn't format table on TAB: ${err}`);
}
}

let col = getColumnRanges(cm.getLine(cursor.line), cursor, colIndex);
let range;
// Does next cell exist in row?
if (col.nextRange) {
// then select that cell:
range = col.nextRange;
} else {
// if not, first select to the current cell:
range = col.currentRange;
// skip separator row:
let i = cm.getLine(cursor.line + 1).match(separatorRegex) ? 2 : 1;
// then check, if next row exist and select the first cell in the next row:
if (cm.getLine(cursor.line + i).includes("|")) {
col = getColumnRanges(cm.getLine(cursor.line + i), createPosition(cursor.line + i, 0));
if (col.ranges.length > 0)
range = col.firstRange;
}
}
CodeMirror.defineOption('tableToolsHotkeys', false, async (cm: Editor, value: boolean) => {
const settings = await context.postMessage({ name: 'getSettings' });
const keyCommands = makeKeyCommands(settings);

if (range) {
cm.focus();
switch (settings.tabBehavior) {
case "jumpToStart":
cm.setCursor(range.from);
break;
case "jumpToEnd":
cm.setCursor(range.to);
break;
case "selectContent":
cm.setSelection(range.from, range.to);
break;
}
cm.refresh(); // This is required for the cursor to actually be visible
}
},
// Jump to previous cell:
"Shift-Tab": (cm) => {
if (settings.tabBehavior == "disabled") {
cm.replaceSelection('\t');
return;
}
if (isCodeMirror6(cm)) {
const editorControl = cm as any;

let colIndex = -1;
if (settings.formatOnTab) {
try {
const selection = getRangeOfTable(cm, settings.selectedFormat == "multimd");
if (selection !== null) {
const parsedTable = getMarkdownParser(settings.selectedFormat).parse(cm.getRange(selection.range.from, selection.range.to));
const formattedTable = getMarkdownRenderer(settings.selectedFormat, true).render(parsedTable);
if (formattedTable)
cm.replaceRange(formattedTable, selection.range.from, selection.range.to);
colIndex = selection.column;
}
} catch (err) {
console.error(`Couldn't format table on TAB: ${err}`);
}
}
// Dynamic require: Non-dynamic requires of these may break in CodeMirror 5.
const { Prec } = requireCodeMirrorState();
const { keymap } = requireCodeMirrorView();

let col = getColumnRanges(cm.getLine(cursor.line), cursor, colIndex);
let range;
// Does previous cell exist in row?
if (col.previousRange) {
// then select that cell:
range = col.previousRange;
} else {
// if not, first select to the current cell:
range = col.currentRange;
// skip separator row:
let i = cm.getLine(cursor.line - 1).match(separatorRegex) ? 2 : 1;
// then check, if previous row exist and select the last cell in the previous row:
if (cm.getLine(cursor.line - i).includes("|")) {
col = getColumnRanges(cm.getLine(cursor.line - i), createPosition(cursor.line - i, 0));
if (col.ranges.length > 0)
range = col.lastRange;
}
}
// Convert all key commands into CodeMirror6-style keybindings.
const keybindings = Object.entries(keyCommands).map(([name, command]) => ({
key: name,
run: (_view: EditorView) => {
const insideTable = isCursorInTable(cm, settings.selectedFormat == "multimd");

if (range) {
cm.focus();
switch (settings.tabBehavior) {
case "jumpToStart":
cm.setCursor(range.from);
break;
case "jumpToEnd":
cm.setCursor(range.to);
break;
case "selectContent":
cm.setSelection(range.from, range.to);
break;
}
cm.refresh(); // This is required for the cursor to actually be visible
}
if (insideTable && settings.allowHotkeys) {
command(cm);
return true;
}
});
} else {
// Disable the extraKeys when the cursor is not in the table:
cm.setOption("extraKeys", {});
}
});
return false;
},
}));
editorControl.addExtension([
// High precedence: Override the default keybindings
Prec.high(keymap.of(keybindings))
]);
} else {
cm.on('cursorActivity', async () => {
var insideTable = isCursorInTable(cm, settings.selectedFormat == "multimd");

// if the cursor is in the table and hotkeys are allowed:
if (insideTable && settings.allowHotkeys) {
cm.setOption("extraKeys", keyCommands);
} else {
// Disable the extraKeys when the cursor is not in the table:
cm.setOption("extraKeys", {});
}
});
}
});
}

Expand Down
Loading