eglot-typescript-preset.el

;;; eglot-typescript-preset.el --- Eglot preset for TypeScript -*- lexical-binding: t; -*-

;; Copyright (C) 2026 Michael Olson <mwolson@gnu.org>

;; Version: 1.2.2
;; Author: Michael Olson <mwolson@gnu.org>
;; Maintainer: Michael Olson <mwolson@gnu.org>
;; URL: https://github.com/mwolson/eglot-typescript-preset
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Keywords: typescript, javascript, convenience, languages, tools
;; Package-Requires: ((emacs "30.2") (eglot "1.17"))

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; This package provides a preset for Eglot to work with TypeScript,
;; JavaScript, CSS, Astro, Vue, and Svelte files.  It configures LSP servers
;; and optional linter/formatter integration via rassumfrassum (rass).

;; Prerequisites:
;;
;; - Install typescript-language-server, deno, or tsgo (for TS/JS files)
;; - Install @astrojs/language-server (for Astro files, optional)
;; - Install @vue/language-server (for Vue files, optional)
;; - Install svelte-language-server (for Svelte files, optional)
;; - Optionally install eslint-language-server, biome, oxlint, or oxfmt
;; - Download this file and add it to the load path

;; Quick start (package-vc):
;;
;;   (use-package eglot-typescript-preset
;;     :vc (:url "https://github.com/mwolson/eglot-typescript-preset"))
;;
;; After that, opening TypeScript, JavaScript, CSS, Astro, Vue, or Svelte
;; files will automatically start the LSP server using Eglot.

;;; Code:

(require 'cl-lib)
(require 'json)

(declare-function eglot-client-capabilities "eglot")
(declare-function eglot--major-modes "eglot")
(declare-function eglot--managed-buffers "eglot")
(declare-function eglot--workspace-configuration-plist "eglot")
(declare-function eglot-current-server "eglot")
(declare-function eglot-ensure "eglot")
(declare-function eglot-handle-notification "eglot")
(declare-function eglot-shutdown "eglot")

(defvar eglot-server-programs)
(defvar eglot-workspace-configuration)

(defgroup eglot-typescript-preset nil
  "TypeScript preset for Eglot."
  :group 'eglot
  :prefix "eglot-typescript-preset-")

;;;###autoload
(defcustom eglot-typescript-preset-auto-setup t
  "Whether to automatically set up Eglot for TypeScript when eglot loads.
When non-nil, `eglot-typescript-preset-setup' is called automatically
via an autoloaded `with-eval-after-load' form.  Set to nil before the
package is loaded to suppress automatic setup and call
`eglot-typescript-preset-setup' manually instead."
  :type 'boolean
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-lsp-server 'typescript-language-server
  "LSP server to use for TypeScript and JavaScript files."
  :type '(choice (const :tag "typescript-language-server" typescript-language-server)
                 (const :tag "deno" deno)
                 (const :tag "rass" rass))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-astro-lsp-server 'rass
  "LSP server to use for Astro files."
  :type '(choice (const :tag "astro-ls" astro-ls)
                 (const :tag "rass" rass)
                 (const :tag "Disabled" nil))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-rass-program "rass"
  "Program used when `eglot-typescript-preset-lsp-server' is `rass'."
  :type 'string
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-rass-command nil
  "Exact command vector to run when `eglot-typescript-preset-lsp-server' is `rass'.

When non-nil, this command is used verbatim and no generated preset is written.
The value must include the `rass` executable and any preset or arguments that
should be passed to it."
  :type '(choice
          (const :tag "Use generated preset" nil)
          (restricted-sexp
           :tag "Exact command vector"
           :value ["rass" "tslint"]
           :match-alternatives (eglot-typescript-preset--rass-command-vector-p)))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-astro-rass-command nil
  "Exact command vector to run for Astro when using `rass'.

When non-nil, this command is used verbatim and no generated preset is written."
  :type '(choice
          (const :tag "Use generated preset" nil)
          (restricted-sexp
           :tag "Exact command vector"
           :value ["rass" "tslint"]
           :match-alternatives
           (eglot-typescript-preset--rass-command-vector-p)))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-css-lsp-server 'rass
  "LSP server to use for CSS files."
  :type '(choice (const :tag "vscode-css-language-server"
                   vscode-css-language-server)
                 (const :tag "rass" rass)
                 (const :tag "Disabled" nil))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-css-rass-command nil
  "Exact command vector to run for CSS when using `rass'.

When non-nil, this command is used verbatim and no generated preset is written."
  :type '(choice
          (const :tag "Use generated preset" nil)
          (restricted-sexp
           :tag "Exact command vector"
           :value ["rass" "csstail"]
           :match-alternatives (eglot-typescript-preset--rass-command-vector-p)))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-svelte-lsp-server 'rass
  "LSP server to use for Svelte files."
  :type '(choice (const :tag "svelte-language-server" svelte-language-server)
                 (const :tag "rass" rass)
                 (const :tag "Disabled" nil))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-svelte-rass-command nil
  "Exact command vector to run for Svelte when using `rass'.

When non-nil, this command is used verbatim and no generated preset is written."
  :type '(choice
          (const :tag "Use generated preset" nil)
          (restricted-sexp
           :tag "Exact command vector"
           :value ["rass" "sveltetail"]
           :match-alternatives (eglot-typescript-preset--rass-command-vector-p)))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-vue-lsp-server 'rass
  "LSP server to use for Vue files."
  :type '(choice (const :tag "vue-language-server" vue-language-server)
                 (const :tag "rass" rass)
                 (const :tag "Disabled" nil))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-vue-rass-command nil
  "Exact command vector to run for Vue when using `rass'.

When non-nil, this command is used verbatim and no generated preset is written."
  :type '(choice
          (const :tag "Use generated preset" nil)
          (restricted-sexp
           :tag "Exact command vector"
           :value ["rass" "vuetail"]
           :match-alternatives (eglot-typescript-preset--rass-command-vector-p)))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-rass-generated-directory
  (expand-file-name "eglot-typescript-preset/" user-emacs-directory)
  "Directory where generated `rass` presets are written."
  :type 'directory
  :group 'eglot-typescript-preset)

