Merge branch 'develop' into feat/add-rootless-dockerfiles

This commit is contained in:
Jon Fuller 2025-05-14 09:02:47 -07:00 committed by GitHub
commit 6430d7048d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
71 changed files with 3134 additions and 1249 deletions

View File

@ -20,6 +20,17 @@ There are no special migration steps to migrate from a zadam/Trilium instance to
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
## Documentation
We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.
Below are some quick links for your convenience to navigate the documentation:
- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
## 💬 Discuss with us
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
@ -63,7 +74,7 @@ Feel free to join our official conversations. We would love to hear what feature
To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have a few options:
* Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the ```trilium``` executable.
* Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
* Access TriliumNext via the web interface of a server installation (see below)
* Currently only the latest versions of Chrome & Firefox are supported (and tested).
* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
@ -90,18 +101,38 @@ You can also read [Patterns of personal knowledge base](https://triliumnext.gith
### Code
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
npm install
npm run server:start
pnpm install
pnpm run server:start
```
### Documentation
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx run edit-docs:serve
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
```
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
### Documentation
### Developer Documentation
See the [documentation guide](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Documentation.md) for details.
Please view the [documentation guide](./docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md) for details. If you have more questions, feel free to reach out via the links described in the "Discuss with us" section above.
## 👏 Shoutouts

View File

@ -23,13 +23,13 @@
"@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/commons": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"bootstrap": "5.3.6",
"dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0",
"draggabilly": "3.0.0",
"eslint-linter-browserify": "9.26.0",
"force-graph": "1.49.5",
"force-graph": "1.49.6",
"globals": "16.1.0",
"i18next": "25.1.2",
"i18next-http-backend": "3.0.2",
@ -57,8 +57,8 @@
"@types/jquery": "3.5.32",
"@types/leaflet": "1.9.17",
"@types/leaflet-gpx": "1.3.7",
"@types/react": "19.1.3",
"@types/react-dom": "19.1.3",
"@types/react": "19.1.4",
"@types/react-dom": "19.1.5",
"copy-webpack-plugin": "13.0.0",
"happy-dom": "17.4.7",
"script-loader": "0.7.2"

View File

@ -11,6 +11,7 @@ import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
export interface SetNoteOpts {
triggerSwitchEvent?: unknown;
@ -312,7 +313,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
async getCodeEditor() {
return this.timeout(
new Promise<CodeMirrorInstance>((resolve) =>
new Promise<CodeMirror>((resolve) =>
appContext.triggerCommand("executeWithCodeEditor", {
resolve,
ntxId: this.ntxId

View File

@ -1,74 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
async function validatorHtml(text, options) {
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
if (result !== null) {
// preceding code is copied over but any (non-newline) character is replaced with space
// this will preserve line numbers etc.
const prefix = text.substr(0, result.index).replace(/./g, " ");
const js = prefix + result[1];
return await validatorJavaScript(js, options);
}
return [];
}
async function validatorJavaScript(text, options) {
if (glob.isMobile()
|| glob.getActiveContextNote() == null
|| glob.getActiveContextNote().mime === 'application/json') {
// eslint doesn't seem to validate pure JSON well
return [];
}
if (text.length > 20000) {
console.log("Skipping linting because of large size: ", text.length);
return [];
}
const errors = await glob.linter(text, glob.getActiveContextNote().mime);
console.log(errors);
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
CodeMirror.registerHelper("lint", "html", validatorHtml);
function parseErrors(errors, output) {
for (const error of errors) {
const startLine = error.line - 1;
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
const startCol = error.column - 1;
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
output.push({
message: error.message,
severity: error.severity === 1 ? "warning" : "error",
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol)
});
}
}
});

View File

@ -5,7 +5,6 @@ import libraryLoader from "./library_loader.js";
import ws from "./ws.js";
import froca from "./froca.js";
import linkService from "./link.js";
import { lint } from "./eslint.js";
function setupGlobs() {
window.glob.isDesktop = utils.isDesktop;
@ -19,7 +18,6 @@ function setupGlobs() {
// required for ESLint plugin and CKEditor
window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
window.glob.requireLibrary = libraryLoader.requireLibrary;
window.glob.linter = lint;
window.glob.appContext = appContext; // for debugging
window.glob.froca = froca;
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while

View File

@ -7,37 +7,6 @@ export interface Library {
css?: string[];
}
const CODE_MIRROR: Library = {
js: () => {
const scriptsToLoad = [
"node_modules/codemirror/lib/codemirror.js",
"node_modules/codemirror/addon/display/placeholder.js",
"node_modules/codemirror/addon/edit/matchbrackets.js",
"node_modules/codemirror/addon/edit/matchtags.js",
"node_modules/codemirror/addon/fold/xml-fold.js",
"node_modules/codemirror/addon/lint/lint.js",
"node_modules/codemirror/addon/mode/loadmode.js",
"node_modules/codemirror/addon/mode/multiplex.js",
"node_modules/codemirror/addon/mode/overlay.js",
"node_modules/codemirror/addon/mode/simple.js",
"node_modules/codemirror/addon/search/match-highlighter.js",
"node_modules/codemirror/mode/meta.js",
"node_modules/codemirror/keymap/vim.js",
"libraries/codemirror/eslint.js"
];
const mimeTypes = mimeTypesService.getMimeTypes();
for (const mimeType of mimeTypes) {
if (mimeType.enabled && mimeType.codeMirrorSource) {
scriptsToLoad.push(mimeType.codeMirrorSource);
}
}
return scriptsToLoad;
},
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
};
const KATEX: Library = {
js: ["node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/auto-render.min.js"],
css: ["node_modules/katex/dist/katex.min.css"]
@ -152,7 +121,6 @@ export default {
requireCss,
requireLibrary,
loadHighlightingTheme,
CODE_MIRROR,
KATEX,
HIGHLIGHT_JS
};

View File

@ -68,6 +68,7 @@ export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "GDScript (Godot)", mime: "text/x-gdscript" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ title: "Go", mime: "text/x-go", highlightJs: "go", default: true },
@ -106,6 +107,7 @@ export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nix", mime: "text/x-nix", highlightJs: "nix" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },

View File

@ -420,33 +420,24 @@ body.desktop #context-menu-container .dropdown-item > span {
width: 100%;
}
.CodeMirror {
.cm-editor {
height: 100%;
background: inherit;
outline: none !important;
border-radius: 6px;
overflow: hidden;
margin: 4px;
}
body .CodeMirror {
body .cm-editor {
font-size: var(--monospace-font-size);
}
.CodeMirror-gutters {
body .cm-editor .cm-gutters {
background-color: inherit !important;
border-right: none;
}
.cm-matchhighlight {
background-color: #eeeeee;
}
.cm-matchhighlight.ck-find-result{
background: var(--ck-color-highlight-background);
}
.cm-matchhighlight.ck-find-result_selected {
background-color: #ff9633;
}
.CodeMirror pre.CodeMirror-placeholder {
body .cm-editor .cm-placeholder {
color: #999 !important;
}
@ -457,11 +448,11 @@ body .CodeMirror {
margin-bottom: 10px;
}
#sql-console-query .CodeMirror {
#sql-console-query .cm-editor {
height: 150px;
}
#sql-console-query .CodeMirror-scroll {
#sql-console-query .cm-editor .cm-scroller {
min-height: inherit !important;
}
@ -524,7 +515,7 @@ button.btn-sm {
padding: 0;
}
pre:not(.CodeMirror-line):not(.hljs) {
pre:not(.hljs) {
color: var(--main-text-color) !important;
white-space: pre-wrap;
font-size: 100%;

View File

@ -81,10 +81,6 @@ body ::-webkit-calendar-picker-indicator {
filter: invert(1);
}
body .CodeMirror {
filter: invert(90%) hue-rotate(180deg);
}
.excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important;
}

View File

@ -244,10 +244,6 @@ body ::-webkit-calendar-picker-indicator {
filter: invert(1);
}
body .CodeMirror {
filter: invert(90%) hue-rotate(180deg);
}
.excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important;
}

View File

@ -1616,7 +1616,7 @@
"auto-detect-language": "自动检测"
},
"highlighting": {
"title": "文本笔记的代码语法高亮",
"title": "",
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
"color-scheme": "颜色方案"
},

View File

@ -1568,7 +1568,7 @@
"auto-detect-language": "Automatisch erkannt"
},
"highlighting": {
"title": "Code-Syntax-Hervorhebung für Textnotizen",
"title": "",
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
"color-scheme": "Farbschema"
},

View File

@ -1247,13 +1247,11 @@
"reprocessing_embeddings": "Reprocessing...",
"reprocess_started": "Embedding reprocessing started in the background",
"reprocess_error": "Error starting embedding reprocessing",
"reprocess_index": "Rebuild Search Index",
"reprocess_index_description": "Optimize the search index for better performance. This uses existing embeddings without regenerating them (much faster than reprocessing all embeddings).",
"reprocessing_index": "Rebuilding...",
"reprocess_index_started": "Search index optimization started in the background",
"reprocess_index_error": "Error rebuilding search index",
"index_rebuild_progress": "Index Rebuild Progress",
"index_rebuilding": "Optimizing index ({{percentage}}%)",
"index_rebuild_complete": "Index optimization complete",
@ -1824,7 +1822,7 @@
"auto-detect-language": "Auto-detected"
},
"highlighting": {
"title": "Code Syntax Highlighting for Text Notes",
"title": "Code Blocks",
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
"color-scheme": "Color Scheme"
},
@ -1953,5 +1951,10 @@
},
"svg": {
"export_to_png": "The diagram could not be exported to PNG."
},
"code_theme": {
"title": "Appearance",
"word_wrapping": "Word wrapping",
"color-scheme": "Color scheme"
}
}

View File

@ -1584,7 +1584,7 @@
"auto-detect-language": "Detectado automáticamente"
},
"highlighting": {
"title": "Resaltado de sintaxis de de código para Notas de Texto",
"title": "",
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
"color-scheme": "Esquema de color"
},

View File

@ -1574,7 +1574,7 @@
"auto-detect-language": "Détecté automatiquement"
},
"highlighting": {
"title": "Coloration syntaxique du code pour les notes texte",
"title": "",
"description": "Contrôle la coloration syntaxique des blocs de code à l'intérieur des notes texte, les notes de code ne seront pas affectées.",
"color-scheme": "Jeu de couleurs"
},

View File

@ -1581,7 +1581,7 @@
},
"highlighting": {
"color-scheme": "Temă de culori",
"title": "Evidențiere de sintaxă pentru notițele de tip text",
"title": "",
"description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări."
},
"code_block": {

View File

@ -1514,7 +1514,7 @@
"auto-detect-language": "自動檢測"
},
"highlighting": {
"title": "文字筆記的程式碼語法高亮",
"title": "",
"description": "控制文字筆記中程式碼塊的語法高亮,程式碼筆記不會受到影響。",
"color-scheme": "顏色方案"
},

View File

@ -135,74 +135,6 @@ declare global {
trust: boolean;
}) => void;
interface CodeMirrorOpts {
value: string;
viewportMargin: number;
indentUnit: number;
matchBrackets: boolean;
matchTags: { bothTags: boolean };
highlightSelectionMatches: {
showToken: boolean;
annotateScrollbar: boolean;
};
lineNumbers: boolean;
lineWrapping: boolean;
keyMap?: "vim" | "default";
lint?: boolean;
gutters?: string[];
tabindex?: number;
dragDrop?: boolean;
placeholder?: string;
readOnly?: boolean;
}
var CodeMirror: {
(el: HTMLElement, opts: CodeMirrorOpts): CodeMirrorInstance;
keyMap: {
default: Record<string, string>;
};
modeURL: string;
modeInfo: ModeInfo[];
findModeByMIME(mime: string): ModeInfo;
autoLoadMode(instance: CodeMirrorInstance, mode: string)
registerHelper(type: string, filter: string | null, callback: (text: string, options: object) => unknown);
Pos(line: number, col: number);
}
interface ModeInfo {
name: string;
mode: string;
mime: string;
mimes: string[];
}
interface CodeMirrorInstance {
getValue(): string;
setValue(val: string);
clearHistory();
setOption(name: string, value: string);
refresh();
focus();
getCursor(): { line: number, col: number, ch: number };
setCursor(line: number, col: number);
getSelection(): string;
lineCount(): number;
on(event: string, callback: () => void);
operation(callback: () => void);
scrollIntoView(pos: number);
doc: {
getValue(): string;
markText(
from: { line: number, ch: number } | number,
to: { line: number, ch: number } | number,
opts: {
className: string
});
setSelection(from: number, to: number);
replaceRange(text: string, from: number, to: number);
}
}
var katex: {
renderToString(text: string, opts: {
throwOnError: boolean

View File

@ -198,13 +198,8 @@ export default class FindWidget extends NoteContextAwareWidget {
let selectedText = "";
if (this.note?.type === "code" && this.noteContext) {
if (isReadOnly){
const $content = await this.noteContext.getContentElement();
selectedText = $content.find('.cm-matchhighlight').first().text();
} else {
const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection();
}
const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelectedText();
} else {
selectedText = window.getSelection()?.toString() || "";
}
@ -247,16 +242,16 @@ export default class FindWidget extends NoteContextAwareWidget {
}
async getHandler() {
if (this.note?.type === "render") {
return this.htmlHandler;
}
const readOnly = await this.noteContext?.isReadOnly();
if (readOnly) {
return this.htmlHandler;
} else {
return this.note?.type === "code" ? this.codeHandler : this.textHandler;
switch (this.note?.type) {
case "render":
return this.htmlHandler;
case "code":
return this.codeHandler;
case "text":
return this.textHandler;
default:
const readOnly = await this.noteContext?.isReadOnly();
return readOnly ? this.htmlHandler : this.textHandler;
}
}

View File

@ -17,10 +17,16 @@ interface Match {
};
}
interface SearchParameters {
searchTerm: string;
matchCase: boolean;
wholeWord: boolean;
}
export default class FindInCode {
private parent: FindWidget;
private findResult?: Match[] | null;
private searchParameters: SearchParameters | null = null;
constructor(parent: FindWidget) {
this.parent = parent;
@ -31,217 +37,54 @@ export default class FindInCode {
}
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
let findResult: Match[] | null = null;
let totalFound = 0;
let currentFound = -1;
// See https://codemirror.net/addon/search/searchcursor.js for tips
const codeEditor = await this.getCodeEditor();
if (!codeEditor) {
return { totalFound: 0, currentFound: 0 };
}
const doc = codeEditor.doc;
const text = doc.getValue();
// Clear all markers
if (this.findResult) {
codeEditor.operation(() => {
const findResult = this.findResult as Match[];
for (let i = 0; i < findResult.length; ++i) {
const marker = findResult[i];
marker.clear();
}
});
}
if (searchTerm !== "") {
searchTerm = utils.escapeRegExp(searchTerm);
// Find and highlight matches
// Find and highlight matches
// XXX Using \\b and not using the unicode flag probably doesn't
// work with non-ASCII alphabets, findAndReplace uses a more
// complicated regexp, see
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145
const wholeWordChar = wholeWord ? "\\b" : "";
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
let curLine = 0;
let curChar = 0;
let curMatch: RegExpExecArray | null = null;
findResult = [];
// All those markText take several seconds on e.g., this ~500-line
// script, batch them inside an operation, so they become
// unnoticeable. Alternatively, an overlay could be used, see
// https://codemirror.net/addon/search/match-highlighter.js ?
codeEditor.operation(() => {
for (let i = 0; i < text.length; ++i) {
// Fetch the next match if it's the first time or if past the current match start
if (curMatch == null || curMatch.index < i) {
curMatch = re.exec(text);
if (curMatch == null) {
// No more matches
break;
}
}
// Create a non-selected highlight marker for the match, the
// selected marker highlight will be done later
if (i === curMatch.index) {
let fromPos = { line: curLine, ch: curChar };
// If multiline is supported, this needs to recalculate curLine since the match may span lines
let toPos = { line: curLine, ch: curChar + curMatch[0].length };
// or css = "color: #f3"
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
findResult?.push(marker);
// Set the first match beyond the cursor as the current match
if (currentFound === -1) {
const cursorPos = codeEditor.getCursor();
if (fromPos.line > cursorPos.line || (fromPos.line === cursorPos.line && fromPos.ch >= cursorPos.ch)) {
currentFound = totalFound;
}
}
totalFound++;
}
// Do line and char position tracking
if (text[i] === "\n") {
curLine++;
curChar = 0;
} else {
curChar++;
}
}
});
}
this.findResult = findResult;
// Calculate curfound if not already, highlight it as selected
if (findResult && totalFound > 0) {
currentFound = Math.max(0, currentFound);
let marker = findResult[currentFound];
let pos = marker.find();
codeEditor.scrollIntoView(pos.to);
marker.clear();
findResult[currentFound] = doc.markText(pos.from, pos.to, { className: FIND_RESULT_SELECTED_CSS_CLASSNAME });
}
return {
totalFound,
currentFound: Math.min(currentFound + 1, totalFound)
this.searchParameters = {
searchTerm,
matchCase,
wholeWord,
};
const { totalFound, currentFound } = await codeEditor.performFind(searchTerm, matchCase, wholeWord);
return { totalFound, currentFound };
}
async findNext(direction: number, currentFound: number, nextFound: number) {
const codeEditor = await this.getCodeEditor();
if (!codeEditor || !this.findResult) {
if (!codeEditor) {
return;
}
const doc = codeEditor.doc;
//
// Dehighlight current, highlight & scrollIntoView next
//
let marker = this.findResult[currentFound];
let pos = marker.find();
marker.clear();
marker = doc.markText(pos.from, pos.to, { className: FIND_RESULT_CSS_CLASSNAME });
this.findResult[currentFound] = marker;
marker = this.findResult[nextFound];
pos = marker.find();
marker.clear();
marker = doc.markText(pos.from, pos.to, { className: FIND_RESULT_SELECTED_CSS_CLASSNAME });
this.findResult[nextFound] = marker;
codeEditor.scrollIntoView(pos.from);
codeEditor.findNext(direction, currentFound, nextFound);
}
async findBoxClosed(totalFound: number, currentFound: number) {
const codeEditor = await this.getCodeEditor();
if (codeEditor && totalFound > 0) {
const doc = codeEditor.doc;
const pos = this.findResult?.[currentFound].find();
// Note setting the selection sets the cursor to
// the end of the selection and scrolls it into
// view
if (pos) {
doc.setSelection(pos.from, pos.to);
}
// Clear all markers
codeEditor.operation(() => {
if (!this.findResult) {
return;
}
for (let i = 0; i < this.findResult.length; ++i) {
let marker = this.findResult[i];
marker.clear();
}
});
}
this.findResult = null;
codeEditor?.cleanSearch();
codeEditor?.focus();
}
async replace(replaceText: string) {
// this.findResult may be undefined and null
if (!this.findResult || this.findResult.length === 0) {
return;
}
let currentFound = -1;
this.findResult.forEach((marker, index) => {
const pos = marker.find();
if (pos) {
if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
currentFound = index;
return;
}
}
});
if (currentFound >= 0) {
let marker = this.findResult[currentFound];
let pos = marker.find();
const codeEditor = await this.getCodeEditor();
const doc = codeEditor?.doc;
if (doc) {
doc.replaceRange(replaceText, pos.from, pos.to);
}
marker.clear();
let nextFound;
if (currentFound === this.findResult.length - 1) {
nextFound = 0;
} else {
nextFound = currentFound;
}
this.findResult.splice(currentFound, 1);
if (this.findResult.length > 0) {
this.findNext(0, nextFound, nextFound);
}
}
}
async replaceAll(replaceText: string) {
if (!this.findResult || this.findResult.length === 0) {
return;
}
const codeEditor = await this.getCodeEditor();
const doc = codeEditor?.doc;
codeEditor?.operation(() => {
if (!this.findResult) {
return;
}
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound];
let pos = marker.find();
doc?.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
}
});
this.findResult = [];
await codeEditor?.replace(replaceText);
this.rerunSearch();
}
async replaceAll(replaceText: string) {
const codeEditor = await this.getCodeEditor();
await codeEditor?.replaceAll(replaceText);
this.rerunSearch();
}
private rerunSearch() {
if (this.searchParameters) {
this.performFind(
this.searchParameters.searchTerm,
this.searchParameters.matchCase,
this.searchParameters.wholeWord);
}
}
}

View File

@ -1,7 +1,11 @@
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import options from "../../services/options.js";
import { getThemeById } from "@triliumnext/codemirror";
import type FNote from "../../entities/fnote.js";
import options from "../../services/options.js";
import TypeWidget from "./type_widget.js";
import CodeMirror, { type EditorConfig } from "@triliumnext/codemirror";
import type { EventData } from "../../components/app_context.js";
export const DEFAULT_PREFIX = "default:";
/**
* An abstract {@link TypeWidget} which implements the CodeMirror editor, meant to be used as a parent for
@ -19,43 +23,27 @@ import type FNote from "../../entities/fnote.js";
export default class AbstractCodeTypeWidget extends TypeWidget {
protected $editor!: JQuery<HTMLElement>;
protected codeEditor!: CodeMirrorInstance;
protected codeEditor!: CodeMirror;
doRender() {
this.initialized = this.#initEditor();
}
async #initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
// these conflict with backward/forward navigation shortcuts
delete CodeMirror.keyMap.default["Alt-Left"];
delete CodeMirror.keyMap.default["Alt-Right"];
CodeMirror.modeURL = `${window.glob.assetPath}/node_modules/codemirror/mode/%N/%N.js`;
const jsMode = CodeMirror.modeInfo.find((mode) => mode.name === "JavaScript");
if (jsMode) {
jsMode.mimes.push(...["application/javascript;env=frontend", "application/javascript;env=backend"]);
}
const sqlMode = CodeMirror.modeInfo.find((mode) => mode.name === "SQLite");
if (sqlMode) {
sqlMode.mimes = ["text/x-sqlite", "text/x-sqlite;schema=trilium"];
}
this.codeEditor = CodeMirror(this.$editor[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: false, annotateScrollbar: false },
lineNumbers: true,
// we line wrap partly also because without it horizontal scrollbar displays only when you scroll
// all the way to the bottom of the note. With line wrap, there's no horizontal scrollbar so no problem
this.codeEditor = new CodeMirror({
parent: this.$editor[0],
lineWrapping: options.is("codeLineWrapEnabled"),
...this.getExtraOpts()
});
this.onEditorInitialized();
// Load the theme.
const themeId = options.get("codeNoteTheme");
if (themeId?.startsWith(DEFAULT_PREFIX)) {
const theme = getThemeById(themeId.substring(DEFAULT_PREFIX.length));
if (theme) {
await this.codeEditor.setTheme(theme);
}
}
}
/**
@ -64,7 +52,7 @@ export default class AbstractCodeTypeWidget extends TypeWidget {
*
* @returns the extra options to be passed to the CodeMirror constructor.
*/
getExtraOpts(): Partial<CodeMirrorOpts> {
getExtraOpts(): Partial<EditorConfig> {
return {};
}
@ -81,50 +69,68 @@ export default class AbstractCodeTypeWidget extends TypeWidget {
/**
* Must be called by the derived classes in `#doRefresh(note)` in order to react to changes.
*
* @param {*} note the note that was changed.
* @param {*} content the new content of the note.
* @param the note that was changed.
* @param new content of the note.
*/
_update(note: { mime: string }, content: string) {
// CodeMirror breaks pretty badly on null, so even though it shouldn't happen (guarded by a consistency check)
// we provide fallback
this.codeEditor.setValue(content || "");
_update(note: FNote, content: string) {
this.codeEditor.setText(content);
this.codeEditor.setMimeType(note.mime);
this.codeEditor.clearHistory();
let info = CodeMirror.findModeByMIME(note.mime);
if (!info) {
// Switch back to plain text if CodeMirror does not have a mode for whatever MIME type we're editing.
// To avoid inheriting a mode from a previously open code note.
info = CodeMirror.findModeByMIME("text/plain");
}
this.codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
}
show() {
this.$widget.show();
if (this.codeEditor) {
// show can be called before render
this.codeEditor.refresh();
}
this.#updateBackgroundColor();
}
focus() {
this.$editor.focus();
this.codeEditor.focus();
}
scrollToEnd() {
this.codeEditor.setCursor(this.codeEditor.lineCount(), 0);
this.codeEditor.scrollToEnd();
this.codeEditor.focus();
}
cleanup() {
if (this.codeEditor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {
this.codeEditor.setValue("");
this.codeEditor.setText("");
});
}
this.#updateBackgroundColor("unset");
}
async executeWithCodeEditorEvent({ resolve, ntxId }: EventData<"executeWithCodeEditor">) {
if (!this.isNoteContext(ntxId)) {
return;
}
await this.initialized;
resolve(this.codeEditor);
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("codeNoteTheme")) {
const themeId = options.get("codeNoteTheme");
if (themeId?.startsWith(DEFAULT_PREFIX)) {
const theme = getThemeById(themeId.substring(DEFAULT_PREFIX.length));
if (theme) {
await this.codeEditor.setTheme(theme);
}
this.#updateBackgroundColor();
}
}
if (loadResults.isOptionReloaded("codeLineWrapEnabled")) {
this.codeEditor.setLineWrapping(options.is("codeLineWrapEnabled"));
}
}
#updateBackgroundColor(color?: string) {
const $editorEl = $(this.codeEditor.dom);
this.$widget.closest(".scrolling-container").css("background-color", color ?? $editorEl.css("background-color"));
}
}

View File

@ -8,6 +8,7 @@ import options from "../../services/options.js";
import type SwitchSplitOrientationButton from "../floating_buttons/switch_layout_button.js";
import type { EventData } from "../../components/app_context.js";
import type OnClickButtonWidget from "../buttons/onclick_button.js";
import type { EditorConfig } from "@triliumnext/codemirror";
const TPL = /*html*/`\
<div class="note-detail-split note-detail-printable">
@ -131,7 +132,14 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
super();
this.editorTypeWidget = new EditableCodeTypeWidget();
this.editorTypeWidget.isEnabled = () => true;
this.editorTypeWidget.getExtraOpts = this.buildEditorExtraOptions;
const defaultOptions = this.editorTypeWidget.getExtraOpts();
this.editorTypeWidget.getExtraOpts = () => {
return {
...defaultOptions,
...this.buildEditorExtraOptions()
};
};
}
doRender(): void {
@ -242,7 +250,7 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
/**
* Called upon when the code editor is being initialized. Can be used to add additional options to the editor.
*/
buildEditorExtraOptions(): Partial<CodeMirrorOpts> {
buildEditorExtraOptions(): Partial<EditorConfig> {
return {
lineWrapping: false
};

View File

@ -34,7 +34,7 @@ import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_
import RibbonOptions from "./options/appearance/ribbon.js";
import MultiFactorAuthenticationOptions from './options/multi_factor_authentication.js';
import LocalizationOptions from "./options/i18n/i18n.js";
import CodeBlockOptions from "./options/appearance/code_block.js";
import CodeBlockOptions from "./options/text_notes/code_block.js";
import EditorOptions from "./options/text_notes/editor.js";
import ShareSettingsOptions from "./options/other/share_settings.js";
import AiSettingsOptions from "./options/ai_settings.js";
@ -42,8 +42,9 @@ import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "i18next";
import LanguageOptions from "./options/i18n/language.js";
import type { EventData, EventNames } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js";
import CodeTheme from "./options/code_notes/code_theme.js";
import RelatedSettings from "./options/related_settings.js";
const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable">
<style>
@ -68,11 +69,12 @@ const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printabl
<div class="note-detail-content-widget-content"></div>
</div>`;
const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_optionsTextNotes" | "_optionsCodeNotes" | "_optionsImages" | "_optionsSpellcheck" | "_optionsPassword" | "_optionsMFA" | "_optionsEtapi" | "_optionsBackup" | "_optionsSync" | "_optionsAi" | "_optionsOther" | "_optionsLocalization" | "_optionsAdvanced";
const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", (typeof NoteContextAwareWidget)[]> = {
_optionsAppearance: [
ThemeOptions,
FontsOptions,
CodeBlockOptions,
ElectronIntegrationOptions,
MaxContentWidthOptions,
RibbonOptions
@ -83,12 +85,14 @@ const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
_optionsTextNotes: [
EditorOptions,
HeadingStyleOptions,
CodeBlockOptions,
TableOfContentsOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions
],
_optionsCodeNotes: [
CodeEditorOptions,
CodeTheme,
CodeMimeTypesOptions,
CodeAutoReadOnlySizeOptions
],
@ -164,7 +168,10 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
this.$content.empty();
this.children = [];
const contentWidgets = CONTENT_WIDGETS[note.noteId];
const contentWidgets = [
...((CONTENT_WIDGETS as Record<string, typeof NoteContextAwareWidget[]>)[note.noteId]),
RelatedSettings
];
this.$content.toggleClass("options", note.noteId.startsWith("_options"));
if (contentWidgets) {

View File

@ -7,6 +7,7 @@ import AbstractCodeTypeWidget from "./abstract_code_type_widget.js";
import appContext from "../../components/app_context.js";
import type { TouchBarItem } from "../../components/touch_bar.js";
import { hasTouchBar } from "../../services/utils.js";
import type { EditorConfig } from "@triliumnext/codemirror";
const TPL = /*html*/`
<div class="note-detail-code note-detail-printable">
@ -41,19 +42,13 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
super.doRender();
}
getExtraOpts(): Partial<CodeMirrorOpts> {
getExtraOpts(): Partial<EditorConfig> {
return {
keyMap: options.is("vimKeymapEnabled") ? "vim" : "default",
lint: true,
gutters: ["CodeMirror-lint-markers"],
tabindex: 300,
dragDrop: false, // with true the editor inlines dropped files which is not what we expect
placeholder: t("editable_code.placeholder")
};
}
onEditorInitialized() {
this.codeEditor.on("change", () => this.spacedUpdate.scheduleUpdate());
placeholder: t("editable_code.placeholder"),
vimKeybindings: options.is("vimKeymapEnabled"),
onContentChanged: () => this.spacedUpdate.scheduleUpdate(),
tabIndex: 300
}
}
async doRefresh(note: FNote) {
@ -72,20 +67,10 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
getData() {
return {
content: this.codeEditor.getValue()
content: this.codeEditor.getText()
};
}
async executeWithCodeEditorEvent({ resolve, ntxId }: EventData<"executeWithCodeEditor">) {
if (!this.isNoteContext(ntxId)) {
return;
}
await this.initialized;
resolve(this.codeEditor);
}
buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
const items: TouchBarItem[] = [];
const note = this.note;

View File

@ -1,3 +1,4 @@
import type { EditorConfig } from "@triliumnext/codemirror";
import { getMermaidConfig, loadElkIfNeeded, postprocessMermaidSvg } from "../../services/mermaid.js";
import AbstractSvgSplitTypeWidget from "./abstract_svg_split_type_widget.js";

View File

@ -43,7 +43,8 @@ const TPL = /*html*/`
</label>
</div>
</div>
</div>`;
</div>
`;
interface Theme {
val: string;

View File

@ -10,30 +10,19 @@ const TPL = /*html*/`
${t("vim_key_bindings.use_vim_keybindings_in_code_notes")}
</label>
<p class="form-text">${t("vim_key_bindings.enable_vim_keybindings")}</p>
<label class="tn-checkbox">
<input type="checkbox" class="line-wrap-enabled form-check-input">
${t("wrap_lines.wrap_lines_in_code_notes")}
</label>
<p class="form-text">${t("wrap_lines.enable_line_wrap")}</p>
</div>`;
export default class CodeEditorOptions extends OptionsWidget {
private $vimKeymapEnabled!: JQuery<HTMLElement>;
private $codeLineWrapEnabled!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
this.$vimKeymapEnabled = this.$widget.find(".vim-keymap-enabled");
this.$vimKeymapEnabled.on("change", () => this.updateCheckboxOption("vimKeymapEnabled", this.$vimKeymapEnabled));
this.$codeLineWrapEnabled = this.$widget.find(".line-wrap-enabled");
this.$codeLineWrapEnabled.on("change", () => this.updateCheckboxOption("codeLineWrapEnabled", this.$codeLineWrapEnabled));
}
async optionsLoaded(options: OptionMap) {
this.setCheckboxState(this.$vimKeymapEnabled, options.vimKeymapEnabled);
this.setCheckboxState(this.$codeLineWrapEnabled, options.codeLineWrapEnabled);
}
}

View File

@ -0,0 +1,173 @@
import type { OptionMap } from "@triliumnext/commons";
import OptionsWidget from "../options_widget";
import server from "../../../../services/server";
import CodeMirror, { getThemeById } from "@triliumnext/codemirror";
import { DEFAULT_PREFIX } from "../../abstract_code_type_widget";
import { t } from "../../../../services/i18n";
import { ColorThemes } from "@triliumnext/codemirror";
// TODO: Deduplicate
interface Theme {
title: string;
val: string;
}
type Response = Theme[];
const SAMPLE_MIME = "application/typescript";
const SAMPLE_CODE = `\
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter } from "@codemirror/language";
import { Compartment, EditorState, type Extension } from "@codemirror/state";
import { highlightSelectionMatches } from "@codemirror/search";
import { vim } from "@replit/codemirror-vim";
import byMimeType from "./syntax_highlighting.js";
import smartIndentWithTab from "./extensions/custom_tab.js";
import type { ThemeDefinition } from "./color_themes.js";
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
type ContentChangedListener = () => void;
export interface EditorConfig {
parent: HTMLElement;
placeholder?: string;
lineWrapping?: boolean;
vimKeybindings?: boolean;
readOnly?: boolean;
onContentChanged?: ContentChangedListener;
}
export default class CodeMirror extends EditorView {
private config: EditorConfig;
private languageCompartment: Compartment;
private historyCompartment: Compartment;
private themeCompartment: Compartment;
constructor(config: EditorConfig) {
const languageCompartment = new Compartment();
const historyCompartment = new Compartment();
const themeCompartment = new Compartment();
let extensions: Extension[] = [];
if (config.vimKeybindings) {
extensions.push(vim());
}
extensions = [
...extensions,
languageCompartment.of([]),
themeCompartment.of([
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
]),
highlightActiveLine(),
highlightSelectionMatches(),
bracketMatching(),
lineNumbers(),
foldGutter(),
indentUnit.of(" ".repeat(4)),
keymap.of([
...defaultKeymap,
...historyKeymap,
...smartIndentWithTab
])
]
super({
parent: config.parent,
extensions
});
}
}`;
const TPL = /*html*/`\
<div class="options-section">
<h4>${t("code_theme.title")}</h4>
<div class="form-group row">
<div class="col-md-6">
<label for="color-theme">${t("code_theme.color-scheme")}</label>
<select id="color-theme" class="theme-select form-select"></select>
</div>
<div class="col-md-6 side-checkbox">
<label class="form-check tn-checkbox">
<input type="checkbox" class="word-wrap form-check-input" />
${t("code_theme.word_wrapping")}
</label>
</div>
</div>
<div class="note-detail-readonly-code-content">
</div>
<style>
.options-section .note-detail-readonly-code-content {
margin: 0;
}
.options-section .note-detail-readonly-code-content .cm-editor {
height: 200px;
}
</style>
</div>
`;
export default class CodeTheme extends OptionsWidget {
private $themeSelect!: JQuery<HTMLElement>;
private $sampleEl!: JQuery<HTMLElement>;
private $lineWrapEnabled!: JQuery<HTMLElement>;
private editor?: CodeMirror;
doRender() {
this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select");
this.$themeSelect.on("change", async () => {
const newTheme = String(this.$themeSelect.val());
await server.put(`options/codeNoteTheme/${newTheme}`);
});
// Populate the list of themes.
for (const theme of ColorThemes) {
const option = $("<option>")
.attr("value", `default:${theme.id}`)
.text(theme.name);
this.$themeSelect.append(option);
}
this.$sampleEl = this.$widget.find(".note-detail-readonly-code-content");
this.$lineWrapEnabled = this.$widget.find(".word-wrap");
this.$lineWrapEnabled.on("change", () => this.updateCheckboxOption("codeLineWrapEnabled", this.$lineWrapEnabled));
}
async #setupPreview(options: OptionMap) {
if (!this.editor) {
this.editor = new CodeMirror({
parent: this.$sampleEl[0],
});
}
this.editor.setText(SAMPLE_CODE);
this.editor.setMimeType(SAMPLE_MIME);
this.editor.setLineWrapping(options.codeLineWrapEnabled === "true");
// Load the theme.
const themeId = options.codeNoteTheme;
if (themeId?.startsWith(DEFAULT_PREFIX)) {
const theme = getThemeById(themeId.substring(DEFAULT_PREFIX.length));
if (theme) {
await this.editor.setTheme(theme);
}
}
}
async optionsLoaded(options: OptionMap) {
this.$themeSelect.val(options.codeNoteTheme);
this.#setupPreview(options);
this.setCheckboxState(this.$lineWrapEnabled, options.codeLineWrapEnabled);
}
}

View File

@ -0,0 +1,72 @@
import type FNote from "../../../entities/fnote";
import type { OptionPages } from "../content_widget";
import OptionsWidget from "./options_widget";
const TPL = `\
<div class="options-section">
<h4>Related settings</h4>
<nav class="related-settings">
<li>Color scheme for code blocks in text notes</li>
<li>Color scheme for code notes</li>
</nav>
<style>
.related-settings {
padding: 0;
margin: 0;
list-style-type: none;
}
</style>
</div>
`;
interface RelatedSettingsConfig {
items: {
title: string;
targetPage: OptionPages;
}[];
}
const RELATED_SETTINGS: Record<string, RelatedSettingsConfig> = {
"_optionsAppearance": {
items: [
{
title: "Color scheme for code blocks in text notes",
targetPage: "_optionsTextNotes"
},
{
title: "Color scheme for code notes",
targetPage: "_optionsCodeNotes"
}
]
}
};
export default class RelatedSettings extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
const config = this.noteId && RELATED_SETTINGS[this.noteId];
if (!config) {
return;
}
const $relatedSettings = this.$widget.find(".related-settings");
$relatedSettings.empty();
for (const item of config.items) {
const $item = $("<li>");
const $link = $("<a>").text(item.title);
$item.append($link);
$link.attr("href", `#root/_hidden/_options/${item.targetPage}`);
$relatedSettings.append($item);
}
}
isEnabled() {
return (!!this.noteId && this.noteId in RELATED_SETTINGS);
}
}

View File

@ -25,8 +25,6 @@ const TPL = /*html*/`
<div class="options-section">
<h4>${t("highlighting.title")}</h4>
<p class="form-text">${t("highlighting.description")}</p>
<div class="form-group row">
<div class="col-md-6">
<label for="highlighting-color-scheme-select">${t("highlighting.color-scheme")}</label>
@ -41,20 +39,23 @@ const TPL = /*html*/`
</div>
</div>
<div class="form-group row">
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
</div>
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
</div>
<style>
.code-sample-wrapper {
margin-top: 1em;
}
.code-sample-wrapper pre {
margin-bottom: 0;
}
</style>
</div>
`;
// TODO: Deduplicate
interface Theme {
title: string;
val: string;

View File

@ -9,10 +9,6 @@ const TPL = /*html*/`
min-height: 50px;
position: relative;
}
.note-detail-readonly-code-content {
padding: 10px;
}
</style>
<pre class="note-detail-readonly-code-content"></pre>
@ -43,7 +39,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
this.show();
}
getExtraOpts(): Partial<CodeMirrorOpts> {
getExtraOpts() {
return {
readOnly: true
};

View File

@ -35,10 +35,13 @@
],
"references": [
{
"path": "../../packages/ckeditor5/tsconfig.lib.json"
"path": "../../packages/codemirror/tsconfig.lib.json"
},
{
"path": "../../packages/commons/tsconfig.lib.json"
},
{
"path": "../../packages/ckeditor5/tsconfig.lib.json"
}
]
}

