From 54a6e3d9a1baa8bc0e7336d697980fc106444f0b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 14 Apr 2026 23:33:32 +0300 Subject: [PATCH] feat(code): adjustable default tab width --- .../src/translations/en/translation.json | 4 ++- .../src/widgets/type_widgets/code/Code.tsx | 2 ++ .../widgets/type_widgets/code/CodeMirror.tsx | 7 +++++ .../type_widgets/options/code_notes.tsx | 26 ++++++++++++++++--- .../type_widgets/options/text_notes.tsx | 7 ++--- apps/server/src/routes/api/options.ts | 1 + apps/server/src/services/options_init.ts | 1 + packages/codemirror/src/index.ts | 13 +++++++++- packages/commons/src/lib/options_interface.ts | 1 + 9 files changed, 53 insertions(+), 9 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index f9cbbb146f..42ceef2e2a 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1268,7 +1268,9 @@ "unit": "characters" }, "code-editor-options": { - "title": "Editor" + "title": "Editor", + "tab_width": "Tab width", + "tab_width_unit": "spaces" }, "code_mime_types": { "title": "Available MIME types in the dropdown", diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index f59d6eff3c..ec91a4f836 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -146,6 +146,7 @@ export function CodeEditor({ parentComponent, ntxId, containerRef: externalConta const initialized = useRef($.Deferred()); const [ codeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled"); const [ codeNoteTheme ] = useTriliumOption("codeNoteTheme"); + const [ codeNoteTabWidth ] = useTriliumOption("codeNoteTabWidth"); // React to background color. const [ backgroundColor, setBackgroundColor ] = useState(); @@ -200,6 +201,7 @@ export function CodeEditor({ parentComponent, ntxId, containerRef: externalConta editorRef={codeEditorRef} containerRef={containerRef} lineWrapping={lineWrapping ?? codeLineWrapEnabled} + indentSize={parseInt(codeNoteTabWidth) || 4} onInitialized={() => { if (externalContainerRef && containerRef.current) { externalContainerRef.current = containerRef.current; diff --git a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx index ae4f849dd1..ecc5e2c658 100644 --- a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx +++ b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx @@ -49,6 +49,13 @@ export default function CodeMirror({ className, content, mime, editorRef: extern // React to line wrapping. useEffect(() => codeEditorRef.current?.setLineWrapping(!!lineWrapping), [ lineWrapping ]); + // React to indent size changes. + useEffect(() => { + if (extraOpts.indentSize != null) { + codeEditorRef.current?.setIndentSize(extraOpts.indentSize); + } + }, [ extraOpts.indentSize ]); + return (
     )
diff --git a/apps/client/src/widgets/type_widgets/options/code_notes.tsx b/apps/client/src/widgets/type_widgets/options/code_notes.tsx
index 7721e0de17..4b8192be96 100644
--- a/apps/client/src/widgets/type_widgets/options/code_notes.tsx
+++ b/apps/client/src/widgets/type_widgets/options/code_notes.tsx
@@ -21,11 +21,12 @@ const SAMPLE_MIME = "application/typescript";
 
 export default function CodeNoteSettings() {
     const [codeLineWrapEnabled, setCodeLineWrapEnabled] = useTriliumOptionBool("codeLineWrapEnabled");
+    const [codeNoteTabWidth] = useTriliumOption("codeNoteTabWidth");
 
     return (
         <>
             
-            
+            
             
         
     );
@@ -39,6 +40,7 @@ interface EditorProps {
 function Editor({ wordWrapping, setWordWrapping }: EditorProps) {
     const [vimKeymapEnabled, setVimKeymapEnabled] = useTriliumOptionBool("vimKeymapEnabled");
     const [autoReadonlySize, setAutoReadonlySize] = useTriliumOption("autoReadonlySizeCode");
+    const [codeNoteTabWidth, setCodeNoteTabWidth] = useTriliumOption("codeNoteTabWidth");
 
     return (
         
@@ -49,6 +51,17 @@ function Editor({ wordWrapping, setWordWrapping }: EditorProps) {
                 onChange={setWordWrapping}
             />
 
+            {/* Avoid using "code" in the name of numeric inputs to prevent KeepassXC from triggering. */}
+            
+                
+            
+
             
                  {
@@ -93,12 +107,12 @@ function Appearance({ wordWrapping }: AppearanceProps) {
                 />
             
 
-            
+            
         
     );
 }
 
-function CodeNotePreview({ themeName, wordWrapping }: { themeName: string, wordWrapping: boolean }) {
+function CodeNotePreview({ themeName, wordWrapping, indentSize }: { themeName: string, wordWrapping: boolean, indentSize: number }) {
     const editorRef = useRef(null);
     const containerRef = useRef(null);
 
@@ -124,6 +138,10 @@ function CodeNotePreview({ themeName, wordWrapping }: { themeName: string, wordW
         editorRef.current?.setLineWrapping(wordWrapping);
     }, [ wordWrapping ]);
 
+    useEffect(() => {
+        editorRef.current?.setIndentSize(indentSize);
+    }, [ indentSize ]);
+
     useEffect(() => {
         if (themeName?.startsWith(DEFAULT_PREFIX)) {
             const theme = getThemeById(themeName.substring(DEFAULT_PREFIX.length));
diff --git a/apps/client/src/widgets/type_widgets/options/text_notes.tsx b/apps/client/src/widgets/type_widgets/options/text_notes.tsx
index 4cf24dcedd..b766dad698 100644
--- a/apps/client/src/widgets/type_widgets/options/text_notes.tsx
+++ b/apps/client/src/widgets/type_widgets/options/text_notes.tsx
@@ -286,12 +286,14 @@ function CodeBlockStyle() {
                 onChange={setCodeBlockWordWrap}
             />
 
-            
+            {/* Avoid using "code" in the name of numeric inputs to prevent KeepassXC from triggering. */}
+            
                 
             
 
@@ -336,7 +338,7 @@ function CodeBlockPreview({ theme, wordWrap, tabWidth }: { theme: string, wordWr
         }
     }, [theme]);
 
-    const codeStyle = useMemo(() => {
+    const codeStyle: CSSProperties = useMemo(() => {
         return {
             whiteSpace: wordWrap ? "pre-wrap" : "pre",
             tabSize: tabWidth ?? "4"
@@ -416,4 +418,3 @@ export function HighlightsListOptions() {
         
     );
 }
-
diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts
index 8191b6812e..137ab51cf3 100644
--- a/apps/server/src/routes/api/options.ts
+++ b/apps/server/src/routes/api/options.ts
@@ -32,6 +32,7 @@ const ALLOWED_OPTIONS = new Set([
     "codeBlockWordWrap",
     "codeBlockTabWidth",
     "codeNoteTheme",
+    "codeNoteTabWidth",
     "syncServerHost",
     "syncServerTimeout",
     "syncServerTimeoutTimeScale",
diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts
index bdab81af62..94151b269c 100644
--- a/apps/server/src/services/options_init.ts
+++ b/apps/server/src/services/options_init.ts
@@ -131,6 +131,7 @@ const defaultOptions: DefaultOption[] = [
     { name: "autoFixConsistencyIssues", value: "true", isSynced: false },
     { name: "vimKeymapEnabled", value: "false", isSynced: false },
     { name: "codeLineWrapEnabled", value: "true", isSynced: false },
+    { name: "codeNoteTabWidth", value: "4", isSynced: true },
     {
         name: "codeNotesMimeTypes",
         value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-elixir","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml","text/x-sh","application/typescript"]',
diff --git a/packages/codemirror/src/index.ts b/packages/codemirror/src/index.ts
index ae6142c1de..7b868c859a 100644
--- a/packages/codemirror/src/index.ts
+++ b/packages/codemirror/src/index.ts
@@ -34,6 +34,8 @@ export interface EditorConfig {
     /** Disables some of the nice-to-have features (bracket matching, syntax highlighting, indentation markers) in order to improve performance. */
     preferPerformance?: boolean;
     tabIndex?: number;
+    /** The number of spaces used for indentation. Defaults to 4. */
+    indentSize?: number;
     onContentChanged?: ContentChangedListener;
 }
 
@@ -44,6 +46,7 @@ export default class CodeMirror extends EditorView {
     private historyCompartment: Compartment;
     private themeCompartment: Compartment;
     private lineWrappingCompartment: Compartment;
+    private indentUnitCompartment: Compartment;
     private searchHighlightCompartment: Compartment;
     private searchPlugin?: SearchHighlighter | null;
 
@@ -52,6 +55,7 @@ export default class CodeMirror extends EditorView {
         const historyCompartment = new Compartment();
         const themeCompartment = new Compartment();
         const lineWrappingCompartment = new Compartment();
+        const indentUnitCompartment = new Compartment();
         const searchHighlightCompartment = new Compartment();
 
         let extensions: Extension[] = [];
@@ -68,7 +72,7 @@ export default class CodeMirror extends EditorView {
             searchHighlightCompartment.of([]),
             highlightActiveLine(),
             lineNumbers(),
-            indentUnit.of(" ".repeat(4)),
+            indentUnitCompartment.of(indentUnit.of(" ".repeat(config.indentSize ?? 4))),
             keymap.of([
                 ...preventCtrlEnterKeymap,
                 ...defaultKeymap,
@@ -121,6 +125,7 @@ export default class CodeMirror extends EditorView {
         this.historyCompartment = historyCompartment;
         this.themeCompartment = themeCompartment;
         this.lineWrappingCompartment = lineWrappingCompartment;
+        this.indentUnitCompartment = indentUnitCompartment;
         this.searchHighlightCompartment = searchHighlightCompartment;
     }
 
@@ -168,6 +173,12 @@ export default class CodeMirror extends EditorView {
         });
     }
 
+    setIndentSize(size: number) {
+        this.dispatch({
+            effects: [ this.indentUnitCompartment.reconfigure(indentUnit.of(" ".repeat(size))) ]
+        });
+    }
+
     /**
      * Clears the history of undo/redo. Generally useful when changing to a new document.
      */
diff --git a/packages/commons/src/lib/options_interface.ts b/packages/commons/src/lib/options_interface.ts
index b64ae1e568..d89f1460fd 100644
--- a/packages/commons/src/lib/options_interface.ts
+++ b/packages/commons/src/lib/options_interface.ts
@@ -161,6 +161,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions