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.
| Layer | Where it lives | Responsibility |
|---|---|---|
| Embedded in Red | Red binary | Tree-sitter syntax support for JS, JSX, TS, and TSX, plus a default typescript server definition |
| Server executables | Your PATH | typescript-language-server and vscode-eslint-language-server installed separately from Red |
| Red user configuration | ~/.config/red/config.toml or $XDG_CONFIG_HOME/red/config.toml | ESLint server registration, ESLint workspace/configuration settings, and optional external formatter definitions |
| Project workspace | Project directory | package.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):
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 --versionInstalling 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:
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.3Create 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.
{
"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"
}
}{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"noUnusedLocals": true
},
"include": ["src"]
}import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import ts from 'typescript-eslint';
export default defineConfig(
js.configs.recommended,
ts.configs.recommended,
);export default {
singleQuote: true,
semi: true,
};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:
npx eslint src/index.tsConfigure 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:
[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.tomlalready 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:
[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:
red --check-configRestart 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:
:languages reloadUse 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.
| Extension | LSP documents language_id | Red [languages.ID] key |
|---|---|---|
.js, .mjs, .cjs | javascript | javascript |
.jsx | javascriptreact | jsx |
.ts | typescript | typescript |
.tsx | typescriptreact | tsx |
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
documentsselector matches the file attaches to the buffer. Each attached server receives open, change, save, and close notifications with its ownlanguage_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. Becauseeslintdoes not advertise hover, completion, definition, or symbol providers, those requests go totypescripteven thougheslintsorts first alphabetically.
Verify TypeScript and ESLint in the same buffer
- 1Open the file.
From the project directory, run
red -r . src/index.ts. - 2Check TypeScript hover.
Move to
teston line 1 (w w) and press K. Red displays the hover popup fromtypescript-language-server(const test: "variable test"). Press Esc to close the popup. - 3Check TypeScript completion.
Press G then o to open a new line, and type
test.(or press Ctrl-Space). String members fromtypescript-language-serversuch asslice,trim, andlengthappear in the completion menu. Press Esc and u to undo the test line. - 4Check combined diagnostics.
Move to
unusedTeston line 2 (gg j w) and press D (or press Space d to open the workspace diagnostics picker). Both diagnostics appear forunusedTest: 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:
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
| Symptom | Cause and fix |
|---|---|
| TypeScript works, but ESLint diagnostics never appear | Confirm 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 disabled | Duplicate 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 JSX | Check 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 actions | Run 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.toml | A [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.