feat(editor): Mermaid syntax highlighting, linter and autocomplete
- StreamLanguage tokenizer for Mermaid: diagram-type keywords, general keywords, directions, arrows/links, strings, %% comments, init directives and YAML frontmatter, with a dark-theme HighlightStyle - live linter via mermaid.parse: maps parse errors to the right editor line (accounting for the frontmatter lines mermaid strips), with wavy underline, gutter marker and message tooltip - autocomplete: Mermaid keyword/type/direction options plus starter diagram snippets offered on the first line; bracket closing - dark-themed completion + diagnostic tooltips Adds @codemirror/lint, @codemirror/autocomplete and @lezer/highlight.
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -8,10 +8,13 @@
|
|||||||
"name": "mermix",
|
"name": "mermix",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.18.0",
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/commands": "^6.6.0",
|
||||||
"@codemirror/language": "^6.10.2",
|
"@codemirror/language": "^6.10.2",
|
||||||
|
"@codemirror/lint": "^6.8.0",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.28.0",
|
"@codemirror/view": "^6.28.0",
|
||||||
|
"@lezer/highlight": "^1.2.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
|||||||
@@ -13,10 +13,13 @@
|
|||||||
"app:build": "tauri build"
|
"app:build": "tauri build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.18.0",
|
||||||
"@codemirror/commands": "^6.6.0",
|
"@codemirror/commands": "^6.6.0",
|
||||||
"@codemirror/language": "^6.10.2",
|
"@codemirror/language": "^6.10.2",
|
||||||
|
"@codemirror/lint": "^6.8.0",
|
||||||
"@codemirror/state": "^6.4.1",
|
"@codemirror/state": "^6.4.1",
|
||||||
"@codemirror/view": "^6.28.0",
|
"@codemirror/view": "^6.28.0",
|
||||||
|
"@lezer/highlight": "^1.2.0",
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
|
|||||||
359
src/lib/cm-mermaid.ts
Normal file
359
src/lib/cm-mermaid.ts
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
import type { Extension } from '@codemirror/state';
|
||||||
|
import { HighlightStyle, StreamLanguage, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { tags as t } from '@lezer/highlight';
|
||||||
|
import { linter, lintGutter, type Diagnostic } from '@codemirror/lint';
|
||||||
|
import {
|
||||||
|
autocompletion,
|
||||||
|
snippetCompletion,
|
||||||
|
type Completion,
|
||||||
|
type CompletionContext,
|
||||||
|
type CompletionResult,
|
||||||
|
} from '@codemirror/autocomplete';
|
||||||
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Vocabulary
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Diagram-type openers (also offered as snippet anchors in autocomplete). */
|
||||||
|
const DIAGRAM_TYPES = [
|
||||||
|
'flowchart',
|
||||||
|
'graph',
|
||||||
|
'sequenceDiagram',
|
||||||
|
'classDiagram-v2',
|
||||||
|
'classDiagram',
|
||||||
|
'stateDiagram-v2',
|
||||||
|
'stateDiagram',
|
||||||
|
'erDiagram',
|
||||||
|
'journey',
|
||||||
|
'gantt',
|
||||||
|
'pie',
|
||||||
|
'quadrantChart',
|
||||||
|
'requirementDiagram',
|
||||||
|
'gitGraph',
|
||||||
|
'mindmap',
|
||||||
|
'timeline',
|
||||||
|
'sankey-beta',
|
||||||
|
'xychart-beta',
|
||||||
|
'block-beta',
|
||||||
|
'packet-beta',
|
||||||
|
'architecture-beta',
|
||||||
|
'kanban',
|
||||||
|
'radar',
|
||||||
|
'treemap',
|
||||||
|
'zenuml',
|
||||||
|
'C4Context',
|
||||||
|
'C4Container',
|
||||||
|
'C4Component',
|
||||||
|
'C4Dynamic',
|
||||||
|
'C4Deployment',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** General keywords shared across diagram types. */
|
||||||
|
const KEYWORDS = [
|
||||||
|
'participant',
|
||||||
|
'actor',
|
||||||
|
'as',
|
||||||
|
'activate',
|
||||||
|
'deactivate',
|
||||||
|
'note',
|
||||||
|
'over',
|
||||||
|
'left',
|
||||||
|
'right',
|
||||||
|
'of',
|
||||||
|
'loop',
|
||||||
|
'alt',
|
||||||
|
'else',
|
||||||
|
'opt',
|
||||||
|
'par',
|
||||||
|
'and',
|
||||||
|
'critical',
|
||||||
|
'break',
|
||||||
|
'rect',
|
||||||
|
'box',
|
||||||
|
'end',
|
||||||
|
'autonumber',
|
||||||
|
'link',
|
||||||
|
'links',
|
||||||
|
'subgraph',
|
||||||
|
'direction',
|
||||||
|
'click',
|
||||||
|
'call',
|
||||||
|
'href',
|
||||||
|
'callback',
|
||||||
|
'class',
|
||||||
|
'classDef',
|
||||||
|
'cssClass',
|
||||||
|
'style',
|
||||||
|
'linkStyle',
|
||||||
|
'state',
|
||||||
|
'namespace',
|
||||||
|
'choice',
|
||||||
|
'fork',
|
||||||
|
'join',
|
||||||
|
'title',
|
||||||
|
'accTitle',
|
||||||
|
'accDescr',
|
||||||
|
'section',
|
||||||
|
'dateFormat',
|
||||||
|
'axisFormat',
|
||||||
|
'tickInterval',
|
||||||
|
'excludes',
|
||||||
|
'includes',
|
||||||
|
'todayMarker',
|
||||||
|
'weekday',
|
||||||
|
'done',
|
||||||
|
'active',
|
||||||
|
'crit',
|
||||||
|
'milestone',
|
||||||
|
'after',
|
||||||
|
'branch',
|
||||||
|
'checkout',
|
||||||
|
'merge',
|
||||||
|
'commit',
|
||||||
|
'cherry-pick',
|
||||||
|
'order',
|
||||||
|
'tag',
|
||||||
|
'type',
|
||||||
|
'requirement',
|
||||||
|
'functionalRequirement',
|
||||||
|
'performanceRequirement',
|
||||||
|
'interfaceRequirement',
|
||||||
|
'physicalRequirement',
|
||||||
|
'designConstraint',
|
||||||
|
'element',
|
||||||
|
'satisfies',
|
||||||
|
'contains',
|
||||||
|
'copies',
|
||||||
|
'derives',
|
||||||
|
'refines',
|
||||||
|
'traces',
|
||||||
|
'verifies',
|
||||||
|
'showData',
|
||||||
|
'Person',
|
||||||
|
'System',
|
||||||
|
'Container',
|
||||||
|
'Component',
|
||||||
|
'Rel',
|
||||||
|
'Boundary',
|
||||||
|
'beta',
|
||||||
|
];
|
||||||
|
|
||||||
|
const DIRECTIONS = ['TB', 'TD', 'BT', 'RL', 'LR'];
|
||||||
|
|
||||||
|
function escapeRe(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
function alternation(words: string[]): RegExp {
|
||||||
|
const sorted = [...words].sort((a, b) => b.length - a.length).map(escapeRe);
|
||||||
|
return new RegExp(`^(?:${sorted.join('|')})(?![\\w-])`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TYPE_RE = alternation(DIAGRAM_TYPES);
|
||||||
|
const KEYWORD_RE = alternation(KEYWORDS);
|
||||||
|
const DIR_RE = new RegExp(`^(?:${DIRECTIONS.join('|')})(?![\\w-])`);
|
||||||
|
// Links / arrows: -->, ---, ==>, -.->, --x, --o, <-->, ~~~ etc.
|
||||||
|
const ARROW_RE = /^(?:<{0,2}[-=.]{2,}[->ox]{0,2}|~~~+|x--x|o--o)/;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Syntax highlighting (StreamLanguage)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface MermaidState {
|
||||||
|
fm: boolean; // inside YAML frontmatter block
|
||||||
|
}
|
||||||
|
|
||||||
|
const mermaidLanguage = StreamLanguage.define<MermaidState>({
|
||||||
|
name: 'mermaid',
|
||||||
|
startState: () => ({ fm: false }),
|
||||||
|
token(stream, state) {
|
||||||
|
// Frontmatter fences (`---`) toggle a YAML block at the top of the file.
|
||||||
|
if (stream.sol() && stream.match(/^---[ \t]*$/)) {
|
||||||
|
state.fm = !state.fm;
|
||||||
|
return 'meta';
|
||||||
|
}
|
||||||
|
if (state.fm) {
|
||||||
|
if (stream.match(/^[ \t]*[\w-]+[ \t]*:/)) return 'keyword';
|
||||||
|
stream.skipToEnd();
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stream.eatSpace()) return null;
|
||||||
|
|
||||||
|
// Comments & init directives.
|
||||||
|
if (stream.match(/^%%\{.*?\}%%/)) return 'meta';
|
||||||
|
if (stream.match(/^%%.*/)) return 'comment';
|
||||||
|
|
||||||
|
// Quoted strings.
|
||||||
|
if (stream.match(/^"(?:[^"\\]|\\.)*"?/)) return 'string';
|
||||||
|
|
||||||
|
// Links / arrows before words (arrows never start with a letter).
|
||||||
|
if (stream.match(ARROW_RE)) return 'operator';
|
||||||
|
|
||||||
|
// Keywords, diagram types and directions.
|
||||||
|
if (stream.match(TYPE_RE)) return 'keyword';
|
||||||
|
if (stream.match(KEYWORD_RE)) return 'keyword';
|
||||||
|
if (stream.match(DIR_RE)) return 'direction';
|
||||||
|
|
||||||
|
if (stream.match(/^\d+(?:\.\d+)?/)) return 'number';
|
||||||
|
if (stream.match(/^[A-Za-z_]\w*/)) return 'variable';
|
||||||
|
|
||||||
|
if (stream.match(/^[[\]{}()]/)) return 'bracket';
|
||||||
|
if (stream.match(/^[|]/)) return 'operator';
|
||||||
|
if (stream.match(/^[:;,.&#@]/)) return 'punct';
|
||||||
|
|
||||||
|
stream.next();
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
tokenTable: {
|
||||||
|
keyword: t.keyword,
|
||||||
|
direction: t.atom,
|
||||||
|
number: t.number,
|
||||||
|
string: t.string,
|
||||||
|
comment: t.lineComment,
|
||||||
|
operator: t.operator,
|
||||||
|
variable: t.variableName,
|
||||||
|
meta: t.meta,
|
||||||
|
bracket: t.bracket,
|
||||||
|
punct: t.punctuation,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mermaidHighlight = HighlightStyle.define([
|
||||||
|
{ tag: t.keyword, color: '#c4b5fd', fontWeight: '600' },
|
||||||
|
{ tag: t.atom, color: '#f0a868' },
|
||||||
|
{ tag: t.number, color: '#79c0ff' },
|
||||||
|
{ tag: t.string, color: '#7ee787' },
|
||||||
|
{ tag: t.lineComment, color: '#6b7280', fontStyle: 'italic' },
|
||||||
|
{ tag: t.operator, color: '#ff9d7b' },
|
||||||
|
{ tag: t.variableName, color: '#e6e9ef' },
|
||||||
|
{ tag: t.meta, color: '#d29922' },
|
||||||
|
{ tag: t.bracket, color: '#9aa3b2' },
|
||||||
|
{ tag: t.punctuation, color: '#9aa3b2' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Linter (mermaid.parse)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function cleanMessage(raw: string): string {
|
||||||
|
// Trim the giant "Expecting ..., got ..." tail that jison errors append.
|
||||||
|
const firstLine = raw.split('\n')[0].trim();
|
||||||
|
return firstLine.length > 200 ? `${firstLine.slice(0, 200)}…` : firstLine || raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractLine(err: unknown): number | undefined {
|
||||||
|
const anyErr = err as { hash?: { loc?: { first_line?: number }; line?: number }; message?: string };
|
||||||
|
const loc = anyErr?.hash?.loc?.first_line ?? anyErr?.hash?.line;
|
||||||
|
if (typeof loc === 'number' && loc > 0) return loc;
|
||||||
|
const msg = anyErr?.message ?? String(err);
|
||||||
|
const m = msg.match(/(?:on line|line)\s+(\d+)/i);
|
||||||
|
return m ? parseInt(m[1], 10) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mermaid removes a leading `---\n…\n---` YAML frontmatter block before
|
||||||
|
* parsing, so the line numbers it reports are shifted up by that many lines.
|
||||||
|
* Returns the number of lines to add back.
|
||||||
|
*/
|
||||||
|
function frontmatterOffset(code: string): number {
|
||||||
|
const m = code.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/);
|
||||||
|
if (!m) return 0;
|
||||||
|
return m[0].replace(/\r\n/g, '\n').replace(/\n$/, '').split('\n').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mermaidLinter = linter(
|
||||||
|
async (view): Promise<Diagnostic[]> => {
|
||||||
|
const code = view.state.doc.toString();
|
||||||
|
if (!code.trim()) return [];
|
||||||
|
try {
|
||||||
|
await mermaid.parse(code);
|
||||||
|
return [];
|
||||||
|
} catch (err) {
|
||||||
|
const message = cleanMessage(
|
||||||
|
(err as { message?: string })?.message ?? String(err)
|
||||||
|
);
|
||||||
|
const doc = view.state.doc;
|
||||||
|
const reported = extractLine(err);
|
||||||
|
const line = reported ? reported + frontmatterOffset(code) : undefined;
|
||||||
|
if (line && line >= 1 && line <= doc.lines) {
|
||||||
|
const l = doc.line(line);
|
||||||
|
return [{ from: l.from, to: l.to, severity: 'error', message }];
|
||||||
|
}
|
||||||
|
// Unknown location — flag the first non-empty line.
|
||||||
|
const first = doc.line(1);
|
||||||
|
return [{ from: first.from, to: first.to, severity: 'error', message }];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ delay: 450 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Autocomplete (keywords + starter snippets)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const SNIPPETS: Completion[] = [
|
||||||
|
snippetCompletion(
|
||||||
|
'flowchart ${TD}\n ${A}[${Start}] --> ${B}[${End}]',
|
||||||
|
{ label: 'flowchart', type: 'class', detail: 'starter', info: 'Flowchart skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion(
|
||||||
|
'sequenceDiagram\n participant ${A}\n participant ${B}\n ${A}->>${B}: ${message}',
|
||||||
|
{ label: 'sequenceDiagram', type: 'class', detail: 'starter', info: 'Sequence diagram skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion(
|
||||||
|
'classDiagram\n class ${Name} {\n +${field}\n +${method}()\n }',
|
||||||
|
{ label: 'classDiagram', type: 'class', detail: 'starter', info: 'Class diagram skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion(
|
||||||
|
'stateDiagram-v2\n [*] --> ${State1}\n ${State1} --> [*]',
|
||||||
|
{ label: 'stateDiagram-v2', type: 'class', detail: 'starter', info: 'State diagram skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion(
|
||||||
|
'erDiagram\n ${CUSTOMER} ||--o{ ${ORDER} : ${places}',
|
||||||
|
{ label: 'erDiagram', type: 'class', detail: 'starter', info: 'Entity-relationship skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion(
|
||||||
|
'gantt\n title ${Project}\n dateFormat YYYY-MM-DD\n section ${Phase}\n ${Task} :a1, ${2024-01-01}, ${7d}',
|
||||||
|
{ label: 'gantt', type: 'class', detail: 'starter', info: 'Gantt chart skeleton' }
|
||||||
|
),
|
||||||
|
snippetCompletion('pie\n title ${Title}\n "${A}" : ${40}\n "${B}" : ${60}', {
|
||||||
|
label: 'pie',
|
||||||
|
type: 'class',
|
||||||
|
detail: 'starter',
|
||||||
|
info: 'Pie chart skeleton',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const KEYWORD_OPTIONS: Completion[] = [
|
||||||
|
...DIAGRAM_TYPES.map((label) => ({ label, type: 'type' as const })),
|
||||||
|
...KEYWORDS.map((label) => ({ label, type: 'keyword' as const })),
|
||||||
|
...DIRECTIONS.map((label) => ({ label, type: 'constant' as const })),
|
||||||
|
];
|
||||||
|
|
||||||
|
function mermaidCompletions(context: CompletionContext): CompletionResult | null {
|
||||||
|
const word = context.matchBefore(/[\w-]+/);
|
||||||
|
if (!word || (word.from === word.to && !context.explicit)) return null;
|
||||||
|
|
||||||
|
// On the very first line, lead with starter snippets.
|
||||||
|
const onFirstLine = context.state.doc.lineAt(context.pos).number === 1;
|
||||||
|
const options = onFirstLine ? [...SNIPPETS, ...KEYWORD_OPTIONS] : KEYWORD_OPTIONS;
|
||||||
|
|
||||||
|
return { from: word.from, options, validFor: /^[\w-]*$/ };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public bundle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** All editor extensions providing Mermaid highlighting, linting and hints. */
|
||||||
|
export function mermaidSupport(): Extension[] {
|
||||||
|
return [
|
||||||
|
mermaidLanguage,
|
||||||
|
syntaxHighlighting(mermaidHighlight),
|
||||||
|
mermaidLinter,
|
||||||
|
lintGutter(),
|
||||||
|
autocompletion({ override: [mermaidCompletions], icons: true }),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -17,7 +17,10 @@
|
|||||||
indentWithTab,
|
indentWithTab,
|
||||||
} from '@codemirror/commands';
|
} from '@codemirror/commands';
|
||||||
import { bracketMatching, indentOnInput } from '@codemirror/language';
|
import { bracketMatching, indentOnInput } from '@codemirror/language';
|
||||||
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
|
||||||
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
import { store } from '../store.svelte';
|
import { store } from '../store.svelte';
|
||||||
|
import { mermaidSupport } from '../cm-mermaid';
|
||||||
|
|
||||||
let host: HTMLDivElement;
|
let host: HTMLDivElement;
|
||||||
let view: EditorView | undefined;
|
let view: EditorView | undefined;
|
||||||
@@ -48,6 +51,43 @@
|
|||||||
backgroundColor: 'var(--accent-soft) !important',
|
backgroundColor: 'var(--accent-soft) !important',
|
||||||
},
|
},
|
||||||
'.cm-matchingBracket': { backgroundColor: 'var(--accent-soft)', outline: 'none' },
|
'.cm-matchingBracket': { backgroundColor: 'var(--accent-soft)', outline: 'none' },
|
||||||
|
|
||||||
|
// Autocomplete popup
|
||||||
|
'.cm-tooltip': {
|
||||||
|
backgroundColor: 'var(--bg-elev-2)',
|
||||||
|
border: '1px solid var(--border-strong)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
color: 'var(--text)',
|
||||||
|
boxShadow: '0 8px 28px rgba(0,0,0,0.45)',
|
||||||
|
},
|
||||||
|
'.cm-tooltip.cm-tooltip-autocomplete > ul': { fontFamily: 'var(--mono)', maxHeight: '16em' },
|
||||||
|
'.cm-tooltip-autocomplete ul li[aria-selected]': {
|
||||||
|
backgroundColor: 'var(--accent)',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'.cm-completionIcon': { color: 'var(--text-dim)', paddingRight: '0.6em' },
|
||||||
|
'.cm-completionDetail': { color: 'var(--text-faint)', fontStyle: 'normal' },
|
||||||
|
'.cm-completionInfo': {
|
||||||
|
backgroundColor: 'var(--bg-elev-2)',
|
||||||
|
border: '1px solid var(--border-strong)',
|
||||||
|
borderRadius: '6px',
|
||||||
|
color: 'var(--text-dim)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lint diagnostics
|
||||||
|
'.cm-diagnostic': {
|
||||||
|
fontFamily: 'var(--mono)',
|
||||||
|
fontSize: '12px',
|
||||||
|
borderLeft: '3px solid var(--red)',
|
||||||
|
padding: '6px 8px',
|
||||||
|
},
|
||||||
|
'.cm-diagnostic-error': { borderLeftColor: 'var(--red)' },
|
||||||
|
'.cm-lintRange-error': {
|
||||||
|
backgroundImage: 'none',
|
||||||
|
textDecoration: 'underline wavy var(--red)',
|
||||||
|
textDecorationSkipInk: 'none',
|
||||||
|
},
|
||||||
|
'.cm-lint-marker-error': { content: '""' },
|
||||||
},
|
},
|
||||||
{ dark: true }
|
{ dark: true }
|
||||||
);
|
);
|
||||||
@@ -65,8 +105,16 @@
|
|||||||
drawSelection(),
|
drawSelection(),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
...mermaidSupport(),
|
||||||
|
keymap.of([
|
||||||
|
...closeBracketsKeymap,
|
||||||
|
...defaultKeymap,
|
||||||
|
...historyKeymap,
|
||||||
|
...lintKeymap,
|
||||||
|
indentWithTab,
|
||||||
|
]),
|
||||||
theme,
|
theme,
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((u) => {
|
EditorView.updateListener.of((u) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user