diff --git a/README.md b/README.md index 496ce506b..9f7fba27a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/apps/client/package.json b/apps/client/package.json index ca4953cf1..8a98e47f1 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -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" diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 81eae41e1..dd2391b67 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -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((resolve) => + new Promise((resolve) => appContext.triggerCommand("executeWithCodeEditor", { resolve, ntxId: this.ntxId diff --git a/apps/client/src/libraries/codemirror/eslint.js b/apps/client/src/libraries/codemirror/eslint.js deleted file mode 100644 index 403cb4e54..000000000 --- a/apps/client/src/libraries/codemirror/eslint.js +++ /dev/null @@ -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 = /]*>([\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) - }); - } - } -}); diff --git a/apps/client/src/services/glob.ts b/apps/client/src/services/glob.ts index 3dafc03fd..a1666b041 100644 --- a/apps/client/src/services/glob.ts +++ b/apps/client/src/services/glob.ts @@ -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 diff --git a/apps/client/src/services/library_loader.ts b/apps/client/src/services/library_loader.ts index 90a19cd72..a5ad0e3fc 100644 --- a/apps/client/src/services/library_loader.ts +++ b/apps/client/src/services/library_loader.ts @@ -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 }; diff --git a/apps/client/src/services/mime_type_definitions.ts b/apps/client/src/services/mime_type_definitions.ts index 26e7011b0..10c73e113 100644 --- a/apps/client/src/services/mime_type_definitions.ts +++ b/apps/client/src/services/mime_type_definitions.ts @@ -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" }, diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 544b6581b..5f9ffb664 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -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%; diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index cfabe2f15..3b7a8fcc2 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -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; } diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 98f379c6e..c32a01e84 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -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; } diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index da17ebc9e..bcc0bc269 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1616,7 +1616,7 @@ "auto-detect-language": "自动检测" }, "highlighting": { - "title": "文本笔记的代码语法高亮", + "title": "", "description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。", "color-scheme": "颜色方案" }, diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index 108586d05..8ec5097aa 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -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" }, diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index a5526659b..24f104441 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -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" } } diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index 172ca8ea0..46be1d55f 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -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" }, diff --git a/apps/client/src/translations/fr/translation.json b/apps/client/src/translations/fr/translation.json index 91758b8d6..d7597a3ac 100644 --- a/apps/client/src/translations/fr/translation.json +++ b/apps/client/src/translations/fr/translation.json @@ -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" }, diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 6e371c28a..efaf270f2 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -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": { diff --git a/apps/client/src/translations/tw/translation.json b/apps/client/src/translations/tw/translation.json index 2d2e256ad..4360fcdf4 100644 --- a/apps/client/src/translations/tw/translation.json +++ b/apps/client/src/translations/tw/translation.json @@ -1514,7 +1514,7 @@ "auto-detect-language": "自動檢測" }, "highlighting": { - "title": "文字筆記的程式碼語法高亮", + "title": "", "description": "控制文字筆記中程式碼塊的語法高亮,程式碼筆記不會受到影響。", "color-scheme": "顏色方案" }, diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index 0a466825a..70e51aede 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -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; - }; - 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 diff --git a/apps/client/src/widgets/find.ts b/apps/client/src/widgets/find.ts index 8db84e448..0239251cb 100644 --- a/apps/client/src/widgets/find.ts +++ b/apps/client/src/widgets/find.ts @@ -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; } } diff --git a/apps/client/src/widgets/find_in_code.ts b/apps/client/src/widgets/find_in_code.ts index 63081bb0b..5dc8536ba 100644 --- a/apps/client/src/widgets/find_in_code.ts +++ b/apps/client/src/widgets/find_in_code.ts @@ -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); + } + } + } diff --git a/apps/client/src/widgets/type_widgets/abstract_code_type_widget.ts b/apps/client/src/widgets/type_widgets/abstract_code_type_widget.ts index 69bc38bd7..6ec033df5 100644 --- a/apps/client/src/widgets/type_widgets/abstract_code_type_widget.ts +++ b/apps/client/src/widgets/type_widgets/abstract_code_type_widget.ts @@ -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; - 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 { + getExtraOpts(): Partial { 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")); + } + } diff --git a/apps/client/src/widgets/type_widgets/abstract_split_type_widget.ts b/apps/client/src/widgets/type_widgets/abstract_split_type_widget.ts index 3b1cb222b..3758229ea 100644 --- a/apps/client/src/widgets/type_widgets/abstract_split_type_widget.ts +++ b/apps/client/src/widgets/type_widgets/abstract_split_type_widget.ts @@ -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*/`\
@@ -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 { + buildEditorExtraOptions(): Partial { return { lineWrapping: false }; diff --git a/apps/client/src/widgets/type_widgets/content_widget.ts b/apps/client/src/widgets/type_widgets/content_widget.ts index 45af111e0..3283672a4 100644 --- a/apps/client/src/widgets/type_widgets/content_widget.ts +++ b/apps/client/src/widgets/type_widgets/content_widget.ts @@ -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*/`
+
+`; + +export default class CodeTheme extends OptionsWidget { + + private $themeSelect!: JQuery; + private $sampleEl!: JQuery; + private $lineWrapEnabled!: JQuery; + 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 = $("