View File

@ -4,11 +4,14 @@
"include": [],
"references": [
{
"path": "../../packages/ckeditor5"
"path": "../../packages/codemirror"
},
{
"path": "../../packages/commons"
},
{
"path": "../../packages/ckeditor5"
},
{
"path": "./tsconfig.app.json"
},

View File

@ -1,13 +1,10 @@
const child_process = require("child_process");
const fs = require("fs");
const { default: path } = require("path");
const path = require("path");
module.exports = function (filePath) {
const { WINDOWS_SIGN_EXECUTABLE } = process.env;
const stats = fs.lstatSync(filePath);
console.log(filePath, stats);
if (!WINDOWS_SIGN_EXECUTABLE) {
console.warn("[Sign] Skip signing due to missing environment variable.");
return;
@ -19,11 +16,15 @@ module.exports = function (filePath) {
fs.mkdirSync(outputDir);
}
fs.copyFileSync(sourcePath, destPath);
const command = `${WINDOWS_SIGN_EXECUTABLE} --executable "${filePath}"`;
console.log(`[Sign] ${command}`);
console.log(`[Sign] Running ${command}`);
const output = child_process.execSync(command);
console.log(`[Sign] ${output}`);
try {
child_process.execSync(command);
} catch (e) {
console.warn(`[Sign] Unable to sign ${filePath} due to:\n${e.stdout.toString("utf-8")})}`)
return;
}
console.log(`[Sign] Signed ${filePath} successfully.`);
}

View File

@ -29,7 +29,7 @@
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
},
"nixos": {
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_33 --run \"electron --version\")"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_35 --run \"electron --version\")"
}
}
},

