First, for every IDE experience you definitely need flycheck mode:
(use-package flycheck
:diminish flycheck-mode
:init
(global-flycheck-mode))
This will underline syntax errors for you. (It'll use the language server as a backend once lsp-mode runs.)
Next we install and set up "company-mode" for simple autocompletion. Basically, it starts the (minor) company mode after starting "lsp-mode", sets up key bindings and reduces the delay to zero:
(use-package company
:after lsp-mode
:hook (lsp-mode . company-mode)
:bind
(:map company-active-map
("<tab>" . company-complete-selection))
(:map lsp-mode-map
("<tab>" . company-indent-or-complete-common))
:custom
(company-minimum-prefix-length 1)
(company-idle-delay 0.0))
This stuff is for basic JSX/TSX support you'll need to work on React projects. "web-mode" is old-school but it still works for me quite well. Since Emacs 29 you can use "typescript-ts-mode" instead but I haven't tried it out yet:
(use-package web-mode
:config
;; plain HTML may also contain JS
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
;; JSX
(add-to-list 'auto-mode-alist '("\\.jsx\\'" . web-mode))
; TSX
(add-to-list 'auto-mode-alist '("\\.tsx\\'" . web-mode))
;; web-mode overrides these key strokes with "link" and "reload"
;; which I *never* needed. But I love compile mode:
:bind (:map web-mode-map
("C-c C-l" . 'compile)
("C-c C-r" . 'recompile)))
The next stuff is for setting up lsp mode integrating the various language modes and company mode in a single user experience:
(defun gernoti/lsp-mode-setup ()
"Gernot's Emacs: My personal modifications to lsp mode."
(setq lsp-headerline-breadcrumb-segments '(path-up-to-project file symbols))
(lsp-headerline-breadcrumb-mode)
(lsp-treemacs-sync-mode 1))
;; I am still using "web-mode" for react projects. The function
;; activates lsp mode only if the file actually contains
;; javascript/typescript (IIRC it'll also work for TSX):
(defun gernoti/lsp-deferred-maybe-jsx ()
(if (string= web-mode-content-type "jsx")
(lsp-deferred)))
(use-package lsp-mode
:commands (lsp lsp-deferred)
:hook
(lsp-mode . gernoti/lsp-mode-setup)
(js-mode . lsp-deferred)
(typescript-mode . lsp-deferred)
;; We only turn lsp-mode on if web-mode runs in JSX or TSX mode.
(web-mode . gernoti/lsp-deferred-maybe-jsx)
:init
(setq lsp-keymap-prefix "C-c l") ;; Or 'C-l', 's-l'
:config
;; You want this if you want to discover all lsp mode's goodies.
;; Though I found I actually don't use many features, really.
(lsp-enable-which-key-integration t))
At first startup it'll download and run the default js/ts language server. It seems to be working well for me.
You definitely want the "lsp-ui" extensions for jumping around in definitions, too:
(use-package lsp-ui
:after lsp-mode
:hook (lsp-mode . lsp-ui-mode)
:custom
(lsp-ui-doc-position 'bottom)
;; remap "M-." and "M-?" to point to the nice looking lsp-ui
;; replacements
:config
(define-key lsp-ui-mode-map [remap xref-find-definitions] #'lsp-ui-peek-find-definitions)
(define-key lsp-ui-mode-map [remap xref-find-references] #'lsp-ui-peek-find-references))