TypeScript and ESLint (multiple language servers)

TypeScript and ESLint (multiple language servers)

Attach TypeScript and ESLint language servers to the same buffer, keep Prettier formatting, and troubleshoot multi-server setups.

Goal and required Red version

This guide configures typescript-language-server and vscode-eslint-language-server to work together in the same .ts, .tsx, .js, or .jsx buffer while keeping Prettier as the external formatter. TypeScript serves completion, hover, signature help, symbols, and go-to-definition; ESLint publishes lint diagnostics and rule code actions alongside TypeScript's own diagnostics and quick fixes.

LayerWhere it livesResponsibility
Embedded in RedRed binaryTree-sitter syntax support for JS, JSX, TS, and TSX, plus a default typescript server definition
Server executablesYour PATHtypescript-language-server and vscode-eslint-language-server installed separately from Red
Red user configuration~/.config/red/config.toml or $XDG_CONFIG_HOME/red/config.tomlESLint server registration, ESLint workspace/configuration settings, and optional external formatter definitions
Project workspaceProject directorypackage.json, local node_modules (eslint, typescript, prettier), tsconfig.json, eslint.config.js, and prettier.config.js

Red does not automatically discover or register ESLint from a project's eslint.config.js or package.json. Once matching servers are configured in config.toml, Red attaches all of them automatically without an extra multi-LSP flag. Each server's root_markers only select the workspace root directory passed to that server; they do not install or register a server.

Install the required tools

Keep language-server binaries on PATH separate from the packages installed inside your project's node_modules.

1. Install the language-server executables on PATH

Install typescript-language-server, typescript, and vscode-langservers-extracted (which provides vscode-eslint-language-server):

Shell
npm install -g typescript-language-server@6.0.1 typescript@6.0.3 vscode-langservers-extracted@4.8.0
which typescript-language-server
which vscode-eslint-language-server
typescript-language-server --version

Installing typescript alone provides tsc and tsserver, not the typescript-language-server executable that Red launches.

2. Install project dependencies

vscode-eslint-language-server loads ESLint from the project's local node_modules, and typescript-language-server uses the workspace TypeScript compiler when present. Inside your project directory, install the compiler, linter, and formatter:

Shell
npm install --save-dev eslint@10.4.1 @eslint/js@10.0.1 typescript-eslint@8.60.1 typescript@6.0.3 prettier@3.8.3

Create a small TypeScript + ESLint project

If you want a self-contained project to verify the setup before opening a larger repository, create a directory with the following files and install the devDependencies above.

package.jsonJSON
{
  "name": "red-ts-eslint-demo",
  "private": true,
  "type": "module",
  "devDependencies": {
    "@eslint/js": "10.0.1",
    "eslint": "10.4.1",
    "prettier": "3.8.3",
    "typescript": "6.0.3",
    "typescript-eslint": "8.60.1"
  }
}
tsconfig.jsonJSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUnusedLocals": true
  },
  "include": ["src"]
}
eslint.config.jsJavaScript
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import ts from 'typescript-eslint';

export default defineConfig(
  js.configs.recommended,
  ts.configs.recommended,
);
prettier.config.jsJavaScript
export default {
  singleQuote: true,
  semi: true,
};
src/index.tsTypeScript
export const test = 'variable test';
const unusedTest = 'unused variable test';

Confirm that ESLint reports @typescript-eslint/no-unused-vars on src/index.ts from the terminal:

Shell
npx eslint src/index.ts

Configure Red

Red reads its user configuration from $XDG_CONFIG_HOME/red/config.toml when XDG_CONFIG_HOME is set and non-empty, or ~/.config/red/config.toml otherwise. Because Red already embeds a default typescript server definition for .ts, .tsx, .js, .mjs, .cjs, and .jsx, the primary setup keeps that built-in TypeScript server and adds ESLint plus Prettier:

TOML
[formatting]
provider = "auto"

[languages.typescript.formatter]
name = "Prettier"
command = "prettier"
args = ["--stdin-filepath", "{file}"]
root_markers = ["prettier.config.js", "package.json", ".git"]

[lsp.servers.eslint]
command = "vscode-eslint-language-server"
args = ["--stdio"]
documents = [
  { language_id = "javascript", file_extensions = ["js", "mjs", "cjs"] },
  { language_id = "javascriptreact", file_extensions = ["jsx"] },
  { language_id = "typescript", file_extensions = ["ts"] },
  { language_id = "typescriptreact", file_extensions = ["tsx"] },
]
root_markers = ["eslint.config.js", "package.json", ".git"]