(defcustom eglot-typescript-preset-rass-max-contextual-presets 50
  "Maximum number of contextual generated `rass` presets to keep.

When more contextual presets are present in the generated preset directory,
older ones are deleted.  Shared presets are not affected."
  :type '(choice (const :tag "Disable cleanup" nil)
                 (integer :tag "Maximum contextual presets"))
  :group 'eglot-typescript-preset)

(defun eglot-typescript-preset--rass-command-vector-p (value)
  "Return non-nil if VALUE is a vector-form literal command."
  (and (vectorp value)
       (> (length value) 0)
       (seq-every-p #'stringp value)))

(defvar eglot-typescript-preset--rass-tools-type
  '(repeat
    (choice
     (const :tag "astro-ls" astro-ls)
     (const :tag "biome" biome)
     (const :tag "eslint" eslint)
     (const :tag "oxfmt" oxfmt)
     (const :tag "oxlint" oxlint)
     (const :tag "svelte-language-server" svelte-language-server)
     (const :tag "tailwindcss-language-server" tailwindcss-language-server)
     (const :tag "tsgo" tsgo)
     (const :tag "typescript-language-server" typescript-language-server)
     (const :tag "vscode-css-language-server" vscode-css-language-server)
     (const :tag "vue-language-server" vue-language-server)
     (restricted-sexp
      :tag "Command vector"
      :value ["command"]
      :match-alternatives (eglot-typescript-preset--rass-command-vector-p))))
  "Shared customize type for rass-tools variables.")

;;;###autoload
(defcustom eglot-typescript-preset-rass-tools
  '(typescript-language-server eslint)
  "Tools included in the generated `rass` preset for TS/JS files.

Each entry may be a supported symbol like `typescript-language-server',
`tsgo', `eslint', `biome', `oxlint', or `oxfmt', or a literal
command vector of strings."
  :type eglot-typescript-preset--rass-tools-type
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-astro-rass-tools
  '(astro-ls eslint tailwindcss-language-server)
  "Tools included in the generated `rass` preset for Astro files.

Same format as `eglot-typescript-preset-rass-tools'."
  :type eglot-typescript-preset--rass-tools-type
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-css-rass-tools
  '(vscode-css-language-server tailwindcss-language-server)
  "Tools included in the generated `rass` preset for CSS files.

Same format as `eglot-typescript-preset-rass-tools'."
  :type eglot-typescript-preset--rass-tools-type
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-svelte-rass-tools
  '(svelte-language-server typescript-language-server tailwindcss-language-server)
  "Tools included in the generated `rass` preset for Svelte files.

Same format as `eglot-typescript-preset-rass-tools'."
  :type eglot-typescript-preset--rass-tools-type
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-vue-rass-tools
  '(vue-language-server typescript-language-server tailwindcss-language-server)
  "Tools included in the generated `rass` preset for Vue files.

Same format as `eglot-typescript-preset-rass-tools'."
  :type eglot-typescript-preset--rass-tools-type
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-tsdk nil
  "Path to the TypeScript SDK `lib' directory.

When non-nil, this is used as a fallback for the Astro, Vue, and Svelte
language servers' `typescript.tsdk' initialization option.
A project-local `node_modules/typescript/lib' always takes
priority.  When both are nil, the package falls back to
`npm root -g'."
  :type '(choice (const :tag "Auto-detect" nil)
                 (directory :tag "TypeScript SDK lib path"))
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-js-project-markers
  '("package.json" "tsconfig.json" "jsconfig.json")
  "Files that indicate a JavaScript or TypeScript project root."
  :type '(repeat string)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-js-modes
  '( jtsx-jsx-mode jtsx-tsx-mode jtsx-typescript-mode
     js-mode js-ts-mode typescript-ts-mode tsx-ts-mode)
  "Major modes for JavaScript and TypeScript files."
  :type '(repeat symbol)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-language-id-overrides
  '((js-mode . "javascript")
    (js-ts-mode . "javascript")
    (jtsx-jsx-mode . "javascriptreact")
    (jtsx-tsx-mode . "typescriptreact")
    (jtsx-typescript-mode . "typescript")
    (tsx-ts-mode . "typescriptreact"))
  "Alist mapping major modes to LSP language ID strings.
