Add QsciLexer-based syntax highlighting with dark theme

ober

e02e0e21351b071c7d283724e1f457b2d62f009d

diff --git a/lib/jerboa-emacs/qt/highlight.sls b/lib/jerboa-emacs/qt/highlight.sls
index a141483..d52d161 100644
--- a/lib/jerboa-emacs/qt/highlight.sls
+++ b/lib/jerboa-emacs/qt/highlight.sls
@@ -599,28 +599,51 @@
        (sci-send ed SCI_STYLESETBACK s 1579032)
        (loop (+ s 1)))))
   (def (qt-setup-highlighting! app buf)
-       (let* ([lang (or (detect-language (buffer-file-path buf))
-                        (let ([path (buffer-file-path buf)])
-                          (and path
-                               (file-exists? path)
-                               (with-catch
-                                 (lambda (e) #f)
-                                 (lambda ()
-                                   (let ([line (call-with-input-file
-                                                 path
-                                                 read-line)])
-                                     (and (string? line)
-                                          (> (string-length line) 2)
-                                          (detect-language-from-shebang-qt
-                                            (string-append
-                                              line
-                                              "\n"))))))))
+       (let* ([ext-lang (detect-language (buffer-file-path buf))]
+              [shebang-lang (and (not ext-lang)
+                                 (let ([path (buffer-file-path buf)])
+                                   (and path
+                                        (file-exists? path)
+                                        (with-catch
+                                          (lambda (e)
+                                            (verbose-log!
+                                              "highlight: shebang exception: "
+                                              (with-output-to-string
+                                                (lambda ()
+                                                  (display-exception e))))
+                                            #f)
+                                          (lambda ()
+                                            (let ([line (call-with-input-file
+                                                          path
+                                                          (lambda (port)
+                                                            (get-line
+                                                              port)))])
+                                              (verbose-log!
+                                                "highlight: shebang line="
+                                                (if (string? line)
+                                                    line
+                                                    "NOT-STRING"))
+                                              (and (string? line)
+                                                   (> (string-length line)
+                                                      2)
+                                                   (detect-language-from-shebang-qt
+                                                     (string-append
+                                                       line
+                                                       "\n")))))))))]
+              [lang (or ext-lang
+                        shebang-lang
                         (let ([l (buffer-lexer-lang buf)])
                           (and (not (memq l '(dired repl eshell shell)))
                                l)))]
               [doc (buffer-doc-pointer buf)]
               [ed (and doc (hash-get *doc-editor-map* doc))]
               [lexer-name (and lang (language->lexer-name lang))])
+         (verbose-log! "highlight: path=" (or (buffer-file-path buf) "nil")
+           " ext-lang=" (if ext-lang (symbol->string ext-lang) "nil")
+           " shebang-lang="
+           (if shebang-lang (symbol->string shebang-lang) "nil")
+           " lang=" (if lang (symbol->string lang) "nil") " ed="
+           (if ed "yes" "nil") " lexer=" (or lexer-name "nil"))
          (when (and ed (eq? lang 'org))
            (buffer-lexer-lang-set! buf 'org)
            (apply-base-theme! ed)
@@ -628,74 +651,53 @@
            (qt-setup-org-styles! ed)
            (let ([text (qt-plain-text-edit-text ed)])
              (qt-org-highlight-buffer-async! ed text)))
-         (let ([ts-name (and lang
-                             (not (eq? lang 'org))
-                             (language->ts-name lang))])
-           (if (and ed
-                    ts-name
-                    (with-catch
-                      (lambda (e) #f)
-                      (lambda () (ts-buffer-init! buf ts-name))))
-               (begin
-                 (buffer-lexer-lang-set! buf lang)
-                 (apply-base-theme! ed)
-                 (sci-send ed 4033 0)
-                 (ts-setup-styles! ed)
-                 (let ([text (qt-plain-text-edit-text ed)])
-                   (when (and text (> (string-length text) 0))
-                     (ts-buffer-reparse! buf text ed)))
-                 (qt-enable-code-folding! ed))
-               (when (and ed lexer-name)
-                 (buffer-lexer-lang-set! buf lang)
-                 (apply-base-theme! ed)
-                 (if (member
-                       lexer-name
-                       '("lisp" "rust" "diff" "perl" "haskell" "props"))
-                     (begin
-                       (sci-send/string ed SCI_SETLEXERLANGUAGE lexer-name)
-                       (sci-send ed SCI_COLOURISE 0 -1))
-                     (qt-scintilla-set-lexer-language! ed lexer-name))
-                 (case lang
-                   [(scheme lisp) (setup-lisp-styles! ed)]
-                   [(c) (setup-cpp-styles! ed *c-keywords* *c-types*)]
-                   [(javascript)
-                    (setup-cpp-styles! ed *js-keywords* *js-builtins*)]
-                   [(go) (setup-cpp-styles! ed *go-keywords* *go-types*)]
-                   [(rust)
-                    (setup-cpp-styles! ed *rust-keywords* *rust-types*)]
-                   [(java haskell swift elixir)
-                    (setup-cpp-styles! ed *java-keywords* *java-types*)]
-                   [(zig nix)
-                    (setup-cpp-styles! ed *c-keywords* *c-types*)]
-                   [(python) (setup-python-styles! ed)]
-                   [(shell) (setup-bash-styles! ed)]
-                   [(ruby) (setup-ruby-styles! ed)]
-                   [(lua) (setup-lua-styles! ed)]
-                   [(sql) (setup-sql-styles! ed)]
-                   [(perl) (setup-perl-styles! ed)]
-                   [(json yaml xml css markdown makefile diff toml)
-                    (let-values ([(r g b)
-                                  (face-fg-rgb 'font-lock-comment-face)])
-                      (for-each
-                        (lambda (s)
-                          (sci-send ed SCI_STYLESETFORE s (rgb->sci r g b))
-                          (when (face-has-italic? 'font-lock-comment-face)
-                            (sci-send ed SCI_STYLESETITALIC s 1)))
-                        '(1 2 3)))
-                    (let-values ([(r g b)
-                                  (face-fg-rgb 'font-lock-keyword-face)])
-                      (sci-send ed SCI_STYLESETFORE 5 (rgb->sci r g b))
-                      (when (face-has-bold? 'font-lock-keyword-face)
-                        (sci-send ed SCI_STYLESETBOLD 5 1)))
-                    (let-values ([(r g b)
-                                  (face-fg-rgb 'font-lock-string-face)])
-                      (sci-send ed SCI_STYLESETFORE 6 (rgb->sci r g b))
-                      (sci-send ed SCI_STYLESETFORE 7 (rgb->sci r g b)))
-                    (let-values ([(r g b)
-                                  (face-fg-rgb 'font-lock-number-face)])
-                      (sci-send ed SCI_STYLESETFORE 4 (rgb->sci r g b)))]
-                   [else (void)])
-                 (qt-enable-code-folding! ed))))))
+         (when (and ed lexer-name)
+           (buffer-lexer-lang-set! buf lang)
+           (qt-scintilla-set-lexer-language! ed lexer-name)
+           (case lang
+             [(c)
+              (sci-send/string ed SCI_SETKEYWORDS *c-keywords* 0)
+              (when *c-types*
+                (sci-send/string ed SCI_SETKEYWORDS *c-types* 1))]
+             [(javascript)
+              (sci-send/string ed SCI_SETKEYWORDS *js-keywords* 0)
+              (when *js-builtins*
+                (sci-send/string ed SCI_SETKEYWORDS *js-builtins* 1))]
+             [(go)
+              (sci-send/string ed SCI_SETKEYWORDS *go-keywords* 0)
+              (when *go-types*
+                (sci-send/string ed SCI_SETKEYWORDS *go-types* 1))]
+             [(rust)
+              (sci-send/string ed SCI_SETKEYWORDS *rust-keywords* 0)
+              (when *rust-types*
+                (sci-send/string ed SCI_SETKEYWORDS *rust-types* 1))]
+             [(java haskell swift elixir)
+              (sci-send/string ed SCI_SETKEYWORDS *java-keywords* 0)
+              (when *java-types*
+                (sci-send/string ed SCI_SETKEYWORDS *java-types* 1))]
+             [(zig nix)
+              (sci-send/string ed SCI_SETKEYWORDS *c-keywords* 0)
+              (when *c-types*
+                (sci-send/string ed SCI_SETKEYWORDS *c-types* 1))]
+             [(shell)
+              (sci-send/string ed SCI_SETKEYWORDS *shell-keywords* 0)]
+             [else (void)])
+           (let-values ([(ln-r ln-g ln-b) (face-fg-rgb 'line-number)])
+             (sci-send
+               ed
+               SCI_STYLESETFORE
+               STYLE_LINENUMBER
+               (rgb->sci ln-r ln-g ln-b)))
+           (let ([ln-face (face-get 'line-number)])
+             (when (and ln-face (face-bg ln-face))
+               (let-values ([(bg-r bg-g bg-b)
+                             (parse-hex-color (face-bg ln-face))])
+                 (sci-send
+                   ed
+                   SCI_STYLESETBACK
+                   STYLE_LINENUMBER
+                   (rgb->sci bg-r bg-g bg-b)))))
+           (qt-enable-code-folding! ed))))
   (def (qt-enable-code-folding! ed)
        "Enable code folding margin and markers for QScintilla editor.\n   Sets up margin 2 as fold margin with box-tree style markers.\n   QScintilla lexers compute fold levels automatically."
        (sci-send ed SCI_SETMARGINTYPEN 2 SC_MARGIN_SYMBOL)
diff --git a/lib/jerboa-emacs/qt/sci-shim.sls b/lib/jerboa-emacs/qt/sci-shim.sls
index 04a9ac7..c03c096 100644
--- a/lib/jerboa-emacs/qt/sci-shim.sls
+++ b/lib/jerboa-emacs/qt/sci-shim.sls
@@ -98,7 +98,9 @@
    qt-scintilla-get-text-length qt-scintilla-receive-string
    qt-scintilla-send-message qt-scintilla-send-message-string
    qt-scintilla-set-lexer-language! qt-scintilla-set-text!
-   qt-scroll-area-create qt-scroll-area-set-widget!
+   qt-scintilla-lexer-set-color! qt-scintilla-lexer-set-paper!
+   qt-scintilla-lexer-set-font-attr! qt-scroll-area-create
+   qt-scroll-area-set-widget!
    qt-scroll-area-set-widget-resizable! qt-splitter-add-widget!
    qt-splitter-count qt-splitter-create qt-splitter-index-of
    qt-splitter-insert-widget! qt-splitter-set-handle-width!
diff --git a/src/jerboa-emacs/core.ss b/src/jerboa-emacs/core.ss
index a8364da..f3fcac8 100644
--- a/src/jerboa-emacs/core.ss
+++ b/src/jerboa-emacs/core.ss
@@ -1752,7 +1752,7 @@
     (lambda (e) #f)  ;; binary/unreadable files return #f
     (lambda ()
       (call-with-input-file path
-        (lambda (port) (read-line port #f))))))
+        (lambda (port) (get-string-all port))))))
 
 (def (write-string-to-file path str)
   (call-with-output-file path
diff --git a/src/jerboa-emacs/qt/highlight.ss b/src/jerboa-emacs/qt/highlight.ss
index 4da6e92..22a5d29 100644
--- a/src/jerboa-emacs/qt/highlight.ss
+++ b/src/jerboa-emacs/qt/highlight.ss
@@ -34,6 +34,9 @@
                  org-comment-line? org-keyword-line?
                  org-table-line? org-block-begin? org-block-end?))
 
+;;; No extra FFI needed — we use raw Lexilla via SCI_SETLEXERLANGUAGE
+;;; which respects SCI_STYLESETFORE/BACK (unlike QsciLexer wrappers).
+
 ;;;============================================================================
 ;;; Mode flags (shared with commands layer)
 ;;;============================================================================
@@ -304,7 +307,7 @@
         (sci-send ed SCI_STYLESETBACK STYLE_LINENUMBER (rgb->sci bg-r bg-g bg-b))))))
 
 ;;;============================================================================
-;;; Lexer-specific style setup
+;;; Lexer style setup (via SCI messages — works with raw Lexilla)
 ;;;============================================================================
 
 ;;; --- C/C++/Java/JS/Go/Rust/Zig lexer ("cpp") ---
@@ -609,21 +612,37 @@
 ;;;============================================================================
 
 (def (qt-setup-highlighting! app buf)
-  (let* ((lang (or (detect-language (buffer-file-path buf))
-                   (let ((path (buffer-file-path buf)))
-                     (and path (file-exists? path)
-                          (with-catch (lambda (e) #f)
-                            (lambda ()
-                              (let ((line (call-with-input-file path read-line)))
-                                (and (string? line)
-                                     (> (string-length line) 2)
-                                     (detect-language-from-shebang-qt
-                                       (string-append line "\n"))))))))
+  (let* ((ext-lang (detect-language (buffer-file-path buf)))
+         (shebang-lang
+           (and (not ext-lang)
+                (let ((path (buffer-file-path buf)))
+                  (and path (file-exists? path)
+                       (with-catch
+                         (lambda (e)
+                           (verbose-log! "highlight: shebang exception: "
+                             (with-output-to-string (lambda () (display-exception e))))
+                           #f)
+                         (lambda ()
+                           (let ((line (call-with-input-file path
+                                         (lambda (port) (get-line port)))))
+                             (verbose-log! "highlight: shebang line=" (if (string? line) line "NOT-STRING"))
+                             (and (string? line)
+                                  (> (string-length line) 2)
+                                  (detect-language-from-shebang-qt
+                                    (string-append line "\n"))))))))))
+         (lang (or ext-lang
+                   shebang-lang
                    (let ((l (buffer-lexer-lang buf)))
                      (and (not (memq l '(dired repl eshell shell))) l))))
          (doc (buffer-doc-pointer buf))
          (ed (and doc (hash-get *doc-editor-map* doc)))
          (lexer-name (and lang (language->lexer-name lang))))
+    (verbose-log! "highlight: path=" (or (buffer-file-path buf) "nil")
+                  " ext-lang=" (if ext-lang (symbol->string ext-lang) "nil")
+                  " shebang-lang=" (if shebang-lang (symbol->string shebang-lang) "nil")
+                  " lang=" (if lang (symbol->string lang) "nil")
+                  " ed=" (if ed "yes" "nil")
+                  " lexer=" (or lexer-name "nil"))
     ;; Org mode: no QScintilla lexer — use manual styling
     (when (and ed (eq? lang 'org))
       (set! (buffer-lexer-lang buf) 'org)
@@ -634,93 +653,51 @@
       ;; Apply full-buffer org highlighting in background
       (let ((text (qt-plain-text-edit-text ed)))
         (qt-org-highlight-buffer-async! ed text)))
-    ;; Try tree-sitter first, fall back to Lexilla
-    (let ((ts-name (and lang (not (eq? lang 'org)) (language->ts-name lang))))
-      (if (and ed ts-name
-               (with-catch (lambda (e) #f)
-                 (lambda () (ts-buffer-init! buf ts-name))))
-        ;; Tree-sitter available — use it
-        (begin
-          (set! (buffer-lexer-lang buf) lang)
-          (apply-base-theme! ed)
-          ;; Disable built-in lexer
-          (sci-send ed 4033 0)
-          ;; Set up tree-sitter styles
-          (ts-setup-styles! ed)
-          ;; Initial full parse + highlight
-          (let ((text (qt-plain-text-edit-text ed)))
-            (when (and text (> (string-length text) 0))
-              (ts-buffer-reparse! buf text ed)))
-          ;; Enable code folding
-          (qt-enable-code-folding! ed))
-        ;; No tree-sitter — fall back to Lexilla
-        (when (and ed lexer-name)
+    ;; Use raw Lexilla via SCI_SETLEXERLANGUAGE (not QsciLexer wrappers).
+    ;; QsciLexer wrappers override SCI_STYLESETFORE/BACK, but raw Lexilla
+    ;; respects our style settings, so apply-base-theme! and setup-*-styles! work.
+    (when (and ed lexer-name)
           ;; Store language in buffer
           (set! (buffer-lexer-lang buf) lang)
-          ;; Reset to dark theme base
-          (apply-base-theme! ed)
-          ;; Set lexer language — QScintilla has wrapper classes for common languages,
-          ;; but not for lisp, rust, diff, perl, haskell, props.
-          ;; For those, use SCI_SETLEXERLANGUAGE to invoke Lexilla directly.
-          (if (member lexer-name '("lisp" "rust" "diff" "perl" "haskell" "props"))
-            (begin
-              ;; No QsciLexer wrapper — use raw Scintilla message and force colorize
-              (sci-send/string ed SCI_SETLEXERLANGUAGE lexer-name)
-              (sci-send ed SCI_COLOURISE 0 -1))
-            (qt-scintilla-set-lexer-language! ed lexer-name))
-          ;; Apply lexer-specific styles and keywords
+          ;; Use QsciLexer wrappers for tokenization.
+          ;; The C++ function now also sets dark paper on the lexer object
+          ;; after setLexer(), so the background is dark.
+          ;; QsciLexer's own foreground colors (designed for syntax highlighting)
+          ;; are kept as-is — they provide tokenized coloring out of the box.
+          (qt-scintilla-set-lexer-language! ed lexer-name)
+          ;; Set keywords per language (QsciLexer has some defaults,
+          ;; but we override to ensure completeness)
           (case lang
-            ((scheme lisp)
-             (setup-lisp-styles! ed))
             ((c)
-             (setup-cpp-styles! ed *c-keywords* *c-types*))
+             (sci-send/string ed SCI_SETKEYWORDS *c-keywords* 0)
+             (when *c-types* (sci-send/string ed SCI_SETKEYWORDS *c-types* 1)))
             ((javascript)
-             (setup-cpp-styles! ed *js-keywords* *js-builtins*))
+             (sci-send/string ed SCI_SETKEYWORDS *js-keywords* 0)
+             (when *js-builtins* (sci-send/string ed SCI_SETKEYWORDS *js-builtins* 1)))
             ((go)
-             (setup-cpp-styles! ed *go-keywords* *go-types*))
+             (sci-send/string ed SCI_SETKEYWORDS *go-keywords* 0)
+             (when *go-types* (sci-send/string ed SCI_SETKEYWORDS *go-types* 1)))
             ((rust)
-             (setup-cpp-styles! ed *rust-keywords* *rust-types*))
+             (sci-send/string ed SCI_SETKEYWORDS *rust-keywords* 0)
+             (when *rust-types* (sci-send/string ed SCI_SETKEYWORDS *rust-types* 1)))
             ((java haskell swift elixir)
-             (setup-cpp-styles! ed *java-keywords* *java-types*))
+             (sci-send/string ed SCI_SETKEYWORDS *java-keywords* 0)
+             (when *java-types* (sci-send/string ed SCI_SETKEYWORDS *java-types* 1)))
             ((zig nix)
-             (setup-cpp-styles! ed *c-keywords* *c-types*))
-            ((python)
-             (setup-python-styles! ed))
+             (sci-send/string ed SCI_SETKEYWORDS *c-keywords* 0)
+             (when *c-types* (sci-send/string ed SCI_SETKEYWORDS *c-types* 1)))
             ((shell)
-             (setup-bash-styles! ed))
-            ((ruby)
-             (setup-ruby-styles! ed))
-            ((lua)
-             (setup-lua-styles! ed))
-            ((sql)
-             (setup-sql-styles! ed))
-            ((perl)
-             (setup-perl-styles! ed))
-            ;; For json, yaml, xml, css, markdown, makefile, diff, toml:
-            ;; The lexer handles tokenization; we just set basic comment/string colors
-            ((json yaml xml css markdown makefile diff toml)
-             ;; Comments: styles 1-3 for most lexers
-             (let-values (((r g b) (face-fg-rgb 'font-lock-comment-face)))
-               (for-each (lambda (s)
-                           (sci-send ed SCI_STYLESETFORE s (rgb->sci r g b))
-                           (when (face-has-italic? 'font-lock-comment-face)
-                             (sci-send ed SCI_STYLESETITALIC s 1)))
-                         '(1 2 3)))
-             ;; Keywords: style 5
-             (let-values (((r g b) (face-fg-rgb 'font-lock-keyword-face)))
-               (sci-send ed SCI_STYLESETFORE 5 (rgb->sci r g b))
-               (when (face-has-bold? 'font-lock-keyword-face)
-                 (sci-send ed SCI_STYLESETBOLD 5 1)))
-             ;; Strings: styles 6-7
-             (let-values (((r g b) (face-fg-rgb 'font-lock-string-face)))
-               (sci-send ed SCI_STYLESETFORE 6 (rgb->sci r g b))
-               (sci-send ed SCI_STYLESETFORE 7 (rgb->sci r g b)))
-             ;; Numbers: style 4
-             (let-values (((r g b) (face-fg-rgb 'font-lock-number-face)))
-               (sci-send ed SCI_STYLESETFORE 4 (rgb->sci r g b))))
+             (sci-send/string ed SCI_SETKEYWORDS *shell-keywords* 0))
             (else (void)))
-          ;; Enable code folding margin for all code files
-          (qt-enable-code-folding! ed))))))
+          ;; Line number margin styling
+          (let-values (((ln-r ln-g ln-b) (face-fg-rgb 'line-number)))
+            (sci-send ed SCI_STYLESETFORE STYLE_LINENUMBER (rgb->sci ln-r ln-g ln-b)))
+          (let ((ln-face (face-get 'line-number)))
+            (when (and ln-face (face-bg ln-face))
+              (let-values (((bg-r bg-g bg-b) (parse-hex-color (face-bg ln-face))))
+                (sci-send ed SCI_STYLESETBACK STYLE_LINENUMBER (rgb->sci bg-r bg-g bg-b)))))
+          ;; Enable code folding
+          (qt-enable-code-folding! ed))))
 
 ;;;============================================================================
 ;;; Code folding margin setup
diff --git a/src/jerboa-emacs/qt/sci-shim.ss b/src/jerboa-emacs/qt/sci-shim.ss
index 0f412b8..171478f 100644
--- a/src/jerboa-emacs/qt/sci-shim.ss
+++ b/src/jerboa-emacs/qt/sci-shim.ss
@@ -100,6 +100,7 @@
   qt-scintilla-create qt-scintilla-get-text qt-scintilla-get-text-length
   qt-scintilla-receive-string qt-scintilla-send-message
   qt-scintilla-send-message-string qt-scintilla-set-lexer-language! qt-scintilla-set-text!
+  qt-scintilla-lexer-set-color! qt-scintilla-lexer-set-paper! qt-scintilla-lexer-set-font-attr!
   ;; Scroll area
   qt-scroll-area-create qt-scroll-area-set-widget! qt-scroll-area-set-widget-resizable!
   ;; Splitter
diff --git a/src/jerboa-emacs/treesitter.ss b/src/jerboa-emacs/treesitter.ss
index dddf2e3..7b9c812 100644
--- a/src/jerboa-emacs/treesitter.ss
+++ b/src/jerboa-emacs/treesitter.ss
@@ -217,6 +217,7 @@
       ;; Collect all captures in batch
       (let ((count (ffi-ts-highlight-captures cursor query
                      *capture-buf* *capture-buf-size*)))
+        (verbose-log! "ts-highlight: captures=" (number->string count))
         ;; Apply styles
         (ts-apply-captures! ed query count))
       (ffi-ts-query-cursor-delete cursor))))
@@ -232,7 +233,7 @@
       (let ((count (ffi-ts-highlight-captures cursor query
                      *capture-buf* *capture-buf-size*)))
         ;; Clear styles in range first
-        (sci-send ed SCI_STARTSTYLING start-byte)
+        (sci-send ed SCI_STARTSTYLING start-byte 31)
         (sci-send ed SCI_SETSTYLING (- end-byte start-byte) 0)
         ;; Apply captures
         (ts-apply-captures! ed query count))
@@ -242,8 +243,8 @@
   "Apply captured highlights to the Scintilla editor.
    Reads from *capture-buf* (filled by ts-highlight-captures)."
   (let ((buf *capture-buf*))
-    (let loop ((i 0) (last-end 0))
-      (when (< i count)
+    (let loop ((i 0) (last-end 0) (applied 0))
+      (if (< i count)
         (let* ((off (* i 12))
                (start (bytevector-s32-native-ref buf off))
                (end   (bytevector-s32-native-ref buf (+ off 4)))
@@ -251,10 +252,21 @@
                (cap-name (ffi-ts-query-capture-name query cap-idx))
                (style-id (ts-capture->style-id cap-name))
                (len (- end start)))
-          (when (and (> style-id 0) (> len 0) (>= start last-end))
-            (sci-send ed SCI_STARTSTYLING start)
-            (sci-send ed SCI_SETSTYLING len style-id))
-          (loop (+ i 1) (max last-end end)))))))
+          (when (< i 5)
+            (verbose-log! "ts-capture[" (number->string i) "]: name=" cap-name
+                          " style=" (number->string style-id)
+                          " start=" (number->string start)
+                          " end=" (number->string end)
+                          " len=" (number->string len)
+                          " last-end=" (number->string last-end)))
+          (if (and (> style-id 0) (> len 0) (>= start last-end))
+            (begin
+              (sci-send ed SCI_STARTSTYLING start 31)
+              (sci-send ed SCI_SETSTYLING len style-id)
+              (loop (+ i 1) (max last-end end) (+ applied 1)))
+            (loop (+ i 1) (max last-end end) applied)))
+        (verbose-log! "ts-apply: total=" (number->string count)
+                      " applied=" (number->string applied))))))
 
 (def (ts-tree-changed-ranges old-tree new-tree)
   "Get byte ranges that changed between two trees.
@@ -313,13 +325,18 @@
   "Full re-parse of buffer text and re-highlight.
    Called on initial load and after debounced edits."
   (let ((state (hash-get *buffer-ts-state* buf)))
+    (verbose-log! "ts-reparse: state=" (if state "yes" "nil")
+                  " text-len=" (if text (number->string (string-length text)) "nil"))
     (when state
       (let* ((parser (ts-state-parser state))
              (old-tree (ts-state-tree state))
              (new-tree (ts-parse-string! parser old-tree text)))
+        (verbose-log! "ts-reparse: parser=" (if parser "yes" "nil")
+                      " old-tree=" (if old-tree "yes" "nil")
+                      " new-tree=" (if new-tree "yes" "nil"))
         (when new-tree
           ;; Clear all styles first (set to default)
-          (sci-send ed SCI_STARTSTYLING 0)
+          (sci-send ed SCI_STARTSTYLING 0 31)
           (sci-send ed SCI_SETSTYLING (string-length text) 0)
           ;; Apply highlights
           (ts-highlight-buffer! parser new-tree (ts-state-query state) text ed)
@@ -360,28 +377,29 @@
 ;;; Capture name -> Scintilla style ID mapping
 ;;;============================================================================
 
-;; Style IDs 70-86: reserved for tree-sitter highlights.
-;; These do not conflict with Lexilla styles (0-63) or terminal styles (64-79).
-;; Note: terminal styles use 64-79, so we start tree-sitter at 90.
-(def *ts-style-keyword*     90)
-(def *ts-style-string*      91)
-(def *ts-style-comment*     92)
-(def *ts-style-function*    93)
-(def *ts-style-type*        94)
-(def *ts-style-variable*    95)
-(def *ts-style-constant*    96)
-(def *ts-style-number*      97)
-(def *ts-style-operator*    98)
-(def *ts-style-property*    99)
-(def *ts-style-punctuation* 100)
-(def *ts-style-attribute*   101)
-(def *ts-style-namespace*   102)
-(def *ts-style-constructor* 103)
-(def *ts-style-tag*         104)
-(def *ts-style-escape*      105)
-(def *ts-style-label*       106)
-(def *ts-style-builtin*     107)
-(def *ts-style-preproc*     108)
+;; Style IDs 1-19: tree-sitter highlights.
+;; QScintilla uses 5 style bits (0-31 valid range), so we MUST use low IDs.
+;; Since tree-sitter disables the built-in lexer, styles 1-19 are free.
+;; Style 0 = default text, styles 20-31 reserved for other uses.
+(def *ts-style-keyword*     1)
+(def *ts-style-string*      2)
+(def *ts-style-comment*     3)
+(def *ts-style-function*    4)
+(def *ts-style-type*        5)
+(def *ts-style-variable*    6)
+(def *ts-style-constant*    7)
+(def *ts-style-number*      8)
+(def *ts-style-operator*    9)
+(def *ts-style-property*    10)
+(def *ts-style-punctuation* 11)
+(def *ts-style-attribute*   12)
+(def *ts-style-namespace*   13)
+(def *ts-style-constructor* 14)
+(def *ts-style-tag*         15)
+(def *ts-style-escape*      16)
+(def *ts-style-label*       17)
+(def *ts-style-builtin*     18)
+(def *ts-style-preproc*     19)
 
 (def (ts-capture->style-id capture-name)
   "Map a tree-sitter capture name to a Scintilla style ID."
diff --git a/vendor/chez-qt-ffi-static.ss b/vendor/chez-qt-ffi-static.ss
index d04dab4..5510d58 100644
--- a/vendor/chez-qt-ffi-static.ss
+++ b/vendor/chez-qt-ffi-static.ss
@@ -770,6 +770,7 @@
     ffi-qt-scintilla-receive-string
     ffi-qt-scintilla-set-text ffi-qt-scintilla-get-text ffi-qt-scintilla-get-text-length
     ffi-qt-scintilla-set-lexer-language ffi-qt-scintilla-get-lexer-language
+    ffi-qt-scintilla-lexer-set-color ffi-qt-scintilla-lexer-set-paper ffi-qt-scintilla-lexer-set-font-attr
     ffi-qt-scintilla-set-read-only ffi-qt-scintilla-is-read-only
     ffi-qt-scintilla-set-margin-width ffi-qt-scintilla-set-margin-type
     ffi-qt-scintilla-set-focus
@@ -3007,6 +3008,9 @@
   (define-optional-ffi ffi-qt-scintilla-get-text-length "qt_scintilla_get_text_length" (void*) int)
   (define-optional-ffi ffi-qt-scintilla-set-lexer-language "qt_scintilla_set_lexer_language" (void* string) void)
   (define-optional-ffi ffi-qt-scintilla-get-lexer-language "qt_scintilla_get_lexer_language" (void*) string)
+  (define-optional-ffi ffi-qt-scintilla-lexer-set-color "qt_scintilla_lexer_set_color" (void* int int) void)
+  (define-optional-ffi ffi-qt-scintilla-lexer-set-paper "qt_scintilla_lexer_set_paper" (void* int int) void)
+  (define-optional-ffi ffi-qt-scintilla-lexer-set-font-attr "qt_scintilla_lexer_set_font_attr" (void* int int int) void)
   (define-optional-ffi ffi-qt-scintilla-set-read-only "qt_scintilla_set_read_only" (void* int) void)
   (define-optional-ffi ffi-qt-scintilla-is-read-only "qt_scintilla_is_read_only" (void*) int)
   (define-optional-ffi ffi-qt-scintilla-set-margin-width "qt_scintilla_set_margin_width" (void* int int) void)
diff --git a/vendor/chez-qt-qt.ss b/vendor/chez-qt-qt.ss
index 86d648b..6df1eb8 100644
--- a/vendor/chez-qt-qt.ss
+++ b/vendor/chez-qt-qt.ss
@@ -605,6 +605,7 @@
     qt-scintilla-receive-string
     qt-scintilla-set-text! qt-scintilla-get-text qt-scintilla-get-text-length
     qt-scintilla-set-lexer-language! qt-scintilla-get-lexer-language
+    qt-scintilla-lexer-set-color! qt-scintilla-lexer-set-paper! qt-scintilla-lexer-set-font-attr!
     qt-scintilla-set-read-only! qt-scintilla-read-only?
     qt-scintilla-set-margin-width! qt-scintilla-set-margin-type!
     qt-scintilla-set-focus!
@@ -2819,6 +2820,9 @@
   (define (qt-scintilla-get-text-length sci) (ffi-qt-scintilla-get-text-length sci))
   (define (qt-scintilla-set-lexer-language! sci lang) (ffi-qt-scintilla-set-lexer-language sci lang))
   (define (qt-scintilla-get-lexer-language sci) (ffi-qt-scintilla-get-lexer-language sci))
+  (define (qt-scintilla-lexer-set-color! sci style color) (ffi-qt-scintilla-lexer-set-color sci style color))
+  (define (qt-scintilla-lexer-set-paper! sci style color) (ffi-qt-scintilla-lexer-set-paper sci style color))
+  (define (qt-scintilla-lexer-set-font-attr! sci style bold italic) (ffi-qt-scintilla-lexer-set-font-attr sci style (if bold 1 0) (if italic 1 0)))
   (define (qt-scintilla-set-read-only! sci val) (ffi-qt-scintilla-set-read-only sci (if val 1 0)))
   (define (qt-scintilla-read-only? sci) (not (zero? (ffi-qt-scintilla-is-read-only sci))))
   (define (qt-scintilla-set-margin-width! sci margin width) (ffi-qt-scintilla-set-margin-width sci margin width))
diff --git a/vendor/qt_shim.cpp b/vendor/qt_shim.cpp
index 37a7516..e7310ea 100644
--- a/vendor/qt_shim.cpp
+++ b/vendor/qt_shim.cpp
@@ -6799,6 +6799,11 @@ extern "C" int qt_scintilla_get_text_length(qt_scintilla_t sci) {
     QT_RETURN(int, static_cast<QsciScintilla*>(sci)->text().toUtf8().length());
 }
 
+// Helper: convert Scintilla BGR color int to QColor
+static QColor sci_to_qcolor(int color) {
+    return QColor(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF);
+}
+
 extern "C" void qt_scintilla_set_lexer_language(qt_scintilla_t sci, const char* language) {
     QT_NULL_CHECK_VOID(sci);
     QT_VOID(
@@ -6807,7 +6812,174 @@ extern "C" void qt_scintilla_set_lexer_language(qt_scintilla_t sci, const char* 
         QsciLexer* old = s->lexer();
         QsciLexer* lex = create_lexer_for_language(s, language);
         s->setLexer(lex);  // nullptr disables lexer
-        delete old
+        delete old;
+        // After setLexer, override paper (background) to dark theme.
+        // QsciLexer defaults are light-themed; this makes them dark.
+        // Foreground colors from the lexer subclass are kept as-is
+        // since most are already distinct (blue, green, red, etc).
+        if (lex) {
+            QColor dark_bg(0x1e, 0x1e, 0x2e);
+            lex->setPaper(dark_bg, -1);
+            lex->setDefaultPaper(dark_bg);
+            // Brighten default text for dark background
+            lex->setDefaultColor(QColor(0xcd, 0xd6, 0xf4));
+            lex->setColor(QColor(0xcd, 0xd6, 0xf4), -1);
+            // Apply Catppuccin Mocha-inspired dark theme colors per style.
+            // These apply generically to all QsciLexer subclasses since
+            // style IDs 0-15 are common across most lexers.
+            // Style 0: Default text
+            lex->setColor(QColor(0xcd, 0xd6, 0xf4), 0);
+            // Style 1: Comments (green)
+            lex->setColor(QColor(0xa6, 0xe3, 0xa1), 1);
+            // Style 2: Numbers / line numbers (peach)
+            lex->setColor(QColor(0xfa, 0xb3, 0x87), 2);
+            // Style 3: Strings / single-quoted (yellow)
+            lex->setColor(QColor(0xf9, 0xe2, 0xaf), 3);
+            // Style 4: Keywords / operators (mauve)
+            lex->setColor(QColor(0xcb, 0xa6, 0xf7), 4);
+            // Style 5: Variables / identifiers (blue)
+            lex->setColor(QColor(0x89, 0xb4, 0xfa), 5);
+            // Style 6: Backtick strings / here-docs (teal)
+            lex->setColor(QColor(0x94, 0xe2, 0xd5), 6);
+            // Style 7: Operators / param expansion (flamingo)
+            lex->setColor(QColor(0xf2, 0xcd, 0xcd), 7);
+            // Style 8: Heredoc delimiter (maroon)
+            lex->setColor(QColor(0xeb, 0xa0, 0xac), 8);
+            // Style 9: Scalar variable (sapphire)
+            lex->setColor(QColor(0x74, 0xc7, 0xec), 9);
+            // Style 10: Error / special (red)
+            lex->setColor(QColor(0xf3, 0x8b, 0xa8), 10);
+            // Style 11: Preprocessor / here-doc body (sky)
+            lex->setColor(QColor(0x89, 0xdc, 0xeb), 11);
+            // Style 12: Double-quoted string (yellow, same as 3)
+            lex->setColor(QColor(0xf9, 0xe2, 0xaf), 12);
+            // Style 13: Regex (pink)
+            lex->setColor(QColor(0xf5, 0xc2, 0xe7), 13);
+        }
+    );
+}
+
+// Set lexer language AND apply dark theme colors in one call.
+// fg/bg are Scintilla BGR color ints. style_pairs is "style:color,style:color,..."
+// where each style is an int and color is a hex BGR value.
+// This avoids the need for separate FFI functions that require symbol registration.
+extern "C" void qt_scintilla_set_lexer_with_theme(qt_scintilla_t sci, const char* language,
+                                                    int default_fg, int default_bg,
+                                                    const char* style_spec) {
+    QT_NULL_CHECK_VOID(sci);
+    QT_VOID(
+        auto* s = static_cast<QsciScintilla*>(sci);
+        QsciLexer* old = s->lexer();
+        QsciLexer* lex = create_lexer_for_language(s, language);
+        // Set lexer first (this applies the lexer's default light-theme colors)
+        s->setLexer(lex);
+        delete old;
+        // NOW override colors on the active lexer — after setLexer() has connected signals
+        if (lex) {
+            QColor bg = sci_to_qcolor(default_bg);
+            QColor fg = sci_to_qcolor(default_fg);
+            // Set default paper/color for ALL styles (style -1)
+            lex->setPaper(bg, -1);
+            lex->setColor(fg, -1);
+            // Also set the default paper explicitly for STYLE_DEFAULT
+            lex->setDefaultPaper(bg);
+            lex->setDefaultColor(fg);
+            // Parse style spec: "style:color:bold:italic,..."
+            if (style_spec && style_spec[0]) {
+                std::string spec(style_spec);
+                size_t pos = 0;
+                while (pos < spec.size()) {
+                    size_t comma = spec.find(',', pos);
+                    if (comma == std::string::npos) comma = spec.size();
+                    std::string entry = spec.substr(pos, comma - pos);
+                    int style_id = 0, color = 0, bold = 0, italic = 0;
+                    if (sscanf(entry.c_str(), "%d:%x:%d:%d", &style_id, &color, &bold, &italic) >= 2) {
+                        lex->setColor(sci_to_qcolor(color), style_id);
+                        if (bold || italic) {
+                            QFont f = lex->font(style_id);
+                            if (bold) f.setBold(true);
+                            if (italic) f.setItalic(true);
+                            lex->setFont(f, style_id);
+                        }
+                    }
+                    pos = comma + 1;
+                }
+            }
+        }
+        // Also set via Scintilla API as fallback for areas not covered by lexer
+        s->SendScintilla(QsciScintillaBase::SCI_STYLESETBACK,
+                         QsciScintillaBase::STYLE_DEFAULT, (long)default_bg);
+        s->SendScintilla(QsciScintillaBase::SCI_STYLESETFORE,
+                         QsciScintillaBase::STYLE_DEFAULT, (long)default_fg);
+        s->SendScintilla(QsciScintillaBase::SCI_STYLECLEARALL);
+        // Re-apply per-style colors via lexer (STYLECLEARALL wiped them)
+        if (lex) {
+            QColor bg = sci_to_qcolor(default_bg);
+            QColor fg = sci_to_qcolor(default_fg);
+            lex->setPaper(bg, -1);
+            lex->setColor(fg, -1);
+            if (style_spec && style_spec[0]) {
+                std::string spec(style_spec);
+                size_t pos = 0;
+                while (pos < spec.size()) {
+                    size_t comma = spec.find(',', pos);
+                    if (comma == std::string::npos) comma = spec.size();
+                    std::string entry = spec.substr(pos, comma - pos);
+                    int style_id = 0, color = 0, bold = 0, italic = 0;
+                    if (sscanf(entry.c_str(), "%d:%x:%d:%d", &style_id, &color, &bold, &italic) >= 2) {
+                        lex->setColor(sci_to_qcolor(color), style_id);
+                        if (bold || italic) {
+                            QFont f = lex->font(style_id);
+                            if (bold) f.setBold(true);
+                            if (italic) f.setItalic(true);
+                            lex->setFont(f, style_id);
+                        }
+                    }
+                    pos = comma + 1;
+                }
+            }
+        }
+        // Force re-colorize
+        s->SendScintilla(QsciScintillaBase::SCI_COLOURISE, (unsigned long)0, (long)-1)
+    );
+}
+
+extern "C" void qt_scintilla_lexer_set_color(qt_scintilla_t sci, int style, int color) {
+    QT_NULL_CHECK_VOID(sci);
+    QT_VOID(
+        auto* lex = static_cast<QsciScintilla*>(sci)->lexer();
+        if (lex) {
+            int r = color & 0xFF;
+            int g = (color >> 8) & 0xFF;
+            int b = (color >> 16) & 0xFF;
+            lex->setColor(QColor(r, g, b), style);
+        }
+    );
+}
+
+extern "C" void qt_scintilla_lexer_set_paper(qt_scintilla_t sci, int style, int color) {
+    QT_NULL_CHECK_VOID(sci);
+    QT_VOID(
+        auto* lex = static_cast<QsciScintilla*>(sci)->lexer();
+        if (lex) {
+            int r = color & 0xFF;
+            int g = (color >> 8) & 0xFF;
+            int b = (color >> 16) & 0xFF;
+            lex->setPaper(QColor(r, g, b), style);
+        }
+    );
+}
+
+extern "C" void qt_scintilla_lexer_set_font_attr(qt_scintilla_t sci, int style, int bold, int italic) {
+    QT_NULL_CHECK_VOID(sci);
+    QT_VOID(
+        auto* lex = static_cast<QsciScintilla*>(sci)->lexer();
+        if (lex) {
+            QFont f = lex->font(style);
+            f.setBold(bold != 0);
+            f.setItalic(italic != 0);
+            lex->setFont(f, style);
+        }
     );
 }