[lsp.servers.eslint.settings.""]
validate = "on"
run = "onType"
packageManager = "npm"
workingDirectory = { mode = "auto" }
nodePath = ""
useESLintClass = false
quiet = false
options = {}
experimental = {}
format = false
onIgnoredFiles = "off"
rulesCustomizations = []
codeActionOnSave = { mode = "all" }
problems = { shortenToSingleLine = false }

[lsp.servers.eslint.settings."".codeAction]
disableRuleComment = { enable = true, location = "separateLine", commentStyle = "line" }
showDocumentation = { enable = true }

The [lsp.servers.eslint.settings.""] and [lsp.servers.eslint.settings."".codeAction] tables are required by vscode-eslint-language-server. After initialization, the server sends a workspace/configuration request with section = ""; Red returns the contents of settings."". Without that table, Red replies with null and the ESLint server does not validate buffers or offer rule-disable code actions.

Merge into an existing config.toml safely

  • Avoid duplicate TOML table headers. If your config.toml already has [formatting], [languages.typescript.formatter], or [lsp.servers.eslint], merge the keys into the existing table instead of adding a second header with the same name.
  • Keep the built-in TypeScript server unless you need to customize it. Omit [lsp.servers.typescript] so Red uses its embedded definition. Writing a partial [lsp.servers.typescript] table replaces the built-in entry as a unit and drops any omitted fields or extensions.

Advanced: override the built-in TypeScript server

If you need to customize typescript-language-server arguments, environment variables, or root markers, include the complete definition so you preserve Red's default selectors (including .mjs and .cjs) and root markers:

TOML
[lsp.servers.typescript]
command = "typescript-language-server"
args = ["--stdio"]
documents = [
  { language_id = "javascript", file_extensions = ["js", "mjs", "cjs"] },
  { language_id = "javascriptreact", file_extensions = ["jsx"] },
  { language_id = "typescript", file_extensions = ["ts"] },
  { language_id = "typescriptreact", file_extensions = ["tsx"] },
]
root_markers = ["package.json", "tsconfig.json", "jsconfig.json", ".git"]

Validate your configuration from the terminal:

Shell
red --check-config

Restart Red after changing server definitions or installing server executables. For changes limited to unified language definitions, such as external formatters, you can reload those definitions inside Red:

Red command
:languages reload

Use the right language IDs and selectors

Red uses two distinct sets of identifiers for JavaScript and TypeScript files: LSP language_id values sent to language servers in textDocument/didOpen, and Red's own [languages.ID] table names used for syntax and external formatters.

ExtensionLSP documents language_idRed [languages.ID] key
.js, .mjs, .cjsjavascriptjavascript
.jsxjavascriptreactjsx
.tstypescripttypescript
.tsxtypescriptreacttsx

Keep separate entries in documents for each extension rather than mapping language_id = "typescriptreact" to ["js", "jsx", "ts", "tsx"]. Each attached server receives the language_id from the matching selector entry when a file opens.

If you also format .tsx, .js, and .jsx files with Prettier, add matching formatter tables under Red's language keys ([languages.tsx.formatter], [languages.javascript.formatter], and [languages.jsx.formatter]) using the same Prettier command and arguments.

How Red routes requests across attached servers

  • Attachment and lifecycle: Every configured server whose documents selector matches the file attaches to the buffer. Each attached server receives open, change, save, and close notifications with its own language_id, workspace root, and document version counter.
  • Diagnostics: Push and pull diagnostics remain scoped to the server that produced them and are combined for display. An empty diagnostic report, server reload, or crash clears only that server's diagnostics.
  • Code actions: Pressing Space . or Space c a queries every attached server that advertises codeActionProvider, sending each server only the diagnostics it owns. Red merges the returned actions into one picker and routes the selected edit or command back to its originating server.
  • Single-result requests: Completion, hover, go-to-definition, references, document and workspace symbols, signature help, inlay hints, and rename go to the first ready attached server that advertises the required capability. When multiple attached servers advertise the same capability, Red uses the server configuration name ([lsp.servers.NAME]) in alphabetical order as the tie-breaker. Because eslint does not advertise hover, completion, definition, or symbol providers, those requests go to typescript even though eslint sorts first alphabetically.

