Skip to content

Commit

Permalink
Merge pull request #65 from personalizedrefrigerator/pr/formatting-ba…
Browse files Browse the repository at this point in the history
…r-cm6

Formatting bar: Add support for CodeMirror 6
  • Loading branch information
SeptemberHX authored Mar 2, 2024
2 parents db57eaf + 023ef40 commit bdf2b28
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 14 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
"devDependencies": {
"@types/codemirror": "^5.60.5",
"codemirror": "^5.60.5",
"@codemirror/view": "6.24.1",
"@codemirror/state": "6.4.1",
"@types/node": "^18.7.13",
"chalk": "^4.1.0",
"copy-webpack-plugin": "^11.0.0",
Expand Down
34 changes: 25 additions & 9 deletions src/driver/codemirror/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import { isReadOnly } from "../../../utils/cm-utils";

export function initCommands(cm, CodeMirror) {
const commandBridge = new CommandsBridge(cm);
CodeMirror.defineExtension('markdownHL1', commandBridge.hL1.bind(commandBridge));
CodeMirror.defineExtension('markdownHL2', commandBridge.hL2.bind(commandBridge));
CodeMirror.defineExtension('markdownHL3', commandBridge.hL3.bind(commandBridge));
CodeMirror.defineExtension('markdownHL4', commandBridge.hL4.bind(commandBridge));
CodeMirror.defineExtension('markdownHL5', commandBridge.hL5.bind(commandBridge));
CodeMirror.defineExtension('markdownHL6', commandBridge.hL6.bind(commandBridge));
CodeMirror.defineExtension('markdownHL7', commandBridge.hL7.bind(commandBridge));

const addCommand = (name: string, command: ()=>void) => {
// Work around a bug in CodeMirror.defineExtension on CodeMirror 6 --
// closing and opening settings can leave such extensions only registered once.
// See https://github.com/laurent22/joplin/issues/10023
if (cm.cm6) {
CodeMirror.registerCommand(name, command);
} else {
CodeMirror.defineExtension(name, command);
}
};

addCommand('markdownHL1', commandBridge.hL1.bind(commandBridge));
addCommand('markdownHL2', commandBridge.hL2.bind(commandBridge));
addCommand('markdownHL3', commandBridge.hL3.bind(commandBridge));
addCommand('markdownHL4', commandBridge.hL4.bind(commandBridge));
addCommand('markdownHL5', commandBridge.hL5.bind(commandBridge));
addCommand('markdownHL6', commandBridge.hL6.bind(commandBridge));
addCommand('markdownHL7', commandBridge.hL7.bind(commandBridge));
}

class CommandsBridge {
Expand Down Expand Up @@ -42,7 +56,7 @@ class CommandsBridge {
}

hlWithColor(color: string) {
if (this.cm.isReadOnly()) {
if (isReadOnly(this.cm)) {
return;
}
markdownInline(this.cm, `<mark style="background: ${color}">`, '</mark>', 'mark')
Expand All @@ -62,7 +76,9 @@ function markdownInline (cm: CodeMirror.Editor, pre: string, post: string, token
if (!cm.somethingSelected()) {
// TODO: Check token type state at the cursor position to leave the
// mode if already in the mode.
let currentToken = cm.getTokenAt(cm.getCursor()).type
//
// FIXME(cm6): cm.getTokenAt is unsupported in CodeMirror 6.
let currentToken = cm.getTokenAt?.(cm.getCursor())?.type
if (tokentype !== undefined && currentToken !== null && currentToken?.includes(tokentype)) { // -- the tokentypes can be multiple (spell-error, e.g.)
// We are, indeed, currently in this token. So let's check *how*
// we are going to leave the state.
Expand Down
13 changes: 13 additions & 0 deletions src/driver/codemirror/formattingBar/initFormattingBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ContentScriptContext } from "api/types";
import v6FormattingBar from "./v6/formattingBar"
import v5FormattingBar from "./v5/formattingBar";

const initFormattingBar = (context: ContentScriptContext, cm: any) => {
if (cm.cm6) {
cm.addExtension(v6FormattingBar(context));
} else {
v5FormattingBar(context, cm);
}
};

export default initFormattingBar;
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import CodeMirror from 'codemirror'
import tippy, { Instance } from 'tippy.js'
import {ContextMsgType} from "../../../common";
import {ContextMsgType} from "../../../../common";

/**
* The formatting bar is shown while there is a selection
Expand Down
188 changes: 188 additions & 0 deletions src/driver/codemirror/formattingBar/v6/formattingBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/**
* @ignore
* BEGIN HEADER
*
* Contains: CodeMirror formatting bar hook
* CVM-Role: CodeMirror 6 plugin
* License: GNU GPL v3
*
* Description: Provides a pop-up menu that allows users to quickly
* execute commands with a mouse or touchscreen.
*
* END HEADER
*/

// Helpful documentation:
// - https://codemirror.net/examples/tooltip/

import { requireCodeMirrorState, requireCodeMirrorView } from '../../../../utils/cm-dynamic-require';
import type { EditorState } from '@codemirror/state';
import type { Tooltip } from '@codemirror/view';
import { ContentScriptContext } from 'api/types';
import { ContextMsgType } from '../../../../common';

interface CommandInfo {
name: string;
alt: string;

/** Space-separated list of class names to give to the icon. */
icon: string;
}

const commandInfos: CommandInfo[] = [
{
name: 'markdownBold',
icon: 'fas fa-bold in-button',
alt: 'Bold',
},
{
name: 'markdownItalic',
icon: 'fas fa-italic in-button',
alt: 'Italic',
},
{
name: 'markdownLink',
icon: 'fas fa-link in-button',
alt: 'Link',
},
{
alt: 'Code',
name: 'markdownCode',
icon: 'fas fa-code in-button',
},
...([1, 2, 3, 4, 5, 6, 7].map((i): CommandInfo => ({
name: `markdownHL${i}`,
icon: `fas fa-circle color${i} in-button`,
alt: `Color ${i}`,
}))),
];

const buildTooltips = (state: EditorState, context: ContentScriptContext): Tooltip[] => {
return state.selection.ranges
// Only show for non-empty selection ranges
.filter(range => !range.empty)
.map((range): Tooltip => {
return {
pos: range.from,
above: true,
arrow: false,
create: (_view) => {
const container = document.createElement('div');
container.classList.add('cm-editor-formatting-bar');

for (const commandInfo of commandInfos) {
const commandButton = document.createElement('button');
commandButton.onclick = () => {
context.postMessage({
type: ContextMsgType.SHORTCUT,
content: commandInfo.name,
});
};

const commandIcon = document.createElement('i');
commandIcon.classList.add(...commandInfo.icon.split(' '));

commandButton.setAttribute('aria-label', commandInfo.alt);

commandButton.appendChild(commandIcon);
container.appendChild(commandButton);
}

return { dom: container };
}
};
});
};

const formattingBarStateField = (context: ContentScriptContext) => {
const { StateField } = requireCodeMirrorState();
const { showTooltip } = requireCodeMirrorView();

return StateField.define<readonly Tooltip[]>({
// Initial state
create: state => buildTooltips(state, context),

update: (tooltips, tr) => {
if (!tr.docChanged && !tr.selection) {
return tooltips;
}

return buildTooltips(tr.state, context);
},

provide: (field) => {
const deps = [ field ];
return showTooltip.computeN(
deps,
state => state.field(field),
);
},
});
};

const formattingBar = (context: ContentScriptContext) => {
const { EditorView } = requireCodeMirrorView();

return [
formattingBarStateField(context),
EditorView.baseTheme({
'& .cm-tooltip.cm-editor-formatting-bar': {
display: 'flex',
'flex-direction': 'row',
'border-radius': '5px',

border: 'none',
'background-color': 'rgba(51, 51,51, 0.85)',

'& > button': {
'background-color': 'transparent',
border: 'none',
'flex-grow': 1,
padding: '4px 8px',
width: '35px',
height: '35px',
color: 'white',
transition: '0.3s all ease',

'&:hover, &:focus-visible': {
'background-color': 'rgb(120, 120, 120)',
},
'&:first-child': {
'border-top-left-radius': '4px',
'border-bottom-left-radius': '4px',
},
'&:last-child': {
'border-top-right-radius': '4px',
'border-bottom-right-radius': '4px',
},
},
'& i.fas': {
'font-family': "'Font Awesome 5 Free' !important",
'&.color1': {
color: '#ffd400',
},
'&.color2': {
color: '#ff6666',
},
'&.color3': {
color: '#5fb236',
},
'&.color4': {
color: '#2ea8e5',
},
'&.color5': {
color: '#a28ae5',
},
'&.color6': {
color: '#e56eee',
},
'&.color7': {
color: '#f19837',
},
},
},
}),
];
};

export default formattingBar;
6 changes: 3 additions & 3 deletions src/driver/codemirror/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {taskAndHeaderRender} from "./taskRender";
import {enhancement_mermaid_render} from "./common";
import mermaidRender from "./mermaidRender";
import inlineMarkerRender from "./inlineMarker";
import formattingBarHook from "./formattingBar/formattingBart";
import {initCommands} from "./commands";
import {fixListNumber, listNumberCorrector} from "./listNumber";
import initFormattingBar from "./formattingBar/initFormattingBar";


module.exports = {
Expand Down Expand Up @@ -54,7 +54,7 @@ module.exports = {
await linkFolderOptionFunc(context, cm, val, old);

if (settings.quickCommands) {
new QuickCommands(context, cm as ExtendedEditor & Editor, CodeMirror);
QuickCommands.from(context, cm as ExtendedEditor & Editor, CodeMirror);
}

if (settings.taskCmRender || settings.headerHashRender) {
Expand All @@ -70,7 +70,7 @@ module.exports = {
}

if (settings.formattingBar) {
formattingBarHook(context, cm);
initFormattingBar(context, cm);
}

if (settings.listNumberAutoCorrect) {
Expand Down
6 changes: 6 additions & 0 deletions src/driver/codemirror/linkFolder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const ENHANCEMENT_PLANTUML_SPAN_MARKER_LINE_CLASS = 'enhancement-plantuml-block-
const ENHANCEMENT_HORIZONTAL_LINE_CLASS_LINE_CLASS = 'enhancement-horizontal-line-marker-line';

export async function linkFolderOptionFunc(_context, cm, val, old) {
// FIXME(for:cm6): Disabled because the following logic is broken in
// the CM6 editor.
if (cm.cm6) {
return;
}

const renderBlockLink = cm.state.enhancement ? !cm.state.enhancement.settings.blockLinkFolder : false;
const renderBlockImage = cm.state.enhancement ? cm.state.enhancement.settings.blockImageFolder : false;
const renderBlockImageCaption = cm.state.enhancement ? cm.state.enhancement.settings.blockImageCaption : false;
Expand Down
5 changes: 5 additions & 0 deletions src/driver/codemirror/mode/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
export function initCodeMode(context, CodeMirror) {
// FIXME(cm6): For now, not supported in CM6
if (CodeMirror.cm6) {
return;
}

CodeMirror.defineMode('pseudocode', function (config, modeConfig) {
return CodeMirror.getMode(config, { name: 'stex', inMathMode: false });
});
Expand Down
3 changes: 3 additions & 0 deletions src/driver/codemirror/overlay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ export function initOverlayOption(_context, CodeMirror) {
});

CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
// FIXME(cm6): Unsupported in CodeMirror 6.
if (CodeMirror.cm6) return;

var prev = old && old != CodeMirror.Init;
if (!prev) {
updateActiveLine(cm);
Expand Down
12 changes: 11 additions & 1 deletion src/driver/codemirror/quickCommands/quickCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PlantumlHints from './PlantumlHints'
import {getDateHints} from "./DateHints";
import MermaidHints from "./MermaidHints";
import ShortTypeHints from "./ShortTypeHints";
import { ContentScriptContext } from "api/types";

const TRIGGER_SYMBOL = '/';
const HINT_ITEM_CLASS = 'quick-commands-hint';
Expand Down Expand Up @@ -52,11 +53,20 @@ export type ExtendedEditor = {
};

export default class QuickCommands {
constructor(private readonly context, private readonly editor: ExtendedEditor & Editor, private readonly cm: typeof CodeMirror) {
private constructor(private readonly context, private readonly editor: ExtendedEditor & Editor, private readonly cm: typeof CodeMirror) {
this.editor.on('cursorActivity', this.triggerHints.bind(this));
setTimeout(this.init.bind(this), 100);
}

public static from(context: ContentScriptContext, editor: ExtendedEditor & Editor, cm: typeof CodeMirror) {
// FIXME(cm6): QuickCommands uses CodeMirror 5 functionality that isn't supported by Joplin's compatibility layer.
if ((cm as any).cm6) {
console.warn('QuickCommands is not supported for CodeMirror 6');
} else {
return new QuickCommands(context, editor, cm);
}
}

private async init() {

}
Expand Down
20 changes: 20 additions & 0 deletions src/utils/cm-dynamic-require.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

/**
* Because this plugin supports both CodeMirror 5 and CodeMirror 6,
* we need to be able to dynamic-require some packages that are only available
* in CodeMirror 6.
*
* Normal imports fail in older versions of Joplin where these libraries aren't
* available and perhaps in some cases while using CodeMirror 5.
*/

import type * as CodeMirrorView from '@codemirror/view';
import type * as CodeMirrorState from '@codemirror/state';

export function requireCodeMirrorView() {
return require('@codemirror/view') as typeof CodeMirrorView;
}

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

0 comments on commit bdf2b28

Please sign in to comment.