View File

@ -7,18 +7,14 @@ test("Displays lint warnings for backend script", async ({ page, context }) => {
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint warnings");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
const codeEditor = app.currentNoteSplit.locator(".cm-editor");
// Expect two warning signs in the gutter.
await expect(codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-warning")).toHaveCount(2);
await expect(codeEditor.locator(".cm-gutter-lint .cm-lint-marker-warning")).toHaveCount(2);
// Hover over hello
await codeEditor.getByText("hello").first().hover();
await expectTooltip(page, "'hello' is defined but never used.");
// Hover over world
await codeEditor.getByText("world").first().hover();
await expectTooltip(page, "'world' is defined but never used.");
});
test("Displays lint errors for backend script", async ({ page, context }) => {
@ -27,10 +23,10 @@ test("Displays lint errors for backend script", async ({ page, context }) => {
await app.closeAllTabs();
await app.goToNoteInNewTab("Backend script with lint errors");
const codeEditor = app.currentNoteSplit.locator(".CodeMirror");
const codeEditor = app.currentNoteSplit.locator(".cm-editor");
// Expect two warning signs in the gutter.
const errorMarker = codeEditor.locator(".CodeMirror-gutter-wrapper .CodeMirror-lint-marker-error");
const errorMarker = codeEditor.locator(".cm-gutter-lint .cm-lint-marker-error");
await expect(errorMarker).toHaveCount(1);
// Hover over hello
@ -40,7 +36,7 @@ test("Displays lint errors for backend script", async ({ page, context }) => {
async function expectTooltip(page: Page, tooltip: string) {
await expect(
page.locator(".CodeMirror-lint-tooltip:visible", {
page.locator(".cm-tooltip:visible", {
hasText: tooltip
})
).toBeVisible();

View File

@ -44,12 +44,11 @@
"@types/xml2js": "0.4.14",
"autocomplete.js": "0.38.1",
"boxicons": "2.1.4",
"codemirror": "5.65.19",
"express-http-proxy": "2.1.1",
"jquery": "3.7.1",
"katex": "0.16.22",
"normalize.css": "8.0.1",
"@anthropic-ai/sdk": "0.50.3",
"@anthropic-ai/sdk": "0.50.4",
"@braintree/sanitize-url": "7.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
@ -67,7 +66,7 @@
"csrf-csrf": "3.2.2",
"dayjs": "1.11.13",
"debounce": "2.2.0",
"debug": "4.4.0",
"debug": "4.4.1",
"ejs": "3.1.10",
"electron": "36.2.0",
"electron-debug": "4.1.0",
@ -109,7 +108,7 @@
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",
"striptags": "3.2.0",
"supertest": "7.1.0",
"supertest": "7.1.1",
"swagger-jsdoc": "6.2.8",
"swagger-ui-express": "5.0.1",
"time2fa": "^1.3.0",

View File

@ -37,13 +37,22 @@
and other important Trilium data files are stored in the <a href="#root/_help_tAassRL4RSQL">data directory</a>.
If you prefer a different location, you can change it by setting the <code>TRILIUM_DATA_DIR</code> environment
variable:</p><pre><code class="language-text-x-trilium-auto">export TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data</code></pre>
<h3>Disabling / Modifying the Upload Limit</h3>
<p>If you're running into the 250MB limit imposed on the server by default,
and you'd like to increase the upload limit, you can set the <code>TRILIUM_NO_UPLOAD_LIMIT</code> environment
variable to <code>true</code> disable it completely:</p><pre><code class="language-text-x-trilium-auto">export TRILIUM_NO_UPLOAD_LIMIT=true </code></pre>
<p>Or, if you'd simply like to <em>increase</em> the upload limit size to something
beyond 250MB, you can set the <code>MAX_ALLOWED_FILE_SIZE_MB</code> environment
variable to something larger than the integer <code>250</code> (e.g. <code>450</code> in
the following example):</p><pre><code class="language-text-x-trilium-auto">export MAX_ALLOWED_FILE_SIZE_MB=450</code></pre>
<h3>Disabling Authentication</h3>
<p>If you are running Trilium on localhost only or if authentication is handled
by another component, you can disable Triliums authentication by adding
the following to <code>config.ini</code>:</p><pre><code class="language-text-x-trilium-auto">[General]
noAuthentication=true</code></pre>
<h2>Reverse Proxy Setup</h2>
<p>To configure a reverse proxy for Trilium, you can use either <strong>nginx</strong> or <strong>Apache</strong>.</p>
<p>To configure a reverse proxy for Trilium, you can use either <strong>nginx</strong> or <strong>Apache</strong>.
You can also check out the documentation stored in the Reverse proxy folder.</p>
<h3>nginx</h3>
<p>Add the following configuration to your <code>nginx</code> setup to proxy
requests to Trilium:</p><pre><code class="language-text-x-trilium-auto">location /trilium/ {

View File

@ -37,3 +37,17 @@
to manually <a href="#root/_help_s8alTXmpFR61">refresh the application</a>.</p>
<p>The list of languages is also shared with the&nbsp;<a href="#root/_help_QxEyIjRBizuC">Code blocks</a>&nbsp;feature
of&nbsp;<a href="#root/_help_iPIMuisry3hd">Text</a>&nbsp;notes.</p>
<h2>Color schemes</h2>
<p>Since Trilium 0.94.0 the colors of code notes can be customized by going&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Code Notes and looking for the <em>Appearance</em> section.</p>
<aside
class="admonition note">
<p><strong>Why are there only a few themes whereas the code block themes for text notes have a lot?</strong>
<br>The reason is that Code notes use a different technology than the one
used in Text notes, and as such there is a more limited selection of themes.
If you find a CodeMirror 6 (not 5) theme that you would like to use, let
us know and we might consider adding it to the set of default themes. There
is no possibility of adding new themes (at least for now), since the themes
are defined in JavaScript and not at CSS level.</p>
</aside>

View File

@ -25,7 +25,7 @@
</ul>
</li>
</ul>
<h2>Syntax highlighting</h2>
<h2>Syntax highlighting &amp; color schemes</h2>
<p>Since TriliumNext v0.90.12, Trilium will try to offer syntax highlighting
to the code block. Note that the syntax highlighting mechanism is slightly
different than the one in&nbsp;<a class="reference-link" href="#root/_help_6f9hih2hXXZk">Code</a>&nbsp;notes

View File

@ -22,6 +22,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
"theme",
"codeBlockTheme",
"codeBlockWordWrap",
"codeNoteTheme",
"syncServerHost",
"syncServerTimeout",
"syncProxy",

View File

@ -91,12 +91,6 @@ async function register(app: express.Application) {
app.use(`/${assetPath}/node_modules/jquery.fancytree/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery.fancytree/dist/")));
// CodeMirror
app.use(`/${assetPath}/node_modules/codemirror/lib/`, persistentCacheStatic(path.join(nodeModulesDir, "codemirror/lib/")));
app.use(`/${assetPath}/node_modules/codemirror/addon/`, persistentCacheStatic(path.join(nodeModulesDir, "codemirror/addon/")));
app.use(`/${assetPath}/node_modules/codemirror/mode/`, persistentCacheStatic(path.join(nodeModulesDir, "codemirror/mode/")));
app.use(`/${assetPath}/node_modules/codemirror/keymap/`, persistentCacheStatic(path.join(nodeModulesDir, "codemirror/keymap/")));
app.use(`/${assetPath}/node_modules/@highlightjs/cdn-assets/`, persistentCacheStatic(path.join(nodeModulesDir, "@highlightjs/cdn-assets/")));
}

View File

@ -68,6 +68,7 @@ export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "GDScript (Godot)", mime: "text/x-gdscript" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ title: "Go", mime: "text/x-go", highlightJs: "go", default: true },
@ -106,6 +107,7 @@ export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nix", mime: "text/x-nix", highlightJs: "nix" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },

View File

@ -137,6 +137,21 @@ const defaultOptions: DefaultOption[] = [
// Appearance
{ name: "splitEditorOrientation", value: "horizontal", isSynced: true },
{
name: "codeNoteTheme",
value: (optionsMap) => {
switch (optionsMap.theme) {
case "light":
case "next-light":
return "default:vs-code-light";
case "dark":
case "next-dark":
default:
return "default:vs-code-dark";
}
},
isSynced: false
},
// Internationalization
{ name: "locale", value: "en", isSynced: true },

View File

@ -22,10 +22,6 @@ function buildFilesToCopy() {
"autocomplete.js/dist",
"normalize.css/normalize.css",
"jquery.fancytree/dist",
"codemirror/lib",
"codemirror/addon",
"codemirror/mode",
"codemirror/keymap",
"@highlightjs/cdn-assets",
// Required as they are native dependencies and cannot be well bundled.

View File

@ -87,19 +87,19 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "BRhQZHgwaGyw",
"isInheritable": false,
"position": 10
},
{
"type": "label",
"name": "iconClass",
"value": "bx bx-package",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "BRhQZHgwaGyw",
"isInheritable": false,
"position": 20
}
],
"format": "markdown",

View File

@ -11,6 +11,10 @@
* The goal is not to have basic API integration, but to really let the LLM understand the notes that are part of your knowledge base.
* For more information, consult the in-app User Guide (<kbd>F1</kbd>) and look for the AI section.
* Since this is highly experimental work, the LLM chat functionality might work well or it might have quite a few quirks, please keep this in mind.
* This release brings significant updates to our libraries:
* CKEditor, used by text notes is now updated to the latest version and that brings in new features (see below) and performance improvements.
* CodeMirror, used by code notes has been upgraded to a new generation.
* Code notes now also support themes, similar to code blocks.
## 🐞 Bugfixes
@ -21,15 +25,25 @@
* Improved the text editor style, to match the TriliumNext.
* Footnotes work in image captions by @werererer
* Improvements to text notes, thanks updates to the editor (see the in-app help for more details):
* Improvements to text notes (see the in-app help for more details):
* Bookmarks, similar to HTML anchors.
* Emojis.
* [Make it show which node triggered the event when right-clicking on tree](https://github.com/TriliumNext/Notes/pull/1861) by @SiriusXT
* [Only expand/collapse the left pane of the focused window](https://github.com/TriliumNext/Notes/pull/1905) by @SiriusXT
* Code notes:
* Added the GDScript (Godot) language.
* Added the Nix language (and also in code blocks for text notes).
* Added an indentation marker.
* Note: syntax highlighting for some languages (mostly HTML-template languages such as EJS, JSP) is no longer supported due to lack of upstream support. If this is a problem, feel free to report an issue and we can see what can be done about it.
* Mermaid diagrams: basic syntax highlight (not all diagram types are supported) and code folding.
* Slight organization in Appearance settings: code block themes are now in "Text Notes", added a "Related settings" section in Appearance.
* [Added support for opening and activating a note in a new tab using Ctrl+Shift+click on notes in the launcher pane, note tree, or note images](https://github.com/TriliumNext/Notes/pull/1854) by @SiriusXT
## 📖 Documentation
* \[…\]
* Documented the new text note features: bookmarks and emojis.
* Add documentation links and updated pnpm commands to README by @perfectra1n
* Add documentation around setting the various environment variables to control upload size limit by @perfectra1n
## 🌍 Internationalization
@ -44,3 +58,4 @@
* OpenAPI documentation fixes by @FliegendeWurst
* more info on several database table by @FliegendeWurst
* CKEditor (the editor used for text notes) has been updated 7 versions, from v42 to 45.
* Read-only search refactoring by @SiriusXT

View File

@ -30,6 +30,20 @@ By default, `config.ini`, the [database](../Advanced%20Usage/Database.md), and o
export TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data
```
### Disabling / Modifying the Upload Limit
If you're running into the 250MB limit imposed on the server by default, and you'd like to increase the upload limit, you can set the `TRILIUM_NO_UPLOAD_LIMIT` environment variable to `true` disable it completely:
```
export TRILIUM_NO_UPLOAD_LIMIT=true
```
Or, if you'd simply like to _increase_ the upload limit size to something beyond 250MB, you can set the `MAX_ALLOWED_FILE_SIZE_MB` environment variable to something larger than the integer `250` (e.g. `450` in the following example):
```
export MAX_ALLOWED_FILE_SIZE_MB=450
```
### Disabling Authentication
If you are running Trilium on localhost only or if authentication is handled by another component, you can disable Triliums authentication by adding the following to `config.ini`:
@ -41,7 +55,7 @@ noAuthentication=true
## Reverse Proxy Setup
To configure a reverse proxy for Trilium, you can use either **nginx** or **Apache**.
To configure a reverse proxy for Trilium, you can use either **nginx** or **Apache**. You can also check out the documentation stored in the Reverse proxy folder.
### nginx

View File

@ -25,3 +25,11 @@ Trilium supports syntax highlighting for many languages, but by default displays
Note that the list of languages is not immediately refreshed, you'd have to manually [refresh the application](../Troubleshooting/Refreshing%20the%20application.md).
The list of languages is also shared with the [Code blocks](Text/Developer-specific%20formatting/Code%20blocks.md) feature of [Text](Text.md) notes.
## Color schemes
Since Trilium 0.94.0 the colors of code notes can be customized by going <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Options.md">Options</a> → Code Notes and looking for the _Appearance_ section.
> [!NOTE]
> **Why are there only a few themes whereas the code block themes for text notes have a lot?**
> The reason is that Code notes use a different technology than the one used in Text notes, and as such there is a more limited selection of themes. If you find a CodeMirror 6 (not 5) theme that you would like to use, let us know and we might consider adding it to the set of default themes. There is no possibility of adding new themes (at least for now), since the themes are defined in JavaScript and not at CSS level.

View File

@ -13,7 +13,7 @@ Note that this feature is meant for generally small snippets of code. For larger
* Type ` ``` ` (as in Markdown).
* Note that it's not possible to specify the language, as it will default to the last selected language.
## Syntax highlighting
## Syntax highlighting & color schemes
Since TriliumNext v0.90.12, Trilium will try to offer syntax highlighting to the code block. Note that the syntax highlighting mechanism is slightly different than the one in <a class="reference-link" href="../../Code.md">Code</a> notes as different technologies are involved.

View File

@ -89,7 +89,7 @@
"axios": "^1.6.0",
"express": "^4.21.2"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"pnpm": {
"patchedDependencies": {
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",

View File

@ -0,0 +1,7 @@
# codemirror
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build codemirror` to build the library.

View File

@ -0,0 +1,24 @@
import baseConfig from "../../eslint.config.mjs";
export default [
...baseConfig,
{
"files": [
"**/*.json"
],
"rules": {
"@nx/dependency-checks": [
"error",
{
"ignoredFiles": [
"{projectRoot}/eslint.config.{js,cjs,mjs}",
"{projectRoot}/vite.config.{js,ts,mjs,mts}"
]
}
]
},
"languageOptions": {
"parser": (await import('jsonc-eslint-parser'))
}
}
];

View File

@ -0,0 +1,66 @@
{
"name": "@triliumnext/codemirror",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"nx": {
"name": "codemirror"
},
"dependencies": {
"@codemirror/commands": "6.8.1",
"@codemirror/lang-css": "6.3.1",
"@codemirror/lang-html": "6.4.9",
"@codemirror/lang-javascript": "6.2.3",
"@codemirror/lang-json": "6.0.1",
"@codemirror/lang-markdown": "6.3.2",
"@codemirror/lang-php": "6.0.1",
"@codemirror/lang-vue": "0.1.3",
"@codemirror/lang-xml": "6.1.0",
"@codemirror/legacy-modes": "6.5.1",
"@codemirror/search": "6.5.10",
"@codemirror/view": "6.36.8",
"@fsegurai/codemirror-theme-abcdef": "6.1.4",
"@fsegurai/codemirror-theme-abyss": "6.1.4",
"@fsegurai/codemirror-theme-android-studio": "6.1.4",
"@fsegurai/codemirror-theme-andromeda": "6.1.4",
"@fsegurai/codemirror-theme-basic-dark": "6.1.4",
"@fsegurai/codemirror-theme-basic-light": "6.1.4",
"@fsegurai/codemirror-theme-forest": "6.1.4",
"@fsegurai/codemirror-theme-github-dark": "6.1.4",
"@fsegurai/codemirror-theme-github-light": "6.1.4",
"@fsegurai/codemirror-theme-gruvbox-dark": "6.1.4",
"@fsegurai/codemirror-theme-gruvbox-light": "6.1.4",
"@fsegurai/codemirror-theme-material-dark": "6.1.4",
"@fsegurai/codemirror-theme-material-light": "6.1.4",
"@fsegurai/codemirror-theme-monokai": "6.1.4",
"@fsegurai/codemirror-theme-nord": "6.1.4",
"@fsegurai/codemirror-theme-palenight": "6.1.4",
"@fsegurai/codemirror-theme-solarized-dark": "6.1.4",
"@fsegurai/codemirror-theme-solarized-light": "6.1.4",
"@fsegurai/codemirror-theme-tokyo-night-day": "6.1.4",
"@fsegurai/codemirror-theme-tokyo-night-storm": "6.1.4",
"@fsegurai/codemirror-theme-volcano": "6.1.4",
"@fsegurai/codemirror-theme-vscode-dark": "6.1.4",
"@fsegurai/codemirror-theme-vscode-light": "6.1.4",
"@replit/codemirror-indentation-markers": "6.5.3",
"@replit/codemirror-lang-nix": "6.0.1",
"@replit/codemirror-vim": "6.3.0",
"@ssddanbrown/codemirror-lang-smarty": "1.0.0",
"@ssddanbrown/codemirror-lang-twig": "1.0.0",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "9.26.0"
}
}

View File

@ -0,0 +1,4 @@
declare module "@ssddanbrown/codemirror-lang-smarty" {
import type { StreamParser } from "@codemirror/language"
export const smarty: StreamParser<unknown>;
}

View File

@ -0,0 +1,137 @@
import type { Extension } from '@codemirror/state';
export interface ThemeDefinition {
id: string;
name: string;
load(): Promise<Extension>;
}
const themes: ThemeDefinition[] = [
{
id: "abyss",
name: "Abyss",
load: async () => (await import("@fsegurai/codemirror-theme-abyss")).abyss
},
{
id: "abcdef",
name: "ABCDEF",
load: async () => (await import("@fsegurai/codemirror-theme-abcdef")).abcdef
},
{
id: "android-studio",
name: "Android Studio",
load: async () => (await import("@fsegurai/codemirror-theme-android-studio")).androidStudio
},
{
id: "andromeda",
name: "Andromeda",
load: async () => (await import("@fsegurai/codemirror-theme-andromeda")).andromeda
},
{
id: "basic-dark",
name: "Basic Dark",
load: async () => (await import("@fsegurai/codemirror-theme-basic-dark")).basicDark
},
{
id: "basic-light",
name: "Basic Light",
load: async () => (await import("@fsegurai/codemirror-theme-basic-light")).basicLight
},
{
id: "forest",
name: "Forest",
load: async () => (await import("@fsegurai/codemirror-theme-forest")).forest
},
{
id: "github-dark",
name: "GitHub Dark",
load: async () => (await import("@fsegurai/codemirror-theme-github-dark")).githubDark
},
{
id: "github-light",
name: "GitHub Light",
load: async () => (await import("@fsegurai/codemirror-theme-github-light")).githubLight
},
{
id: "gruvbox-dark",
name: "Gruvbox Dark",
load: async () => (await import("@fsegurai/codemirror-theme-gruvbox-dark")).gruvboxDark
},
{
id: "gruvbox-light",
name: "Gruvbox Light",
load: async () => (await import("@fsegurai/codemirror-theme-gruvbox-light")).gruvboxLight
},
{
id: "material-mark",
name: "Material Dark",
load: async () => (await import("@fsegurai/codemirror-theme-material-dark")).materialDark
},
{
id: "material-light",
name: "Material Light",
load: async () => (await import("@fsegurai/codemirror-theme-material-light")).materialLight
},
{
id: "monokai",
name: "Monokai",
load: async () => (await import("@fsegurai/codemirror-theme-monokai")).monokai
},
{
id: "nord",
name: "Nord",
load: async () => (await import("@fsegurai/codemirror-theme-nord")).nord
},
{
id: "palenight",
name: "Palenight",
load: async () => (await import("@fsegurai/codemirror-theme-palenight")).palenight
},
{
id: "solarized-dark",
name: "Solarized Dark",
load: async () => (await import("@fsegurai/codemirror-theme-solarized-dark")).solarizedDark
},
{
id: "solarized-light",
name: "Solarized Light",
load: async () => (await import("@fsegurai/codemirror-theme-solarized-light")).solarizedLight
},
{
id: "tokyo-night-day",
name: "Tokyo Night Day",
load: async () => (await import("@fsegurai/codemirror-theme-tokyo-night-day")).tokyoNightDay
},
{
id: "tokyo-night-storm",
name: "Tokyo Night Storm",
load: async () => (await import("@fsegurai/codemirror-theme-tokyo-night-storm")).tokyoNightStorm
},
{
id: "volcano",
name: "Volcano",
load: async () => (await import("@fsegurai/codemirror-theme-volcano")).volcano
},
{
id: "vs-code-dark",
name: "VS Code Dark",
load: async () => (await import("@fsegurai/codemirror-theme-vscode-dark")).vsCodeDark
},
{
id: "vs-code-light",
name: "VS Code Light",
load: async () => (await import("@fsegurai/codemirror-theme-vscode-light")).vsCodeLight
},
]
export function getThemeById(id: string) {
for (const theme of themes) {
if (theme.id === id) {
return theme;
}
}
return null;
}
export default themes;

View File

@ -0,0 +1,75 @@
import { indentLess, indentMore } from "@codemirror/commands";
import { EditorSelection, EditorState, type ChangeSpec } from "@codemirror/state";
import type { KeyBinding } from "@codemirror/view";
/**
* Custom key binding for indentation:
*
* - <kbd>Tab</kbd> while at the beginning of a line will indent the line.
* - <kbd>Tab</kbd> while not at the beginning of a line will insert a tab character.
* - <kbd>Tab</kbd> while not at the beginning of a line while text is selected will replace the txt with a tab character.
* - <kbd>Shift</kbd>+<kbd>Tab</kbd> will always unindent.
*/
const smartIndentWithTab: KeyBinding[] = [
{
key: "Tab",
run({ state, dispatch }) {
if (state.facet(EditorState.readOnly)) {
return false;
}
const { selection } = state;
const changes = [];
const newSelections = [];
// Step 1: Handle non-empty selections → replace with tab
if (selection.ranges.some(range => !range.empty)) {
for (let range of selection.ranges) {
changes.push({ from: range.from, to: range.to, insert: "\t" });
newSelections.push(EditorSelection.cursor(range.from + 1));
}
dispatch(
state.update({
changes,
selection: EditorSelection.create(newSelections),
scrollIntoView: true,
userEvent: "input"
})
);
return true;
}
// Step 2: Handle empty selections
for (let range of selection.ranges) {
const line = state.doc.lineAt(range.head);
const beforeCursor = state.doc.sliceString(line.from, range.head);
if (/^\s*$/.test(beforeCursor)) {
// Only whitespace before cursor → indent line
return indentMore({ state, dispatch });
} else {
// Insert tab character at cursor
changes.push({ from: range.head, to: range.head, insert: "\t" });
newSelections.push(EditorSelection.cursor(range.head + 1));
}
}
if (changes.length) {
dispatch(
state.update({
changes,
selection: EditorSelection.create(newSelections),
scrollIntoView: true,
userEvent: "input"
})
);
return true;
}
return false;
},
shift: indentLess
},
]
export default smartIndentWithTab;

View File

@ -1,7 +1,17 @@
import { lint } from "./eslint.js";
import { lint as _lint } from "./eslint.js";
import { trimIndentation } from "@triliumnext/commons";
import { describe, expect, it } from "vitest";
async function lint(code: string, mimeType: string) {
const linterData = await _lint(mimeType);
if (!("linter" in linterData)) {
return [];
}
const { linter, config } = linterData;
const result = linter.verify(code, config);
return result;
}
describe("Linter", () => {
it("reports some basic errors", async () => {
const result = await lint(trimIndentation`

View File

@ -1,4 +1,6 @@
export async function lint(code: string, mimeType: string) {
import type { Linter } from "eslint-linter-browserify";
export async function lint(mimeType: string) {
const Linter = (await import("eslint-linter-browserify")).Linter;
const js = (await import("@eslint/js"));
@ -22,7 +24,7 @@ export async function lint(code: string, mimeType: string) {
}
return new Linter().verify(code, [
const config: (Linter.LegacyConfig | Linter.Config | Linter.Config[]) = [
js.configs.recommended,
{
languageOptions: {
@ -35,6 +37,10 @@ export async function lint(code: string, mimeType: string) {
"no-unused-vars": [ "warn", { vars: "local", args: "after-used" }]
}
}
]);
];
return {
linter: new Linter(),
config
}
}

View File

@ -0,0 +1,154 @@
import { EditorView, Decoration, MatchDecorator, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { Range, RangeSet } from "@codemirror/state";
const searchMatchDecoration = Decoration.mark({ class: "cm-searchMatch" });
const activeMatchDecoration = Decoration.mark({ class: "cm-activeMatch" });
interface Match {
from: number;
to: number;
}
export class SearchHighlighter {
matches: RangeSet<Decoration>;
activeMatch?: Range<Decoration>;
currentFound: number;
totalFound: number;
matcher?: MatchDecorator;
private parsedMatches: Match[];
constructor(public view: EditorView) {
this.parsedMatches = [];
this.currentFound = 0;
this.totalFound = 0;
this.matches = RangeSet.empty;
}
searchFor(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
if (!searchTerm) {
this.matches = RangeSet.empty;
return;
}
// Escape the search term for use in RegExp
const escapedTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const wordBoundary = wholeWord ? "\\b" : "";
const flags = matchCase ? "g" : "gi";
const regex = new RegExp(`${wordBoundary}${escapedTerm}${wordBoundary}`, flags);
this.matcher = new MatchDecorator({
regexp: regex,
decoration: searchMatchDecoration,
});
this.#updateSearchData(this.view);
this.#scrollToMatchNearestSelection();
}
replaceActiveMatch(replacementText: string) {
if (!this.parsedMatches.length || this.currentFound === 0) return;
const matchIndex = this.currentFound - 1;
const match = this.parsedMatches[matchIndex];
this.view.dispatch({
changes: { from: match.from, to: match.to, insert: replacementText }
});
}
replaceAll(replacementText: string) {
if (!this.parsedMatches.length) return;
this.view.dispatch({
changes: this.parsedMatches.map(change => ({
from: change.from,
to: change.to,
insert: replacementText
}))
});
}
scrollToMatch(matchIndex: number) {
if (this.parsedMatches.length <= matchIndex) {
return;
}
const match = this.parsedMatches[matchIndex];
this.currentFound = matchIndex + 1;
this.activeMatch = activeMatchDecoration.range(match.from, match.to);
this.view.dispatch({
effects: EditorView.scrollIntoView(match.from, { y: "center" }),
scrollIntoView: true
});
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.#updateSearchData(update.view);
}
}
destroy() {
// Do nothing.
}
#updateSearchData(view: EditorView) {
if (!this.matcher) {
return;
}
const matches = this.matcher.createDeco(view);
const cursor = matches.iter();
while (cursor.value) {
this.parsedMatches.push({
from: cursor.from,
to: cursor.to
});
cursor.next();
}
this.matches = matches;
this.totalFound = this.parsedMatches.length;
}
#scrollToMatchNearestSelection() {
const cursorPos = this.view.state.selection.main.head;
let index = 0;
for (const match of this.parsedMatches) {
if (match.from >= cursorPos) {
this.scrollToMatch(index);
return;
}
index++;
}
}
static deco = (v: SearchHighlighter) => v.matches;
}
export function createSearchHighlighter() {
return ViewPlugin.fromClass(SearchHighlighter, {
decorations: v => {
if (v.activeMatch) {
return v.matches.update({ add: [v.activeMatch] });
} else {
return v.matches;
}
},
provide: (plugin) => plugin
});
}
export const searchMatchHighlightTheme = EditorView.baseTheme({
".cm-searchMatch": {
backgroundColor: "rgba(255, 255, 0, 0.4)",
borderRadius: "2px"
},
".cm-activeMatch": {
backgroundColor: "rgba(255, 165, 0, 0.6)",
borderRadius: "2px",
outline: "2px solid orange"
}
});

View File

@ -0,0 +1,237 @@
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
import { EditorView, highlightActiveLine, keymap, lineNumbers, placeholder, ViewPlugin, ViewUpdate, type EditorViewConfig } from "@codemirror/view";
import { defaultHighlightStyle, StreamLanguage, syntaxHighlighting, indentUnit, bracketMatching, foldGutter } from "@codemirror/language";
import { Compartment, EditorSelection, EditorState, type Extension } from "@codemirror/state";
import { highlightSelectionMatches } from "@codemirror/search";
import { vim } from "@replit/codemirror-vim";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
import byMimeType from "./syntax_highlighting.js";
import smartIndentWithTab from "./extensions/custom_tab.js";
import type { ThemeDefinition } from "./color_themes.js";
import { createSearchHighlighter, SearchHighlighter, searchMatchHighlightTheme } from "./find_replace.js";
export { default as ColorThemes, type ThemeDefinition, getThemeById } from "./color_themes.js";
type ContentChangedListener = () => void;
export interface EditorConfig {
parent: HTMLElement;
placeholder?: string;
lineWrapping?: boolean;
vimKeybindings?: boolean;
readOnly?: boolean;
tabIndex?: number;
onContentChanged?: ContentChangedListener;
}
export default class CodeMirror extends EditorView {
private config: EditorConfig;
private languageCompartment: Compartment;
private historyCompartment: Compartment;
private themeCompartment: Compartment;
private lineWrappingCompartment: Compartment;
private searchHighlightCompartment: Compartment;
private searchPlugin?: SearchHighlighter | null;
constructor(config: EditorConfig) {
const languageCompartment = new Compartment();
const historyCompartment = new Compartment();
const themeCompartment = new Compartment();
const lineWrappingCompartment = new Compartment();
const searchHighlightCompartment = new Compartment();
let extensions: Extension[] = [];
if (config.vimKeybindings) {
extensions.push(vim());
}
extensions = [
...extensions,
languageCompartment.of([]),
lineWrappingCompartment.of(config.lineWrapping ? EditorView.lineWrapping : []),
themeCompartment.of([
syntaxHighlighting(defaultHighlightStyle, { fallback: true })
]),
searchMatchHighlightTheme,
searchHighlightCompartment.of([]),
highlightActiveLine(),
highlightSelectionMatches(),
bracketMatching(),
lineNumbers(),
foldGutter(),
indentationMarkers(),
indentUnit.of(" ".repeat(4)),
keymap.of([
...defaultKeymap,
...historyKeymap,
...smartIndentWithTab
])
]
if (!config.readOnly) {
// Logic specific to editable notes
if (config.placeholder) {
extensions.push(placeholder(config.placeholder));
}
if (config.onContentChanged) {
extensions.push(EditorView.updateListener.of((v) => this.#onDocumentUpdated(v)));
}
extensions.push(historyCompartment.of(history()));
} else {
// Logic specific to read-only notes
extensions.push(EditorState.readOnly.of(true));
}
super({
parent: config.parent,
extensions
});
if (config.tabIndex) {
this.dom.tabIndex = config.tabIndex;
}
this.config = config;
this.languageCompartment = languageCompartment;
this.historyCompartment = historyCompartment;
this.themeCompartment = themeCompartment;
this.lineWrappingCompartment = lineWrappingCompartment;
this.searchHighlightCompartment = searchHighlightCompartment;
}
#onDocumentUpdated(v: ViewUpdate) {
if (v.docChanged) {
this.config.onContentChanged?.();
}
}
getText() {
return this.state.doc.toString();
}
/**
* Returns the currently selected text.
*
* If there are multiple selections, all of them will be concatenated.
*/
getSelectedText() {
return this.state.selection.ranges
.map((range) => this.state.sliceDoc(range.from, range.to))
.join("");
}
setText(content: string) {
this.dispatch({
changes: {
from: 0,
to: this.state.doc.length,
insert: content || "",
}
})
}
async setTheme(theme: ThemeDefinition) {
const extension = await theme.load();
this.dispatch({
effects: [ this.themeCompartment.reconfigure([ extension ]) ]
});
}
setLineWrapping(wrapping: boolean) {
this.dispatch({
effects: [ this.lineWrappingCompartment.reconfigure(wrapping ? EditorView.lineWrapping : []) ]
});
}
/**
* Clears the history of undo/redo. Generally useful when changing to a new document.
*/
clearHistory() {
if (this.config.readOnly) {
return;
}
this.dispatch({
effects: [ this.historyCompartment.reconfigure([]) ]
});
this.dispatch({
effects: [ this.historyCompartment.reconfigure(history())]
});
}
scrollToEnd() {
const endPos = this.state.doc.length;
this.dispatch({
selection: EditorSelection.cursor(endPos),
effects: EditorView.scrollIntoView(endPos, { y: "end" }),
scrollIntoView: true
});
}
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
const plugin = createSearchHighlighter();
this.dispatch({
effects: this.searchHighlightCompartment.reconfigure(plugin)
});
// Wait for the plugin to activate in the next render cycle
await new Promise(requestAnimationFrame);
const instance = this.plugin(plugin);
instance?.searchFor(searchTerm, matchCase, wholeWord);
this.searchPlugin = instance;
return {
totalFound: instance?.totalFound ?? 0,
currentFound: instance?.currentFound ?? 0
}
}
async findNext(direction: number, currentFound: number, nextFound: number) {
this.searchPlugin?.scrollToMatch(nextFound);
}
async replace(replaceText: string) {
this.searchPlugin?.replaceActiveMatch(replaceText);
}
async replaceAll(replaceText: string) {
this.searchPlugin?.replaceAll(replaceText);
}
cleanSearch() {
if (this.searchPlugin) {
this.dispatch({
effects: this.searchHighlightCompartment.reconfigure([])
});
this.searchPlugin = null;
}
}
async setMimeType(mime: string) {
let newExtension: Extension[] = [];
const correspondingSyntax = byMimeType[mime];
if (correspondingSyntax) {
const resolvedSyntax = await correspondingSyntax();
if ("token" in resolvedSyntax) {
const extension = StreamLanguage.define(resolvedSyntax);
newExtension.push(extension);
} else if (Array.isArray(resolvedSyntax)) {
newExtension = [ ...newExtension, ...resolvedSyntax ];
} else {
newExtension.push(resolvedSyntax);
}
}
this.dispatch({
effects: this.languageCompartment.reconfigure(newExtension)
});
}
}

View File

@ -1,51 +1,48 @@
// Source: https://github.com/deathau/cm-editor-syntax-highlight-obsidian/issues/27#issuecomment-1340586596
(() => {
var varsAndArgsRegex = /(%[0-9]|%~\S+|%\S+%)/;
/**
* @module
*
* Ported to CodeMirror 6 from https://github.com/deathau/cm-editor-syntax-highlight-obsidian/issues/27#issuecomment-1340586596
*/
CodeMirror.defineSimpleMode("batch", {
start: [
{ //comment
import { simpleMode } from "@codemirror/legacy-modes/mode/simple-mode";
const varsAndArgsRegex = /(%[0-9]|%~\S+|%\S+%)/;
export const batch = simpleMode({
start: [
{ //comment
regex: /(rem|::)(?:\s.*|$)/i,
token: "comment",
sol: true
},
{ //echo
},
{ //echo
regex: /(@echo|echo)/i,
token: "builtin",
sol: true,
next: "echo"
},
{ //commands
},
{ //commands
regex: /(?:\s|^)(assoc|aux|break|call|cd|chcp|chdir|choice|cls|cmdextversion|color|com1|com2|com3|com4|com|con|copy|country|ctty|date|defined|del|dir|do|dpath|else|endlocal|erase|errorlevel|exist|exit|for|ftype|goto|if|in|loadfix|loadhigh|lpt|lpt1|lpt2|lpt3|lpt4|md|mkdir|move|not|nul|path|pause|popd|prn|prompt|pushd|rd|rename|ren|rmdir|setlocal|set|shift|start|time|title|type|verify|ver|vol)(?:\s|$)/i,
token: "builtin"
},
{ //variables and arguments
},
{ //variables and arguments
regex: varsAndArgsRegex,
token: "variable-2"
},
{ //label
},
{ //label
regex: /\s*:.*/,
token: "string",
sol: true
}
],
echo: [
{ //highlight variables and arguments in echo command
}
],
echo: [
{ //highlight variables and arguments in echo command
regex: varsAndArgsRegex,
token: "variable-2"
},
{ //go back to start state at end of line
},
{ //go back to start state at end of line
regex: /.$/,
next: "start"
}
]
});
CodeMirror.defineMIME("application/x-bat", "batch");
CodeMirror.modeInfo.push({
ext: [ "bat", "cmd" ],
mime: "application/x-bat",
mode: "batch",
name: "Batch file"
});
})();
}
]
});

View File

@ -0,0 +1,37 @@
/**
* @module
*
* Ported to CodeMirror 6 from https://github.com/RobTheFiveNine/obsidian-gdscript/blob/main/src/main.js
*/
import { simpleMode } from "@codemirror/legacy-modes/mode/simple-mode";
export const gdscript = simpleMode({
start: [
{ regex: /\b0x[0-9a-f]+\b/i, token: "number" },
{ regex: /\b-?\d+\b/, token: "number" },
{ regex: /#.+/, token: 'comment' },
{ regex: /\s*(@onready|@export)\b/, token: 'keyword' },
{ regex: /\b(?:and|as|assert|await|break|breakpoint|const|continue|elif|else|enum|for|if|in|is|master|mastersync|match|not|null|or|pass|preload|puppet|puppetsync|remote|remotesync|return|self|setget|static|tool|var|while|yield)\b/, token: 'keyword' },
{ regex: /[()\[\]{},]/, token: "meta" },
// The words following func, class_name and class should be highlighted as attributes,
// so push onto the definition stack
{ regex: /\b(func|class_name|class|extends|signal)\b/, token: "keyword", push: "definition" },
{ regex: /@?(?:("|')(?:(?!\1)[^\n\\]|\\[\s\S])*\1(?!"|')|"""(?:[^\\]|\\[\s\S])*?""")/, token: "string" },
{ regex: /\$[\w\/]+\b/, token: 'variable' },
{ regex: /\:[\s]*$/, token: 'operator' },
{ regex: /\:[ ]*/, token: 'meta', push: 'var_type' },
{ regex: /\->[ ]*/, token: 'operator', push: 'definition' },
{ regex: /\+|\*|-|\/|:=|>|<|\^|&|\||%|~|=/, token: "operator" },
{ regex: /\b(?:false|true)\b/, token: 'number' },
{ regex: /\b[A-Z][A-Z_\d]*\b/, token: 'operator' },
],
var_type: [
{ regex: /(\w+)/, token: 'attribute', pop: true },
],
definition: [
{ regex: /(\w+)/, token: "attribute", pop: true }
]
});

View File

@ -0,0 +1,197 @@
import { LanguageSupport, type StreamParser } from "@codemirror/language";
import {linter as linterExtension, lintGutter } from "@codemirror/lint";
import type { Extension } from "@codemirror/state";
async function buildJavaScript(mimeType: string) {
const { javascript, esLint } = await import('@codemirror/lang-javascript');
const lint = (await import("./extensions/eslint.js")).lint;
const extensions: Extension[] = [ javascript() ];
const result = await lint(mimeType);
if ("linter" in result) {
const { linter, config } = result;
extensions.push(linterExtension(esLint(linter, config)));
extensions.push(lintGutter())
}
return extensions;
}
async function buildMermaid() {
const { mermaid, foldByIndent } = (await import('codemirror-lang-mermaid'));
return [ mermaid(), foldByIndent() ];
}
const byMimeType: Record<string, (() => Promise<StreamParser<unknown> | LanguageSupport | Extension[]>) | null> = {
"text/plain": null,
"application/dart": async () => (await import('@codemirror/legacy-modes/mode/clike')).dart,
"application/edn": async () => (await import('@codemirror/legacy-modes/mode/clojure')).clojure,
"application/javascript;env=backend": async () => buildJavaScript("application/javascript;env=backend"),
"application/javascript;env=frontend": async () => buildJavaScript("application/javascript;env=frontend"),
"application/json": async () => ((await import('@codemirror/lang-json')).json()),
"application/ld+json": async () => (await import('@codemirror/legacy-modes/mode/javascript')).jsonld,
"application/mbox": async () => (await import('@codemirror/legacy-modes/mode/mbox')).mbox,
"application/n-triples": async () => (await import('@codemirror/legacy-modes/mode/ntriples')).ntriples,
"application/pgp": async () => (await import('@codemirror/legacy-modes/mode/asciiarmor')).asciiArmor,
"application/sieve": async () => (await import('@codemirror/legacy-modes/mode/sieve')).sieve,
"application/sparql-query": async () => (await import('@codemirror/legacy-modes/mode/sparql')).sparql,
"application/typescript": async () => (await import('@codemirror/lang-javascript')).javascript({ typescript: true }),
"application/x-aspx": null,
"application/x-bat": async () => (await import("./languages/batch.js")).batch,
"application/x-cypher-query": async () => (await import('@codemirror/legacy-modes/mode/cypher')).cypher,
"application/x-ejs": null,
"application/x-erb": null,
"application/x-jsp": null,
"application/x-powershell": async () => (await import('@codemirror/legacy-modes/mode/powershell')).powerShell,
"application/xml-dtd": async () => (await import('@codemirror/legacy-modes/mode/xml')).xml,
"application/xquery": async () => (await import('@codemirror/legacy-modes/mode/xquery')).xQuery,
"message/http": async () => (await import('@codemirror/legacy-modes/mode/http')).http,
"text/apl": async () => (await import('@codemirror/legacy-modes/mode/apl')).apl,
"text/coffeescript": async () => (await import('@codemirror/legacy-modes/mode/coffeescript')).coffeeScript,
"text/css": async () => (await import('@codemirror/lang-css')).css(),
"text/html": async () => (await import('@codemirror/lang-html')).html(),
"text/jinja2": async () => (await import('@codemirror/legacy-modes/mode/jinja2')).jinja2,
"text/jsx": async () => (await import('@codemirror/lang-javascript')).javascript({ jsx: true }),
"text/mirc": async () => (await import('@codemirror/legacy-modes/mode/mirc')).mirc,
"text/tiki": async () => (await import('@codemirror/legacy-modes/mode/tiki')).tiki,
"text/troff": async () => (await import('@codemirror/legacy-modes/mode/troff')).troff,
"text/turtle": async () => (await import('@codemirror/legacy-modes/mode/turtle')).turtle,
"text/typescript-jsx": async () => (await import('@codemirror/lang-javascript')).javascript({ typescript: true, jsx: true }),
"text/vbscript": async () => (await import('@codemirror/legacy-modes/mode/vbscript')).vbScript,
"text/velocity": async () => (await import('@codemirror/legacy-modes/mode/velocity')).velocity,
"text/vnd.mermaid": async () => buildMermaid(),
"text/mermaid": async () => buildMermaid(),
"text/x-asm-mips": null,
"text/x-asterisk": async () => (await import('@codemirror/legacy-modes/mode/asterisk')).asterisk,
"text/x-brainfuck": async () => (await import('@codemirror/legacy-modes/mode/brainfuck')).brainfuck,
"text/x-c++src": async () => (await import('@codemirror/legacy-modes/mode/clike')).cpp,
"text/x-cassandra": async () => (await import('@codemirror/legacy-modes/mode/sql')).cassandra,
"text/x-clojure": async () => (await import('@codemirror/legacy-modes/mode/clojure')).clojure,
"text/x-clojurescript": async () => (await import('@codemirror/legacy-modes/mode/clojure')).clojure,
"text/x-cmake": async () => (await import('@codemirror/legacy-modes/mode/cmake')).cmake,
"text/x-cobol": async () => (await import('@codemirror/legacy-modes/mode/cobol')).cobol,
"text/x-common-lisp": async () => (await import('@codemirror/legacy-modes/mode/commonlisp')).commonLisp,
"text/x-crystal": async () => (await import('@codemirror/legacy-modes/mode/crystal')).crystal,
"text/x-csharp": async () => (await import('@codemirror/legacy-modes/mode/clike')).csharp,
"text/x-csrc": async () => (await import('@codemirror/legacy-modes/mode/clike')).c,
"text/x-cython": async () => (await import('@codemirror/legacy-modes/mode/python')).cython,
"text/x-d": async () => (await import('@codemirror/legacy-modes/mode/d')).d,
"text/x-diff": async () => (await import('@codemirror/legacy-modes/mode/diff')).diff,
"text/x-django": null,
"text/x-dockerfile": async () => (await import('@codemirror/legacy-modes/mode/dockerfile')).dockerFile,
"text/x-dylan": async () => (await import('@codemirror/legacy-modes/mode/dylan')).dylan,
"text/x-ebnf": async () => (await import('@codemirror/legacy-modes/mode/ebnf')).ebnf,
"text/x-ecl": async () => (await import('@codemirror/legacy-modes/mode/ecl')).ecl,
"text/x-eiffel": async () => (await import('@codemirror/legacy-modes/mode/eiffel')).eiffel,
"text/x-elm": async () => (await import('@codemirror/legacy-modes/mode/elm')).elm,
"text/x-erlang": async () => (await import('@codemirror/legacy-modes/mode/erlang')).erlang,
"text/x-esper": async () => (await import('@codemirror/legacy-modes/mode/sql')).esper,
"text/x-factor": async () => (await import('@codemirror/legacy-modes/mode/factor')).factor,
"text/x-fcl": async () => (await import('@codemirror/legacy-modes/mode/fcl')).fcl,
"text/x-feature": async () => (await import('@codemirror/legacy-modes/mode/gherkin')).gherkin,
"text/x-forth": async () => (await import('@codemirror/legacy-modes/mode/forth')).forth,
"text/x-fortran": async () => (await import('@codemirror/legacy-modes/mode/fortran')).fortran,
"text/x-fsharp": async () => (await import('@codemirror/legacy-modes/mode/mllike')).fSharp,
"text/x-gas": async () => (await import('@codemirror/legacy-modes/mode/gas')).gas,
"text/x-gdscript": async () => (await import('./languages/gdscript.js')).gdscript,
"text/x-gfm": async () => {
const { markdown, markdownLanguage } = (await import('@codemirror/lang-markdown'));
return markdown({
base: markdownLanguage
});
},
"text/x-go": async () => (await import('@codemirror/legacy-modes/mode/go')).go,
"text/x-groovy": async () => (await import('@codemirror/legacy-modes/mode/groovy')).groovy,
"text/x-gss": async () => (await import('@codemirror/legacy-modes/mode/css')).gss,
"text/x-haml": null,
"text/x-haskell": async () => (await import('@codemirror/legacy-modes/mode/haskell')).haskell,
"text/x-haxe": async () => (await import('@codemirror/legacy-modes/mode/haxe')).haxe,
"text/x-hcl": async () => (await import('codemirror-lang-hcl')).hcl(),
"text/x-hxml": async () => (await import('@codemirror/legacy-modes/mode/haxe')).hxml,
"text/x-idl": async () => (await import('@codemirror/legacy-modes/mode/idl')).idl,
"text/x-java": async () => (await import('@codemirror/legacy-modes/mode/clike')).java,
"text/x-julia": async () => (await import('@codemirror/legacy-modes/mode/julia')).julia,
"text/x-kotlin": async () => (await import('@codemirror/legacy-modes/mode/clike')).kotlin,
"text/x-latex": async () => (await import('@codemirror/legacy-modes/mode/stex')).stex,
"text/x-less": async () => (await import('@codemirror/legacy-modes/mode/css')).less,
"text/x-literate-haskell": null,
"text/x-livescript": async () => (await import('@codemirror/legacy-modes/mode/livescript')).liveScript,
"text/x-lua": async () => (await import('@codemirror/legacy-modes/mode/lua')).lua,
"text/x-mariadb": async () => (await import('@codemirror/legacy-modes/mode/sql')).sqlite,
"text/x-markdown": async () => ((await import('@codemirror/lang-markdown')).markdown()),
"text/x-mathematica": async () => (await import('@codemirror/legacy-modes/mode/mathematica')).mathematica,
"text/x-modelica": async () => (await import('@codemirror/legacy-modes/mode/modelica')).modelica,
"text/x-mscgen": async () => (await import('@codemirror/legacy-modes/mode/mscgen')).mscgen,
"text/x-msgenny": async () => (await import('@codemirror/legacy-modes/mode/mscgen')).msgenny,
"text/x-mssql": async () => (await import('@codemirror/legacy-modes/mode/sql')).msSQL,
"text/x-mumps": async () => (await import('@codemirror/legacy-modes/mode/mumps')).mumps,
"text/x-mysql": async () => (await import('@codemirror/legacy-modes/mode/sql')).mySQL,
"text/x-nix": async () => (await import('@replit/codemirror-lang-nix')).nix(),
"text/x-nginx-conf": async () => (await import('@codemirror/legacy-modes/mode/nginx')).nginx,
"text/x-nsis": async () => (await import('@codemirror/legacy-modes/mode/nsis')).nsis,
"text/x-objectivec": async () => (await import('@codemirror/legacy-modes/mode/clike')).objectiveC,
"text/x-ocaml": async () => (await import('@codemirror/legacy-modes/mode/mllike')).oCaml,
"text/x-octave": async () => (await import('@codemirror/legacy-modes/mode/octave')).octave,
"text/x-oz": async () => (await import('@codemirror/legacy-modes/mode/oz')).oz,
"text/x-pascal": async () => (await import('@codemirror/legacy-modes/mode/pascal')).pascal,
"text/x-perl": async () => (await import('@codemirror/legacy-modes/mode/perl')).perl,
"text/x-pgsql": async () => (await import('@codemirror/legacy-modes/mode/sql')).pgSQL,
"text/x-php": async () => ((await import('@codemirror/lang-php')).php()),
"text/x-pig": async () => (await import('@codemirror/legacy-modes/mode/pig')).pig,
"text/x-plsql": async () => (await import('@codemirror/legacy-modes/mode/sql')).plSQL,
"text/x-properties": async () => (await import('@codemirror/legacy-modes/mode/properties')).properties,
"text/x-protobuf": async () => (await import('@codemirror/legacy-modes/mode/protobuf')).protobuf,
"text/x-pug": async () => (await import('@codemirror/legacy-modes/mode/pug')).pug,
"text/x-puppet": async () => (await import('@codemirror/legacy-modes/mode/puppet')).puppet,
"text/x-python": async () => (await import('@codemirror/legacy-modes/mode/python')).python,
"text/x-q": async () => (await import('@codemirror/legacy-modes/mode/q')).q,
"text/x-rpm-changes": async () => (await import('@codemirror/legacy-modes/mode/rpm')).rpmChanges,
"text/x-rpm-spec": async () => (await import('@codemirror/legacy-modes/mode/rpm')).rpmSpec,
"text/x-rsrc": async () => (await import('@codemirror/legacy-modes/mode/r')).r,
"text/x-rst": null,
"text/x-ruby": async () => (await import('@codemirror/legacy-modes/mode/ruby')).ruby,
"text/x-rustsrc": async () => (await import('@codemirror/legacy-modes/mode/rust')).rust,
"text/x-sas": async () => (await import('@codemirror/legacy-modes/mode/sas')).sas,
"text/x-sass": async () => (await import('@codemirror/legacy-modes/mode/sass')).sass,
"text/x-scala": async () => (await import('@codemirror/legacy-modes/mode/clike')).scala,
"text/x-scheme": async () => (await import('@codemirror/legacy-modes/mode/scheme')).scheme,
"text/x-scss": async () => (await import('@codemirror/legacy-modes/mode/css')).sCSS,
"text/x-sh": async () => (await import('@codemirror/legacy-modes/mode/shell')).shell,
"text/x-slim": null,
"text/x-smarty": async () => ((await import('@ssddanbrown/codemirror-lang-smarty')).smarty),
"text/x-sml": async () => (await import('@codemirror/legacy-modes/mode/mllike')).sml,
"text/x-solr": async () => (await import('@codemirror/legacy-modes/mode/solr')).solr,
"text/x-soy": null,
"text/x-spreadsheet": async () => (await import('@codemirror/legacy-modes/mode/spreadsheet')).spreadsheet,
"text/x-sql": async () => (await import('@codemirror/legacy-modes/mode/sql')).mySQL,
"text/x-sqlite;schema=trilium": async () => (await import('@codemirror/legacy-modes/mode/sql')).sqlite,
"text/x-sqlite": async () => (await import('@codemirror/legacy-modes/mode/sql')).sqlite,
"text/x-squirrel": async () => (await import('@codemirror/legacy-modes/mode/clike')).squirrel,
"text/x-stex": async () => (await import('@codemirror/legacy-modes/mode/stex')).stex,
"text/x-stsrc": async () => (await import('@codemirror/legacy-modes/mode/smalltalk')).smalltalk,
"text/x-styl": async () => (await import('@codemirror/legacy-modes/mode/stylus')).stylus,
"text/x-swift": async () => (await import('@codemirror/legacy-modes/mode/swift')).swift,
"text/x-systemverilog": async () => (await import('@codemirror/legacy-modes/mode/verilog')).verilog,
"text/x-tcl": async () => (await import('@codemirror/legacy-modes/mode/tcl')).tcl,
"text/x-textile": async () => (await import('@codemirror/legacy-modes/mode/textile')).textile,
"text/x-tiddlywiki": async () => (await import('@codemirror/legacy-modes/mode/tiddlywiki')).tiddlyWiki,
"text/x-toml": async () => (await import('@codemirror/legacy-modes/mode/toml')).toml,
"text/x-tornado": null,
"text/x-ttcn-asn": async () => (await import('@codemirror/legacy-modes/mode/ttcn')).ttcn,
"text/x-ttcn-cfg": async () => (await import('@codemirror/legacy-modes/mode/ttcn-cfg')).ttcnCfg,
"text/x-ttcn": async () => (await import('@codemirror/legacy-modes/mode/ttcn')).ttcn,
"text/x-twig": async () => ((await import('@ssddanbrown/codemirror-lang-twig')).twig()),
"text/x-vb": async () => (await import('@codemirror/legacy-modes/mode/vb')).vb,
"text/x-verilog": async () => (await import('@codemirror/legacy-modes/mode/verilog')).verilog,
"text/x-vhdl": async () => (await import('@codemirror/legacy-modes/mode/vhdl')).vhdl,
"text/x-vue": async () => ((await import('@codemirror/lang-vue')).vue()),
"text/x-webidl": async () => (await import('@codemirror/legacy-modes/mode/webidl')).webIDL,
"text/x-xu": async () => (await import('@codemirror/legacy-modes/mode/mscgen')).xu,
"text/x-yacas": async () => (await import('@codemirror/legacy-modes/mode/yacas')).yacas,
"text/x-yaml": async () => (await import('@codemirror/legacy-modes/mode/yaml')).yaml,
"text/x-z80": async () => (await import('@codemirror/legacy-modes/mode/z80')).z80,
"text/xml": async () => (await import('@codemirror/lang-xml')).xml()
}
export default byMimeType;

View File

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "../commons"
},
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,42 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "dist",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo",
"emitDeclarationOnly": true,
"forceConsistentCasingInFileNames": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"lib": [
"DOM"
],
"types": [
"node",
"vite/client"
]
},
"include": [
"src/**/*.ts"
],
"references": [
{
"path": "../commons/tsconfig.lib.json"
}
],
"exclude": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
]
}

View File

@ -0,0 +1,28 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
]
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,37 @@
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/packages/codemirror',
plugins: [dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') }), ],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: 'codemirror',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: []
},
},
}));

View File

@ -91,6 +91,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
// Appearance
splitEditorOrientation: "horziontal" | "vertical";
codeNoteTheme: string;
initialized: boolean;
isPasswordSet: boolean;

2056
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -50,6 +50,9 @@
},
{
"path": "./packages/ckeditor5-math"
},
{
"path": "./packages/codemirror"
}
]
}