Verify TypeScript and ESLint in the same buffer

  1. 1
    Open the file.

    From the project directory, run red -r . src/index.ts.

  2. 2
    Check TypeScript hover.

    Move to test on line 1 (w w) and press K. Red displays the hover popup from typescript-language-server (const test: "variable test"). Press Esc to close the popup.

  3. 3
    Check TypeScript completion.

    Press G then o to open a new line, and type test. (or press Ctrl-Space). String members from typescript-language-server such as slice, trim, and length appear in the completion menu. Press Esc and u to undo the test line.

  4. 4
    Check combined diagnostics.

    Move to unusedTest on line 2 (gg j w) and press D (or press Space d to open the workspace diagnostics picker). Both diagnostics appear for unusedTest: ESLint's 'unusedTest' is assigned a value but never used. (@typescript-eslint/no-unused-vars) and TypeScript's 'unusedTest' is declared but its value is never read. (6133).

Apply an ESLint code action

With the cursor on unusedTest on line 2, press Space . or Space c a. The code-action picker lists actions from both attached servers, including ESLint's rule actions and TypeScript's quick fixes.

Choose Disable @typescript-eslint/no-unused-vars for this line and press Enter. Red applies the workspace edit from vscode-eslint-language-server:

TypeScript
export const test = 'variable test';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const unusedTest = 'unused variable test';

ESLint's @typescript-eslint/no-unused-vars error clears immediately, while TypeScript's own unused-variable diagnostic on unusedTest remains until the variable is read or removed.

Keep Prettier formatting

External formatters remain independent of language-server attachments. With formatting.provider = "auto" (the default) and [languages.typescript.formatter] configured for Prettier, pressing Space f runs Prettier instead of formatting through either language server. Saving with :w also formats when formatting.on_save = true; preserve the user's existing preference.

Red looks for prettier in the project's node_modules/.bin before searching PATH, and passes the current buffer over stdin with --stdin-filepath {file}. Setting format = false under [lsp.servers.eslint.settings.""] also prevents ESLint from advertising LSP formatting if Red ever falls back to LSP formatting.

To verify, change single quotes to double quotes on line 1 (export const test = "variable test";) and press Space f. Red rewrites the line to single quotes and reports formatted with Prettier.

Troubleshoot common setup failures

SymptomCause and fix
TypeScript works, but ESLint diagnostics never appearConfirm which vscode-eslint-language-server succeeds on PATH (note that ~ is not expanded in command). When ESLint cannot start, Red isolates the failure so TypeScript completion, hover, and diagnostics keep working; check Space m (:messages) and red.log in your Red configuration directory. After installing a previously missing executable, restart Red so the cached startup failure is cleared.
red --check-config reports an error or LSP is disabledDuplicate TOML headers such as two [lsp.servers.eslint] tables make the file malformed (CFG002) and start Red in its fail-closed fallback profile with LSP disabled. An invalid field inside one server table reports CFG102 and disables only that server. Fix the reported line and re-run red --check-config.
.tsx or .jsx files miss diagnostics, or .ts files parse as JSXCheck the documents array on both servers. Map .ts to typescript, .tsx to typescriptreact, .js/.mjs/.cjs to javascript, and .jsx to javascriptreact.
ESLint server starts, but reports no diagnostics or code actionsRun npx eslint src/index.ts in the project root to verify local node_modules/eslint and eslint.config.js outside Red. Then confirm [lsp.servers.eslint.settings.""] and [lsp.servers.eslint.settings."".codeAction] are present in config.toml so workspace/configuration does not return null.
TypeScript completion and hover stop working after editing config.tomlA [lsp.servers.typescript] table in config.toml replaces Red's embedded typescript server entry in full, and a [languages.typescript.lsp] block without server = "eslint" also writes to the typescript server key. Remove your custom [lsp.servers.typescript] block to restore Red's built-in default, or use the complete override shown in the advanced section above.

Use the same pattern for other language servers

You can attach any combination of language servers to the same file type by giving each server its own [lsp.servers.NAME] table with overlapping file_extensions or filenames selectors:

  • Diagnostics from all attached servers merge automatically while keeping per-server ownership.
  • Code actions aggregate across every attached server that advertises codeActionProvider.
  • Single-result requests go to the first ready attached server that advertises the capability, using the server key name (NAME) in alphabetical order as a tie-breaker when more than one server supports the same request. Red does not guess a universally “best” server beyond capability checks and configuration-name order, so name server keys intentionally or disable overlapping capabilities in a secondary server's settings when two servers advertise the same single-result feature.

Return to Language tools