Other editors & LSP
How to use other editors & lsp in Paradox Modding Toolkit.
The language server is standard LSP over --stdio. Everything that is not a webview works from neovim, Zed, Helix or any other LSP client, and the server can be embedded in your own application. A web page, which cannot spawn a process at all, gets the language service as a plain library instead (see below).
This page is the orientation. The setup reference that ships with the server and is updated alongside it is packages/server/README.md.
What you get#
Ranked completion, hover docs, go-to-definition, find references, rename, document and workspace symbols, folding, formatting, semantic tokens, inlay hints, and the structural and localization diagnostics, for all three games.
Completion also offers block templates, in every client. When an engine token's script_docs usage: example qualifies, completing the token inserts the block that example shows (if = { limit = { … } … }) instead of the bare name. An example that marks fields # optional offers two, the required fields and every field.
A host that wants a snippet list of its own asks paradox/snippets: the measured skeleton of the folder's definition kind, that kind's child blocks, and the engine tokens legal at the cursor (Protocol Reference).
The display calendar needs no settings plumbing either. A mod that declares one in <mod>/.px-toolkit/calendar.json gets its date inlay hints and hovers in any client, because the server reads the file itself; the calendar initialization option is the fallback (Custom Calendars).
What the server does about your client#
Hovers, code actions and completion inserts adapt to the client automatically, one capability at a time. A plain LSP client that declares nothing gets output it can use rather than affordances it cannot:
snippetSupport(standard LSP, ontextDocument.completion.completionItem). Declare it if your client expands${1:…}tabstops. Without it the same inserts arrive as plain-text skeletons, same block shape, never a literal${.hoverIconsis off by default, and the default is the point: a hover kind badge is a■square rather than a$(codicon)glyph, because a client that does not render theme icons would print the literal text$(symbol-method).fileLinksis off, so every location a hover would link (provenance, variable and saved-scope set sites, define sources,#formatsources, datafunction examples, texture paths) renders as the samefile.txt:12label with no link. No dead links.commandslists thepx.*command ids your client registers, and the server only emits links and code actions for the listed ones. Unlisted, the localization quick fix becomes a realWorkspaceEdit, and the hover's reference count (px.showReferences) and its Examples Wiki link (px.showExamplesWiki) are dropped rather than rendered unclickable.- File watching is the server's job unless you claim it: it registers
workspace/didChangeWatchedFilesdynamically, so external edits re-index without a restart. Index health goes towindow/logMessageeither way.
Nothing has to be configured for any of this. Do not declare the client capability flags, they are how the VS Code client announces itself.
What stays VS Code only: tiger diagnostics, the webview UIs (Project dashboard, event graph, event simulator, GUI widget tree, GUI Editor, Examples Wiki, mod report) and the DDS texture previews.
Install#
Three ways to the same server:
- npm (Node.js 18+):
npm install -g @px-lsp/server, then point your client at thepx-lspbin.--stdiois the default transport,px-lsp --versionanswers without a handshake, and the bundled data cannot get lost. Details: npm Packages. px-lsp-server-<version>.tar.gzfrom the releases page, needs Node.js 18+ on your PATH.px-lsp-win-x64-<version>.zipfrom the same page is the tarball payload plus an unmodified official Node build and apx-lsp.cmdlauncher, so nothing has to be installed first. Point your client atpx-lsp.cmdand pass no arguments.
Extract it anywhere and do not flatten it. The server finds its bundled data at ../data/<gameId>/ relative to dist/server.js, so dist/ and data/ must stay siblings. A lone server.js still starts and still answers requests, it just silently loses the bundled wiki tokens and the completion frequency tables; the startup log line tells you which of the two you have.
Wiring it up#
Three things to get right, and they are the three things that go wrong:
1. Filetypes. Paradox script is plain .txt and localization is plain .yml, so your editor has to be told which files are which. The three language ids are paradox, paradox-loc and paradox-gui. If a file opens with no diagnostics, no highlighting and an empty completion popup, check its filetype first: a .txt that stayed text never reaches the server at all.
2. The mod root. Root markers should be descriptor.mod (CK3) or .metadata (Victoria 3, EU5). If they never match, the server falls back to the first workspace folder, and workspace-mod-only features (reference diagnostics, required-localization checks, the loc quick fix) stay silent because the file belongs to no known mod.
EU5 is the trap here: its content lives under a load-stage folder (in_game/, main_menu/, loading_screen/), but the mod root is still the folder holding .metadata/. Keep the root there, not on in_game/.
3. The game. The server serves one game per instance and there is no auto-detection outside VS Code: set gameId explicitly to "ck3" (the default), "vic3" or "eu5". gamePath and logsPath then describe that game, and Vic3 and EU5 write their script_docs to docs/, not logs/. See Supported Games.
Initialization options mirror the VS Code settings: gameId, gamePath, logsPath, locLanguage, parentPaths, diagnosticsIgnore, diagnosticsIgnorePatterns, scopeInlayHints, hoverDetail, calendar. The full settings shape with every field explained is in the Protocol Reference.
Neovim, copy-paste (0.11+)#
Verified hands-on against neovim 0.12 on a real mod; the committed parity harness in scripts/nvim-parity/ re-checks it before releases.
1. Filetypes. Teach neovim which files are which (anchor the patterns to your mod folders if the generic ones are too broad):
vim.filetype.add({
extension = {
gui = "paradox-gui",
},
pattern = {
[".*/common/.*%.txt"] = "paradox",
[".*/events/.*%.txt"] = "paradox",
[".*/history/.*%.txt"] = "paradox",
[".*/localization/.*%.yml"] = "paradox-loc",
},
})
(If a builtin pattern wins over one of these, move the rules to after/ftdetect/paradox.lua. The patterns are suffix matches, so they also catch EU5's in_game/common/... layout.)
2. The server. With the npm install the cmd is just the bin; adjust paths and the game id:
vim.lsp.config("px_lsp", {
cmd = { "px-lsp" }, -- from `npm install -g @px-lsp/server`; or { "node", "/path/to/dist/server.js" }
filetypes = { "paradox", "paradox-loc", "paradox-gui" },
-- The mod root: the folder holding descriptor.mod (CK3) or .metadata/ (Vic3, EU5).
root_markers = { "descriptor.mod", ".metadata", ".git" },
init_options = {
settings = {
-- "ck3" (default) | "vic3" | "eu5".
gameId = "ck3",
-- The game's data folder ("<steam>/steamapps/common/<Game>/game").
gamePath = "C:/Program Files (x86)/Steam/steamapps/common/Crusader Kings III/game",
-- Folder with the script_docs dumps. Omit for bundled data only.
logsPath = vim.fn.expand("~/Documents/Paradox Interactive/Crusader Kings III/logs"),
locLanguage = "english",
},
},
})
vim.lsp.enable("px_lsp")
Do not set the client capability flags: they are how the VS Code client announces itself, and declaring none of them gives the plain-client behavior described above. On neovim 0.10, use require("lspconfig.configs") with the same cmd/init_options and root_dir = require("lspconfig.util").root_pattern("descriptor.mod", ".metadata").
Failure modes to recognize: a file with no diagnostics, no highlighting and an empty completion popup is almost always a filetype problem. Check :set filetype? first, a .txt that stayed text never reaches the server. If root_markers never match, the server falls back to the first workspace folder and mod-scoped features stay silent. :LspLog carries the server's self-diagnosis lines (resolved data folder, token and definition counts).
Dump your own game data#
More important here than in VS Code. CK3 ships bundled wiki tables and a dump snapshot, Victoria 3 ships a dump snapshot, EU5 ships neither yet. So on EU5 the difference between a working index and a thin list of your own definitions is your own dump.
Launch with -debug_mode, run script_docs in the console, point logsPath at the dump folder, and restart the server.
Embedding the server in an application#
Wiring the server into a mod manager or a custom editor is a supported, documented path, and a different job from configuring an editor:
- Embedding covers the process contract, the initialization options an application should send, and URI and document-sync conventions, with a runnable quickstart on npm Packages.
- The Protocol Reference is the method-by-method reference for the custom
paradox/*requests beyond standard LSP: the mod overview, the event graph and event detail, GUI layout and theparadox/guiSourceEditwriter, scope inference. A plain editor client can ignore all of them. - A host that cannot spawn a process at all, a web page, imports
@px-lsp/server/browserinstead: completion, hover, diagnostics and scope inference against one in-memory document, with no JSON-RPC and no filesystem. npm Packages has the API and the payload sizes, Embedding has the full contract and the list of what a browser build cannot know.
The npm packages are @px-lsp/server (the server, with its bundled per-game data) and @px-lsp/protocol (the wire contract, payload types, settings shapes and pure helpers, with zero dependencies and no vscode import). npm Packages has the install, the package layout and a complete, tested embedding example.