Start
Editors
uf lsp is uf's language server, and every integration under
editors/ is a client of
it. What an editor shows — a lint finding under a line, a file formatted on save,
a quick fix — comes from the same crates uf lint and uf fmt run in a terminal,
so the editor and CI cannot disagree about a file.
What you will be able to do: set up your editor, know exactly what the server answers and what it does not, and make sure it formats and lints to your project's configuration rather than to uf's defaults.
What you need first: uf installed, and a project with a
uf.config.js — Your first project makes one.
What the server answers
A session with one file, app/flags.js, which declares export type Flag = bool;:
initialize
{"serverInfo":{"name":"uf-lsp","version":"…"},"capabilities":{"textDocumentSync":1,"documentFormattingProvider":true,"hoverProvider":true,"codeActionProvider":{"codeActionKinds":["quickfix","source.fixAll.uf"]}}}
textDocument/publishDiagnostics
flow/deprecated-type severity 1 4:20 the `bool` type alias is deprecated; write `boolean`
textDocument/codeAction
quickfix Replace `bool` with `boolean` (preferred)
source.fixAll.uf Fix all uf lint problems in this file
textDocument/hover on `bool`
**`flow/deprecated-type`** · error
the `bool` type alias is deprecated; write `boolean`
---
Flow's own lint set · default `error` · decided from the source text
| What it is | Where it comes from | |
|---|---|---|
| Diagnostics | Pushed when a document opens and on every change, cleared when it closes. The source is uf and the code is the rule id | uf_lint — the rules uf lint runs, at the levels your uf.config.js sets. Not type errors: those are uf check's |
| Formatting | One edit over the whole document | uf_fmt, the printer uf fmt uses, with your fmt options — not the editor's tab settings |
| Quick fixes | quickfix actions for a diagnostic with a mechanical answer, and a "format this document" action where formatting clears a whitespace finding | the fix catalogue uf lint --fix uses |
| Fix all | source.fixAll.uf: every safe fix in the file at once | the same catalogue; the unsafe tier is a quick fix you pick, never part of fix-all |
| Hover | The rule behind a diagnostic, what an import specifier names, what a rule id in a suppression comment means | the lint rules and uf's module registry |
What it does not answer: completion, go-to-definition, rename, find-references, document symbols, and the type at a position. It advertises none of them, and a request for one is refused rather than left waiting:
textDocument/completion
{"code":-32601,"message":"uf lsp does not serve `textDocument/completion`"}
Completion for uf.config.js is being written under
#941. Until the server answers
types, the terminal is where they are: uf check.
Which configuration it reads
The server reads the project's configuration once, when it starts: from the
directory --cwd names, or its working directory, walking up to the nearest
uf.config.js, package.json or .git. Three consequences:
- Editing
uf.config.jsneeds a restart. The server has no way to be told. The VS Code extension watches the file and restarts by itself; every other integration says below how to restart it. - A configuration it cannot read is not reported. The server falls back to
uf's defaults, and the editor formats and lints to those without a word. The
check is to format a file in the editor and run
uf fmt --checkin a terminal: if they disagree, the server is reading something else. - The walk stops at the first
package.json. In a workspace, a member with its ownpackage.jsonand nouf.config.jsgets uf's defaults, not the root's configuration.
Every integration starts the server in the project folder, which is correct
however the directory is chosen. The READMEs under editors/ say that
uf lsp --cwd is ignored; it is honoured, and
#998 is correcting them.
The project's ignore list does not reach the server either: a file you have
opened is linted, even one uf lint would skip.
VS Code and Cursor
An extension in editors/vscode,
built from source. It is not published to the Marketplace or Open VSX yet
(#977):
cd editors/vscode
npm install
npx @vscode/vsce package # writes uf-0.0.0.vsix
code --install-extension uf-0.0.0.vsix
Cursor installs the same file with cursor --install-extension uf-0.0.0.vsix.
It activates in a workspace containing uf.config.js and starts one server per
workspace folder that has a uf.config.js at its root, in that folder. A folder
without one gets no server, and a uf project nested below the folder you opened is
not picked up — open that folder, or add it to the workspace.
| Setting | Default | |
|---|---|---|
uf.server.path | "" | The uf binary, absolute or relative to the workspace folder. Empty means search: node_modules/.bin/uf in the folder, then PATH |
uf.formatOnSave | false | Format with the server on save |
uf.trace.server | "off" | messages or verbose logs the traffic to the uf output channel |
A uf.server.path that points nowhere is an error naming the path, not a quiet
fall back to another uf. To apply every safe fix on save:
"editor.codeActionsOnSave": { "source.fixAll.uf": "explicit" }
uf: Restart Language Server re-resolves the binary and restarts every server;
uf: Show Language Server Log lists every path it searched. The extension's
logic — finding the binary, what counts as a project, the command it starts — is
tested in tests/library/vscode-extension.test.js; the wiring inside VS Code
itself is not, because no editor host runs in CI.
Neovim
editors/neovim/lua/uf.lua,
on core vim.lsp.start, with no plugin manager and no nvim-lspconfig. Copy it to
~/.config/nvim/lua/uf.lua, then:
require("uf").setup({
cmd = { "./node_modules/.bin/uf", "lsp" }, -- optional; "uf" on PATH by default
format_on_save = true, -- optional; off by default
})
A buffer gets a client when its filetype is javascript or javascriptreact and a
uf.config.js is found in or above its directory; the server starts in that
directory. Fix all is
vim.lsp.buf.code_action({ context = { only = { "source.fixAll" } }, apply = true }).
The README has the vim.lsp.config form for a setup that already uses it.
Vim
editors/vim/uf.vim,
a registration for vim-lsp:
Plug 'prabirshrestha/vim-lsp'
source /path/to/uf/editors/vim/uf.vim
vim-lsp starts a server in Vim's working directory, so start Vim from the project
root, or set let g:uf_cd_to_root = 1 before the source line, which starts the
server through /bin/sh -c 'cd <root> && exec uf lsp'. g:uf_executable names a
different binary. Format on save is a commented-out autocommand at the bottom of
the file; restart with :LspStopServer uf and reopen the file.
Helix
editors/helix/languages.toml,
copied into the project or merged into ~/.config/helix/languages.toml:
mkdir -p .helix && cp path/to/uf/editors/helix/languages.toml .helix/languages.toml
It replaces Helix's default servers for JavaScript and JSX with uf, roots them at
uf.config.js, and formats on write. Restart with :lsp-restart.
Emacs
editors/emacs/uf.el,
for Eglot, built into Emacs 29, or lsp-mode:
(add-to-list 'load-path "/path/to/uf/editors/emacs")
(require 'uf)
Then M-x uf-start in a file inside a uf project. uf.el makes a directory with a
uf.config.js a project.el project, which is what starts Eglot's server in the
right directory. uf-executable names a different binary; restart with
M-x eglot-reconnect.
Zed
Not working. editors/zed
holds the extension manifest, and a Zed extension that provides a language server
also needs a Rust half compiled to WebAssembly, which is not written
(#977). Meanwhile uf fmt,
uf lint and uf check run as tasks from Zed's terminal.
Where to go next
Build a reading list is next: one application end to end, written with the server's findings in front of you. Formatting and linting is where those findings and the formatting come from, and Type checking is the half an editor does not show yet.