Each entry is (MODE . LANGUAGE-ID).  During setup, the `eglot-language-id'
property is set on each MODE symbol so that Eglot sends the correct
languageId to language servers."
  :type '(alist :key-type symbol :value-type string)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-astro-modes '(astro-ts-mode)
  "Major modes for Astro files."
  :type '(repeat symbol)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-css-modes '(css-mode css-ts-mode)
  "Major modes for CSS files."
  :type '(repeat symbol)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-svelte-modes '(svelte-mode svelte-ts-mode)
  "Major modes for Svelte files."
  :type '(repeat symbol)
  :group 'eglot-typescript-preset)

;;;###autoload
(defcustom eglot-typescript-preset-vue-modes '(vue-mode vue-ts-mode)
  "Major modes for Vue files."
  :type '(repeat symbol)
  :group 'eglot-typescript-preset)

(defun eglot-typescript-preset--lsp-server-safe-p (value)
  "Return non-nil if VALUE is a safe `eglot-typescript-preset-lsp-server' value."
  (memq value '(typescript-language-server deno rass)))

;;;###autoload
(put 'eglot-typescript-preset-lsp-server 'safe-local-variable
     (lambda (value) (memq value '(typescript-language-server deno rass))))

(put 'eglot-typescript-preset-lsp-server 'safe-local-variable
     #'eglot-typescript-preset--lsp-server-safe-p)

(defun eglot-typescript-preset--astro-lsp-server-safe-p (value)
  "Return non-nil if VALUE is a safe `eglot-typescript-preset-astro-lsp-server'."
  (memq value '(astro-ls rass nil)))

(put 'eglot-typescript-preset-astro-lsp-server 'safe-local-variable
     #'eglot-typescript-preset--astro-lsp-server-safe-p)

(defun eglot-typescript-preset--css-lsp-server-safe-p (value)
  "Return non-nil if VALUE is safe for `eglot-typescript-preset-css-lsp-server'."
  (memq value '(vscode-css-language-server rass nil)))

(put 'eglot-typescript-preset-css-lsp-server 'safe-local-variable
     #'eglot-typescript-preset--css-lsp-server-safe-p)

(defun eglot-typescript-preset--svelte-lsp-server-safe-p (value)
  "Return non-nil if VALUE is safe for `eglot-typescript-preset-svelte-lsp-server'."
  (memq value '(svelte-language-server rass nil)))

(put 'eglot-typescript-preset-svelte-lsp-server 'safe-local-variable
     #'eglot-typescript-preset--svelte-lsp-server-safe-p)

(defun eglot-typescript-preset--vue-lsp-server-safe-p (value)
  "Return non-nil if VALUE is safe for `eglot-typescript-preset-vue-lsp-server'."
  (memq value '(vue-language-server rass nil)))

(put 'eglot-typescript-preset-vue-lsp-server 'safe-local-variable
     #'eglot-typescript-preset--vue-lsp-server-safe-p)

(defun eglot-typescript-preset--rass-tools-safe-p (value)
  "Return non-nil if VALUE is a safe `eglot-typescript-preset-rass-tools' value.
Only lists of known symbols are considered safe.  Literal command vectors
are excluded because they could execute arbitrary programs."
  (and (listp value)
       (seq-every-p (lambda (item)
                      (memq item '( astro-ls biome eslint oxfmt oxlint
                                    svelte-language-server
                                    tailwindcss-language-server
                                    tsgo
                                    typescript-language-server
                                    vscode-css-language-server
                                    vue-language-server)))
                    value)))

;;;###autoload
(put 'eglot-typescript-preset-rass-tools 'safe-local-variable
     (lambda (value)
       (and (listp value)
            (catch 'unsafe
              (dolist (item value t)
                (unless (memq item '( astro-ls biome eslint oxfmt oxlint
                                      svelte-language-server
                                      tailwindcss-language-server
                                      tsgo
                                      typescript-language-server
                                      vscode-css-language-server
                                      vue-language-server))
                  (throw 'unsafe nil)))))))

(put 'eglot-typescript-preset-rass-tools 'safe-local-variable
     #'eglot-typescript-preset--rass-tools-safe-p)

(put 'eglot-typescript-preset-astro-rass-tools 'safe-local-variable
     #'eglot-typescript-preset--rass-tools-safe-p)

(put 'eglot-typescript-preset-css-rass-tools 'safe-local-variable
     #'eglot-typescript-preset--rass-tools-safe-p)

(put 'eglot-typescript-preset-vue-rass-tools 'safe-local-variable
     #'eglot-typescript-preset--rass-tools-safe-p)

(put 'eglot-typescript-preset-svelte-rass-tools 'safe-local-variable
     #'eglot-typescript-preset--rass-tools-safe-p)

(defun eglot-typescript-preset--project-markers-safe-p (value)
  "Return non-nil if VALUE is safe for project markers.
Checks `eglot-typescript-preset-js-project-markers'."
  (and (listp value)
       (seq-every-p #'stringp value)))

(put 'eglot-typescript-preset-js-project-markers 'safe-local-variable
     #'eglot-typescript-preset--project-markers-safe-p)

(defun eglot-typescript-preset--language-id-overrides-safe-p (value)
  "Return non-nil if VALUE is safe for language ID overrides.
Checks `eglot-typescript-preset-language-id-overrides'."
  (and (listp value)
       (seq-every-p (lambda (entry)
                      (and (consp entry) (symbolp (car entry))
                           (stringp (cdr entry))))
                    value)))

(put 'eglot-typescript-preset-language-id-overrides 'safe-local-variable
     #'eglot-typescript-preset--language-id-overrides-safe-p)

(defun eglot-typescript-preset--find-tsdk ()
  "Find the TypeScript SDK `lib' directory.

Project-local `node_modules/typescript/lib' takes highest priority.
If `eglot-typescript-preset-tsdk' is set, use that as a fallback.
As a last resort, try to find it via `npm root -g'."
  (or (when-let* ((root (eglot-typescript-preset--project-root))
                  (tsdk (expand-file-name "node_modules/typescript/lib" root))
                  ((file-directory-p tsdk)))
        tsdk)
      eglot-typescript-preset-tsdk
      (when-let* ((npm (executable-find "npm"))
                  (global-root (string-trim
                                (shell-command-to-string "npm root -g")))
                  ((not (string-empty-p global-root)))
                  (tsdk (expand-file-name "typescript/lib" global-root))
                  ((file-directory-p tsdk)))
        tsdk)))

(defun eglot-typescript-preset--node-modules-bin-dir ()
  "Return the nearest ancestor `node_modules/.bin' directory if it exists."
  (when-let* ((start (or (eglot-typescript-preset--project-root)
                         (when-let* ((file (buffer-file-name)))
                           (file-name-directory file))))
              (root (locate-dominating-file
                     start
                     (lambda (dir)
                       (file-directory-p
                        (expand-file-name "node_modules/.bin" dir))))))
    (expand-file-name "node_modules/.bin" root)))

(defun eglot-typescript-preset--resolve-executable (name)
  "Resolve executable NAME, preferring the current project's `node_modules'."
  (or (when-let* ((bin-dir (eglot-typescript-preset--node-modules-bin-dir)))
        (let ((exec-path (cons bin-dir exec-path)))
          (executable-find name)))
      (executable-find name)
      name))

(defconst eglot-typescript-preset--min-rass-version "0.3.5"
  "Oldest rassumfrassum version that generated `rass' presets support.")

(defvar eglot-typescript-preset--checked-rass-programs
  (make-hash-table :test 'equal)
  "Rass programs whose version has already been checked this session.")

(defun eglot-typescript-preset--rass-version (program)
  "Return the version that rass PROGRAM reports, or nil if unknown."
  (with-temp-buffer
    (when (eq 0 (ignore-errors (call-process program nil t nil "--version")))
      (goto-char (point-min))
      (when (re-search-forward "\\brass \\([0-9]+\\(?:\\.[0-9]+\\)*\\)" nil t)
        (match-string 1)))))

(defun eglot-typescript-preset--check-rass-version (program)
  "Warn once per session if rass PROGRAM is too old for generated presets."
  (unless (gethash program eglot-typescript-preset--checked-rass-programs)
    (puthash program t eglot-typescript-preset--checked-rass-programs)
    (when-let* ((installed (eglot-typescript-preset--rass-version program))
                ((version< installed eglot-typescript-preset--min-rass-version)))
      (display-warning
       'eglot-typescript-preset
       (format "rassumfrassum %s at %s is older than %s, so per-server \
settings will not apply; please upgrade it"
               installed program eglot-typescript-preset--min-rass-version)
       :warning))))

(defun eglot-typescript-preset--rass-program ()
  "Return the resolved rass program, warning once if it is too old."
  (let ((program (eglot-typescript-preset--resolve-executable
                  eglot-typescript-preset-rass-program)))
    (eglot-typescript-preset--check-rass-version program)
    program))

(defun eglot-typescript-preset--tool-kind-from-name (name)
  "Return the known tool kind for executable NAME, or nil."
  (when name
    (let ((base (downcase (file-name-sans-extension
                           (file-name-nondirectory name)))))
      (cond
       ((string= base "astro-ls") 'astro-ls)
       ((string= base "biome") 'biome)
       ((string= base "deno") 'deno)
       ((member base '("eslint" "eslint-language-server"
                       "vscode-eslint-language-server"))
        'eslint)
       ((string= base "oxfmt") 'oxfmt)
       ((string= base "oxlint") 'oxlint)
       ((member base '("svelteserver" "svelte-language-server"))
        'svelte-language-server)
       ((string= base "tailwindcss-language-server")
        'tailwindcss-language-server)
       ((member base '("tsgo" "typescript-language-server" "tsserver"))
        'typescript-language-server)
       ((member base '("vscode-css-language-server" "css-language-server"
                       "css-languageserver"))
        'vscode-css-language-server)
       ((string= base "vue-language-server") 'vue-language-server)))))

(defun eglot-typescript-preset--rass-tool-command (tool)
  "Return the server command for `rass` TOOL."
  (cond
   ((eq tool 'astro-ls)
    (list (eglot-typescript-preset--resolve-executable "astro-ls")
          "--stdio"))
   ((eq tool 'biome)
    (list (eglot-typescript-preset--resolve-executable "biome")
          "lsp-proxy"))
   ((eq tool 'eslint)
    (list (eglot-typescript-preset--resolve-executable
           "vscode-eslint-language-server")
          "--stdio"))
   ((eq tool 'oxfmt)
    (list (eglot-typescript-preset--resolve-executable "oxfmt")
          "--lsp"))
   ((eq tool 'oxlint)
    (list (eglot-typescript-preset--resolve-executable "oxlint")
          "--lsp"))
   ((eq tool 'svelte-language-server)
    (list (eglot-typescript-preset--resolve-executable
           "svelteserver")
          "--stdio"))
   ((eq tool 'tailwindcss-language-server)
    (list (eglot-typescript-preset--resolve-executable
           "tailwindcss-language-server")
          "--stdio"))
   ((eq tool 'tsgo)
    (list (eglot-typescript-preset--resolve-executable "tsgo")
          "--lsp"
          "--stdio"))
   ((eq tool 'typescript-language-server)
    (list (eglot-typescript-preset--resolve-executable
           "typescript-language-server")
          "--stdio"))
   ((eq tool 'vscode-css-language-server)
    (list (eglot-typescript-preset--resolve-executable
           "vscode-css-language-server")
          "--stdio"))
   ((eq tool 'vue-language-server)
    (list (eglot-typescript-preset--resolve-executable
           "vue-language-server")
          "--stdio"))
   ((vectorp tool)
    (let* ((command (append tool nil))
           (kind (eglot-typescript-preset--tool-kind-from-name (car command))))
      (when kind
        (setcar command
                (eglot-typescript-preset--resolve-executable (car command))))
      command))
   (t
    (user-error "Unsupported `rass` tool entry: %S" tool))))

(defun eglot-typescript-preset--rass-tool-kind (command)
  "Return the supported tool kind for COMMAND, or nil."
  (eglot-typescript-preset--tool-kind-from-name (car command)))

(defun eglot-typescript-preset--rass-json-string (object)
  "Serialize OBJECT to JSON.
Handles `:json-false' by serializing nil as false."
  (decode-coding-string
   (json-serialize object :false-object :json-false) 'utf-8))

(defun eglot-typescript-preset--rass-python-literal (object)
  "Serialize OBJECT to a Python-compatible literal string.

Like JSON but with True/False/None instead of true/false/null."
  (let ((json (eglot-typescript-preset--rass-json-string object)))
    (setq json (replace-regexp-in-string "\\btrue\\b" "True" json t))
    (setq json (replace-regexp-in-string "\\bfalse\\b" "False" json t))
    (replace-regexp-in-string "\\bnull\\b" "None" json t)))

(defun eglot-typescript-preset--rass-generated-dir ()
  "Return the directory used for generated `rass` presets."
  (file-name-as-directory
   (expand-file-name eglot-typescript-preset-rass-generated-directory)))

(defconst eglot-typescript-preset--load-dir
  (when-let* ((file (or load-file-name buffer-file-name)))
    (file-name-directory file))
  "Directory this library was loaded from, captured at load time.")

(defun eglot-typescript-preset--library-dir ()
  "Return directory containing the installed library."
  (or eglot-typescript-preset--load-dir
      (file-name-directory
       (or (locate-library "eglot-typescript-preset")
           default-directory))))

(defun eglot-typescript-preset--path-in-directory-p (path dir)
  "Return non-nil if PATH is inside DIR."
  (when (and path dir)
    (let ((path (file-truename path))
          (dir (file-name-as-directory (file-truename dir))))
      (string-prefix-p dir path))))

(defvar eglot-typescript-preset--template-cache (make-hash-table :test 'equal)
  "Cache for package template contents.")

(defun eglot-typescript-preset--template-string (name)
  "Return template NAME from the installed package."
  (let* ((path (expand-file-name
                name (expand-file-name
                      "templates"
                      (eglot-typescript-preset--library-dir))))
         (attrs (file-attributes path))
         (mtime (file-attribute-modification-time attrs))
         (cached (gethash name eglot-typescript-preset--template-cache)))
    (if (and cached
             (equal (plist-get cached :path) path)
             (equal (plist-get cached :mtime) mtime))
        (plist-get cached :content)
      (let ((content
             (with-temp-buffer
               (insert-file-contents path)
               (buffer-string))))
        (puthash name
                 (list :content content :mtime mtime :path path)
                 eglot-typescript-preset--template-cache)
        content))))

(defun eglot-typescript-preset--render-template (name replacements)
  "Render template NAME using REPLACEMENTS.

REPLACEMENTS is an alist mapping literal placeholder strings to values."
  (let ((template (eglot-typescript-preset--template-string name)))
    (dolist (replacement replacements template)
      (setq template
            (replace-regexp-in-string
             (regexp-quote (car replacement))
             (cdr replacement)
             template
             t t)))))

(defun eglot-typescript-preset--rass-tool-label (tool)
  "Return a stable filename label for `rass` TOOL."
  (let* ((text (cond
                ((symbolp tool)
                 (symbol-name tool))
                ((vectorp tool)
                 (let* ((command (append tool nil))
                        (program (file-name-sans-extension
                                  (file-name-nondirectory (car command))))
                        (base (if (string-empty-p program)
                                  "tool"
                                program)))
                   (if (= (length command) 1)
                       base
                     (format "%s-argv-%s"
                             base
                             (substring (secure-hash 'sha256
                                                     (prin1-to-string tool))
                                        0 12)))))
                (t
                 (user-error "Unsupported `rass` tool entry: %S" tool)))))
    (string-trim
     (replace-regexp-in-string
      "-+"
      "-"
      (replace-regexp-in-string "[^[:alnum:]]+" "-" (downcase text)))
     "-"
     "-")))

(defun eglot-typescript-preset--rass-shared-preset-path (tools)
  "Return the stable shared preset path for TOOLS."
  (let* ((labels (mapcar #'eglot-typescript-preset--rass-tool-label tools))
         (slug (string-join labels "-"))
         (hash (substring (secure-hash 'sha256 (prin1-to-string tools))
                          0 12)))
    (expand-file-name
     (format "rass-preset-shared-%s-%s.py" slug hash)
     (eglot-typescript-preset--rass-generated-dir))))

(defun eglot-typescript-preset--rass-contextual-preset-path (hash-input)
  "Return the contextual preset path for HASH-INPUT."
  (expand-file-name
   (format "rass-preset-contextual-%s.py"
           (secure-hash 'sha256 hash-input))
   (eglot-typescript-preset--rass-generated-dir)))

(defun eglot-typescript-preset--rass-tool-spec (tool)
  "Return metadata for `rass` TOOL."
  (let* ((command (eglot-typescript-preset--rass-tool-command tool))
         (kind (eglot-typescript-preset--rass-tool-kind command))
         (program (car command)))
    (list :tool tool
          :command command
          :kind kind
          :local-node-modules-sensitive
          (and kind
               (stringp program)
               (when-let* ((bin-dir
                            (eglot-typescript-preset--node-modules-bin-dir)))
                 (eglot-typescript-preset--path-in-directory-p
                  program bin-dir))))))

(defun eglot-typescript-preset--write-file-if-changed (path content)
  "Write CONTENT to PATH only when it differs from the existing file."
  (make-directory (file-name-directory path) t)
  (unless (and (file-exists-p path)
               (with-temp-buffer
                 (insert-file-contents path)
                 (string= (buffer-string) content)))
    (with-temp-file path
      (insert content))))

(defun eglot-typescript-preset--cleanup-rass-contextual-presets
    (&optional preserve-path)
  "Delete older contextual generated `rass` presets, preserving PRESERVE-PATH."
  (when-let* (((integerp eglot-typescript-preset-rass-max-contextual-presets))
              ((> eglot-typescript-preset-rass-max-contextual-presets 0))
              (dir (eglot-typescript-preset--rass-generated-dir))
              ((file-directory-p dir)))
    (let ((files (directory-files dir t "^rass-preset-contextual-.*\\.py\\'")))
      (setq files
            (sort files
                  (lambda (a b)
                    (time-less-p
                     (file-attribute-modification-time (file-attributes b))
                     (file-attribute-modification-time (file-attributes a))))))
      (when preserve-path
        (setq files
              (cons preserve-path
                    (delete preserve-path files))))
      (let ((overflow
             (nthcdr eglot-typescript-preset-rass-max-contextual-presets
                     files)))
        (dolist (file overflow)
          (when (file-exists-p file)
            (delete-file file)))))))

(defun eglot-typescript-preset--astro-init-options ()
  "Return initializationOptions for the Astro language server."
  (let ((tsdk (eglot-typescript-preset--find-tsdk)))
    (if tsdk
        `(:contentIntellisense t :typescript (:tsdk ,tsdk))
      '(:contentIntellisense t))))

(defun eglot-typescript-preset--vue-init-options ()
  "Return initializationOptions for the Vue language server."
  (let ((tsdk (eglot-typescript-preset--find-tsdk)))
    (if tsdk
        `(:typescript (:tsdk ,tsdk) :vue (:hybridMode t))
      '(:vue (:hybridMode t)))))

(defun eglot-typescript-preset--svelte-init-options ()
  "Return initializationOptions for the Svelte language server."
  (let ((tsdk (eglot-typescript-preset--find-tsdk)))
    (when tsdk
      `(:configuration (:typescript (:tsdk ,tsdk))))))

(defun eglot-typescript-preset--find-vue-ts-plugin ()
  "Find the @vue/typescript-plugin directory.
Walk up from the buffer's directory looking for node_modules that
contain the plugin.  Fall back to resolving via the
vue-language-server binary."
  (or (when-let* ((file (or (buffer-file-name) default-directory))
                  (start (file-name-directory (expand-file-name file)))
                  (nm (locate-dominating-file
                       start
                       (lambda (d)
                         (file-directory-p
                          (expand-file-name
                           "node_modules/@vue/typescript-plugin" d)))))
                  (dir (expand-file-name
                        "node_modules/@vue/typescript-plugin" nm))
                  ((file-directory-p dir)))
        dir)
      (when-let* ((bin (eglot-typescript-preset--resolve-executable
                        "vue-language-server"))
                  ((not (string= bin "vue-language-server")))
                  (real (file-truename bin))
                  (nm (locate-dominating-file
                       (file-name-directory real)
                       (lambda (d)
                         (file-directory-p
                          (expand-file-name
                           "@vue/typescript-plugin" d)))))
                  (dir (expand-file-name "@vue/typescript-plugin" nm))
                  ((file-directory-p dir)))
        dir)))

(defun eglot-typescript-preset--write-rass-preset (path commands
                                                        init-options
                                                        eslint-logic-p
                                                        vue-ts-plugin)
  "Write a generated `rass` preset to PATH.

COMMANDS is the list of server commands.
INIT-OPTIONS, when non-nil, is a plist to inject into `initialize'.
ESLINT-LOGIC-P, when non-nil, enables ESLint workspace/configuration logic.
VUE-TS-PLUGIN, when non-nil, is the path to @vue/typescript-plugin."
  (eglot-typescript-preset--write-file-if-changed
   path
   (eglot-typescript-preset--render-template
    "rass-preset.tpl.py"
    `(("__SERVERS__"
       . ,(eglot-typescript-preset--rass-json-string
           (vconcat (mapcar #'vconcat commands))))
      ("__INIT_OPTIONS__"
       . ,(if init-options
              (eglot-typescript-preset--rass-python-literal init-options)
            "None"))
      ("__ESLINT_LOGIC__"
       . ,(if eslint-logic-p "True" "False"))
      ("__VUE_TS_PLUGIN__"
       . ,(if vue-ts-plugin
              (format "%S" vue-ts-plugin)
            "None"))))))

(defun eglot-typescript-preset--rass-preset-path (tools rass-command)
  "Return the generated `rass` preset path for TOOLS.

RASS-COMMAND, when non-nil, means use it verbatim instead.
Returns the preset path, or nil when RASS-COMMAND is set."
  (unless rass-command
    (eglot-typescript-preset--rass-preset-path-1 tools)))

(defun eglot-typescript-preset--rass-init-options-for-tools (commands)
  "Return merged initializationOptions for COMMANDS, or nil."
  (let ((has-astro (seq-some (lambda (cmd)
                               (eq (eglot-typescript-preset--rass-tool-kind cmd)
                                   'astro-ls))
                             commands))
        (has-vue (seq-some (lambda (cmd)
                             (eq (eglot-typescript-preset--rass-tool-kind cmd)
                                 'vue-language-server))
                           commands))
        (has-svelte (seq-some (lambda (cmd)
                                (eq (eglot-typescript-preset--rass-tool-kind cmd)
                                    'svelte-language-server))
                              commands)))
    (cond
     (has-astro (eglot-typescript-preset--astro-init-options))
     (has-svelte (eglot-typescript-preset--svelte-init-options))
     (has-vue (eglot-typescript-preset--vue-init-options)))))

(defun eglot-typescript-preset--rass-vue-ts-plugin-for-tools (commands)
  "Return the @vue/typescript-plugin path when COMMANDS need it."
  (let ((has-vue (seq-some (lambda (cmd)
                             (eq (eglot-typescript-preset--rass-tool-kind cmd)
                                 'vue-language-server))
                           commands))
        (has-ts (seq-some (lambda (cmd)
                            (eq (eglot-typescript-preset--rass-tool-kind cmd)
                                'typescript-language-server))
                          commands)))
    (when (and has-vue has-ts)
      (eglot-typescript-preset--find-vue-ts-plugin))))

(defun eglot-typescript-preset--rass-preset-path-1 (tools)
  "Generate and return the `rass` preset path for TOOLS."
  (let* ((tool-specs (mapcar #'eglot-typescript-preset--rass-tool-spec tools))
         (commands (mapcar (lambda (tool-spec)
                             (plist-get tool-spec :command))
                           tool-specs))
         (init-options
          (eglot-typescript-preset--rass-init-options-for-tools commands))
         (vue-ts-plugin
          (eglot-typescript-preset--rass-vue-ts-plugin-for-tools commands))
         (has-eslint (seq-some (lambda (command)
                                 (eq (eglot-typescript-preset--rass-tool-kind
                                      command)
                                     'eslint))
                               commands))
         (contextual-p (or init-options
                           vue-ts-plugin
                           (seq-some
                            (lambda (tool-spec)
                              (plist-get
                               tool-spec :local-node-modules-sensitive))
                            tool-specs)))
         (path (if contextual-p
                   (eglot-typescript-preset--rass-contextual-preset-path
                    (mapconcat #'identity
                               (delq nil
                                     (list
                                      (eglot-typescript-preset--rass-json-string
                                       (vconcat (mapcar #'vconcat commands)))
                                      (when init-options
                                        (eglot-typescript-preset--rass-json-string
                                         init-options))
                                      vue-ts-plugin))
                               "\0"))
                 (eglot-typescript-preset--rass-shared-preset-path tools))))
    (eglot-typescript-preset--write-rass-preset
     path commands init-options has-eslint vue-ts-plugin)
    (when contextual-p
      (eglot-typescript-preset--cleanup-rass-contextual-presets path))
    path))

(defun eglot-typescript-preset--in-indirect-md-buffer-p ()
  "Return non-nil if buffer is an indirect buffer from a markdown buffer."
  (when-let* ((buf (buffer-base-buffer))
              ((buffer-live-p buf)))
    (with-current-buffer buf
      (derived-mode-p 'markdown-mode))))

(defun eglot-typescript-preset--js-project-root-p (dir)
  "Return non-nil if DIR has a JS/TS project marker file."
  (seq-some (lambda (file)
              (file-exists-p (expand-file-name file dir)))
            eglot-typescript-preset-js-project-markers))

(defvar eglot-lsp-context)

(defun eglot-typescript-preset--all-managed-modes ()
  "Return a list of all major modes managed by this preset."
  (append eglot-typescript-preset-js-modes
          eglot-typescript-preset-astro-modes
          eglot-typescript-preset-css-modes
          eglot-typescript-preset-svelte-modes
          eglot-typescript-preset-vue-modes))

(defun eglot-typescript-preset--project-find (dir)
  "Project detection for JavaScript and TypeScript files.

Returns (js-project . ROOT) if DIR is inside a JS/TS project.
Only activates when `eglot-lsp-context' is non-nil and the current
buffer is in a JS/TS-related major mode, so that non-JS buffers
fall through to other project backends."
  (when (and (bound-and-true-p eglot-lsp-context)
             (not (eglot-typescript-preset--in-indirect-md-buffer-p))
             (apply #'derived-mode-p
                    (eglot-typescript-preset--all-managed-modes)))
    (when-let* ((root (locate-dominating-file
                       dir
                       #'eglot-typescript-preset--js-project-root-p)))
      (cons 'js-project root))))

(defun eglot-typescript-preset--project-root ()
  "Return the current buffer's project root."
  (when-let* ((file (buffer-file-name))
              (root (locate-dominating-file
                     (file-name-directory file)
                     #'eglot-typescript-preset--js-project-root-p)))
    (file-name-as-directory root)))

(cl-defmethod project-root ((project (head js-project)))
  "Return root directory of PROJECT."
  (cdr project))

(defun eglot-typescript-preset--server-contact (_interactive)
  "Return the server contact spec for TypeScript/JavaScript LSP."
  (pcase eglot-typescript-preset-lsp-server
    ('rass
     (if eglot-typescript-preset-rass-command
         (append eglot-typescript-preset-rass-command nil)
       (list (eglot-typescript-preset--rass-program)
             (eglot-typescript-preset--rass-preset-path
              eglot-typescript-preset-rass-tools
              eglot-typescript-preset-rass-command))))
    ('deno
     `(,(eglot-typescript-preset--resolve-executable "deno") "lsp"
       :initializationOptions (:enable t :lint t)))
    ('typescript-language-server
     (list (eglot-typescript-preset--resolve-executable
            "typescript-language-server")
           "--stdio"))))

(defun eglot-typescript-preset--astro-server-contact (_interactive)
  "Return the server contact spec for Astro LSP."
  (pcase eglot-typescript-preset-astro-lsp-server
    ('rass
     (if eglot-typescript-preset-astro-rass-command
         (append eglot-typescript-preset-astro-rass-command nil)
       (list (eglot-typescript-preset--rass-program)
             (eglot-typescript-preset--rass-preset-path
              eglot-typescript-preset-astro-rass-tools
              eglot-typescript-preset-astro-rass-command))))
    ('astro-ls
     (let* ((init-options (eglot-typescript-preset--astro-init-options))
            (command (list (eglot-typescript-preset--resolve-executable
                            "astro-ls")
                           "--stdio")))
       (if init-options
           `(,@command :initializationOptions ,init-options)
         command)))))

(defun eglot-typescript-preset--vue-server-contact (_interactive)
  "Return the server contact spec for Vue LSP."
  (pcase eglot-typescript-preset-vue-lsp-server
    ('rass
     (if eglot-typescript-preset-vue-rass-command
         (append eglot-typescript-preset-vue-rass-command nil)
       (list (eglot-typescript-preset--rass-program)
             (eglot-typescript-preset--rass-preset-path
              eglot-typescript-preset-vue-rass-tools
              eglot-typescript-preset-vue-rass-command))))
    ('vue-language-server
     (let* ((init-options (eglot-typescript-preset--vue-init-options))
            (command (list (eglot-typescript-preset--resolve-executable
                            "vue-language-server")
                           "--stdio")))
       (if init-options
           `(,@command :initializationOptions ,init-options)
         command)))))

(defun eglot-typescript-preset--svelte-server-contact (_interactive)
  "Return the server contact spec for Svelte LSP."
  (pcase eglot-typescript-preset-svelte-lsp-server
    ('rass
     (if eglot-typescript-preset-svelte-rass-command
         (append eglot-typescript-preset-svelte-rass-command nil)
       (list (eglot-typescript-preset--rass-program)
             (eglot-typescript-preset--rass-preset-path
              eglot-typescript-preset-svelte-rass-tools
              eglot-typescript-preset-svelte-rass-command))))
    ('svelte-language-server
     (let* ((init-options (eglot-typescript-preset--svelte-init-options))
            (command (list (eglot-typescript-preset--resolve-executable
                            "svelteserver")
                           "--stdio")))
       (if init-options
           `(,@command :initializationOptions ,init-options)
         command)))))

(defun eglot-typescript-preset--workspace-configuration-plist-a
    (orig-fn server &optional path)
  "Advice to provide default workspace configuration.

Calls ORIG-FN with SERVER and PATH, then appends TypeScript
validation defaults when the server manages Astro or Vue buffers.
Existing user settings take precedence because `plist-get' returns
the first match."
  (let ((base-config (funcall orig-fn server path)))
    (if-let* ((buf (car (eglot--managed-buffers server)))
              ((with-current-buffer buf
                 (apply #'derived-mode-p
                        (append eglot-typescript-preset-astro-modes
                                eglot-typescript-preset-svelte-modes
                                eglot-typescript-preset-vue-modes)))))
        (append base-config '(:typescript (:validate (:enable t))))
      base-config)))

(defun eglot-typescript-preset--css-server-contact (_interactive)
  "Return the server contact spec for CSS LSP."
  (pcase eglot-typescript-preset-css-lsp-server
    ('rass
     (if eglot-typescript-preset-css-rass-command
         (append eglot-typescript-preset-css-rass-command nil)
       (list (eglot-typescript-preset--rass-program)
             (eglot-typescript-preset--rass-preset-path
              eglot-typescript-preset-css-rass-tools
              eglot-typescript-preset-css-rass-command))))
    ('vscode-css-language-server
     (list (eglot-typescript-preset--resolve-executable
            "vscode-css-language-server")
           "--stdio"))))

(defun eglot-typescript-preset--our-server-p (server)
  "Return non-nil if SERVER manages modes from this preset."
  (let ((our-modes (eglot-typescript-preset--all-managed-modes)))
    (cl-some (lambda (m) (memq m our-modes))
             (eglot--major-modes server))))

(defvar eglot-typescript-preset--streaming-diag-table (make-hash-table :test #'equal)
  "Per-URI hash of streaming diagnostic tokens.
Each key is a URI string.  Each value is a hash table mapping
token strings to their latest diagnostics vector.")

(defun eglot-typescript-preset--client-capabilities-a (orig-fn server)
  "Advice to inject streaming diagnostics capability.
Calls ORIG-FN with SERVER and adds :$streamingDiagnostics t to the
:textDocument plist so that rass enables pull-to-push streaming.
Only applies to servers managing modes from this preset."
  (let ((caps (funcall orig-fn server)))
    (when (eglot-typescript-preset--our-server-p server)
      (clrhash eglot-typescript-preset--streaming-diag-table)
      (let ((td (plist-get caps :textDocument)))
        (when td
          (plist-put td :$streamingDiagnostics t))))
    caps))

(cl-defmethod eglot-handle-notification
  (server (_method (eql $/streamDiagnostics)) &key uri token diagnostics
          &allow-other-keys)
  "Accumulate per-token diagnostics and publish the merged set.
SERVER receives the merged diagnostics for URI.  Each TOKEN
identifies a sub-server whose DIAGNOSTICS are stored separately,
then all tokens for the URI are combined before forwarding to
the standard publishDiagnostics handler."
  (let ((by-token (or (gethash uri eglot-typescript-preset--streaming-diag-table)
                      (puthash uri (make-hash-table :test #'equal)
                               eglot-typescript-preset--streaming-diag-table))))
    (puthash token (or diagnostics []) by-token)
    (let ((merged []))
      (maphash (lambda (_tok diags) (setq merged (vconcat merged diags))) by-token)
      (eglot-handle-notification server 'textDocument/publishDiagnostics
                                 :uri uri :diagnostics merged))))

(defvar eglot-typescript-preset--setup-done nil
  "Non-nil if `eglot-typescript-preset-setup' has already run.")

;;;###autoload
(defun eglot-typescript-preset-setup ()
  "Set up Eglot for TypeScript, JavaScript, CSS, Astro, Svelte, and Vue modes.

Adds hooks for project detection and Eglot configuration.
Configures `eglot-server-programs' based on the preset settings."
  (interactive)
  (require 'eglot)
  (unless eglot-typescript-preset--setup-done
    (setq eglot-typescript-preset--setup-done t)
    (dolist (entry eglot-typescript-preset-language-id-overrides)
      (put (car entry) 'eglot-language-id (cdr entry)))
    (add-to-list 'eglot-server-programs
                 `(,eglot-typescript-preset-js-modes
                   . eglot-typescript-preset--server-contact))
    (when eglot-typescript-preset-astro-lsp-server
      (add-to-list 'eglot-server-programs
                   `(,eglot-typescript-preset-astro-modes
                     . eglot-typescript-preset--astro-server-contact)))
    (when eglot-typescript-preset-css-lsp-server
      (add-to-list 'eglot-server-programs
                   `(,eglot-typescript-preset-css-modes
                     . eglot-typescript-preset--css-server-contact)))
    (when eglot-typescript-preset-svelte-lsp-server
      (add-to-list 'eglot-server-programs
                   `(,eglot-typescript-preset-svelte-modes
                     . eglot-typescript-preset--svelte-server-contact)))
    (when eglot-typescript-preset-vue-lsp-server
      (add-to-list 'eglot-server-programs
                   `(,eglot-typescript-preset-vue-modes
                     . eglot-typescript-preset--vue-server-contact)))
    (advice-add 'eglot-client-capabilities :around
                #'eglot-typescript-preset--client-capabilities-a)
    (advice-add 'eglot--workspace-configuration-plist :around
                #'eglot-typescript-preset--workspace-configuration-plist-a)
    (add-hook 'project-find-functions
              #'eglot-typescript-preset--project-find)))

;;;###autoload
(progn
  (defvar eglot-typescript-preset--maybe-setup-in-progress nil)
  (defun eglot-typescript-preset--maybe-setup ()
    "Set up Eglot for TS/JS if auto-setup is enabled."
    (unless eglot-typescript-preset--maybe-setup-in-progress
      (let ((eglot-typescript-preset--maybe-setup-in-progress t))
        (when eglot-typescript-preset-auto-setup
          (require 'eglot-typescript-preset nil t)
          (eglot-typescript-preset-setup)))))
  (unless noninteractive
    (if after-init-time
        (eglot-typescript-preset--maybe-setup)
      (add-hook 'after-init-hook #'eglot-typescript-preset--maybe-setup t))))

(provide 'eglot-typescript-preset)
;;; eglot-typescript-preset.el ends here