Readiness: Implemented
Start
IDE setup
Connect your editor to uf lsp, configure formatting on save, and keep its
JavaScript service from reporting TypeScript errors on Flow files. Choose your
editor below; all integrations use the same language server and project settings.
What you need first: uf installed, and a project with a
uf.config.js — Your first project makes one.
During project creation, uf new my-app offers an optional IDE checkbox picker.
Use Space to select several editors and Enter to continue; choose none to skip.
The same setup can be requested without prompts:
uf new my-app --package-manager pnpm --editors vscode,zed --yes
VS Code and Cursor receive Flow validation, completion and formatting settings
and the release extension. The other integrations use the existing uf editor
commands and report any activation or editor-plugin step that must be completed.
If an install fails, the generated project is kept and the command names the IDE
to retry. No setup is performed for an IDE that was not selected.
Choose your editor
| Editor | Install the integration | Configure the project |
|---|---|---|
| VS Code and Cursor | uf editor install vscode or cursor | uf editor setup vscode or cursor |
| Zed (experimental) | uf editor install zed, then install the dev extension | uf editor setup zed |
| JetBrains IDEs | uf editor install jetbrains, then import the LSP4IJ template | Set the Flow language level and TypeScript service in the IDE |
| Neovim and Vim | uf editor install neovim or vim | uf editor setup neovim; configure vim-lsp for Vim |
| Helix | None: Helix has an LSP client | uf editor setup helix |
| Emacs | uf editor install emacs, then load uf.el | uf editor setup emacs |
Run setup from your project root and commit the settings it writes. Setup adds
what is missing and keeps existing values. uf editor setup <editor> --check
writes nothing and fails when settings are missing.
Every integration provides uf's diagnostics, hover, definitions, completion,
references, highlights, rename, document symbols, quick fixes, fix-all and
formatting through uf lsp. The
shared server guidance below explains those features,
and configuration explains how to restart the
server after changing uf.config.js.
VS Code and Cursor
VS Code runs TypeScript's language service over every JavaScript file, so a Flow file gets TypeScript's errors beside uf's. The settings below turn those off for one project, in a settings file you commit, and make uf the formatter — without changing anything in a TypeScript project opened in the same editor.
VS Code: 1. Install the extension
The extension is uniflowed.uf, and Cursor installs the same one. Each uf
release attaches it to its GitHub release, and uf installs the one that
matches it:
uf editor install vscode # or: uf editor install cursor
It downloads uf-vscode-<version>.vsix from the release of the uf you run,
checks it against the published sha256, and installs it with the editor's own
--install-extension. --version picks another release, and --vsix FILE
installs one you already have.
The release job also publishes to the Visual Studio Marketplace and Open VSX,
but only with the publishing tokens the repository owner sets, and no release
has had them yet: until one does, code --install-extension uniflowed.uf
and a search in the Extensions view find nothing. To run your own build,
package it from a checkout and install the .vsix, as
editors/vscode
describes. The extension starts the project's own uf (node_modules/.bin/uf)
before one on PATH, one server per folder that has a uf.config.js.
VS Code: 2. Use Flow language mode for this project
Run uf editor setup vscode (or cursor) and commit the generated settings:
{
"[flow]": {
"editor.defaultFormatter": "uniflowed.uf",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": { "source.fixAll.uf": "explicit" }
}
}
The Flow language mode colors component and hook signatures, parameters,
type annotations, generics, exact objects and JSX. Hover signatures use the same
Flow grammar and your theme. TypeScript's JavaScript providers do not match
these documents, so a second any hover or conflicting semantic coloring does
not appear beside uf's inferred type.
The extension selects Flow through VS Code's public language API only for files
inside the uf folder, including multi-root windows. File associations are left
unchanged. JavaScript opened from other folders and .ts files retain their
own language services.
A custom association such as "*.js": "javascript" is kept. Remove it to use
Flow automatically, or select Flow in the file's language-mode picker.
"uf.workspace.disableBuiltinValidation": false opts out of automatic setup.
The setup notification offers Undo, and uf: Configure Workspace for Flow
shows the full configuration before writing it.
Setup also retains the legacy JavaScript validation/completion switches for custom configurations that keep the JavaScript language mode. Switching off validation alone does not remove TypeScript hover; Flow mode does.
Commit .vscode/extensions.json so teammates are offered the extension:
{ "recommendations": ["uniflowed.uf"] }
VS Code: 4. Check that it worked
- Open a file that uses
componentormatch. The Problems panel lists only entries whose source isuforflow, with nots(…)codes. - Hover a variable. uf's hover shows the type as Flow infers it
(
const first: Flag). - Run Format Document, then
uf fmt --checkin a terminal. If the terminal reports the file, the server read a different configuration. Open the folder that holdsuf.config.js. - The status bar item on the right shows
ufwith the version it is running. Click it for the log and a restart.
VS Code: A TypeScript project in the same editor
Nothing above leaves the project. The settings live in its .vscode/, and the
extension starts no server and writes no settings in a folder without
uf.config.js.
In a multi-root window, Flow mode is selected per document in the uf folder.
The extension does not disable the built-in TypeScript extension or write the
uf folder's associations to the shared .code-workspace file.
Zed
Experimental: install a dev extension; Rust through rustup is required.
The integration is tested on the host, but Zed itself is not run in CI.
Zed starts vtsls, a TypeScript server, for every JavaScript file, and it reads
a Flow file as TypeScript. A project's own .zed/settings.json can
drop vtsls for that project and put uf in its place, both as the language
server and as the formatter.
Zed: 1. Install the extension
Zed has no setting that adds a language server of your own. A server has to
come from an extension, so uf's lives in
editors/zed.
It is not in Zed's extension registry yet, so install it as a dev extension.
uf editor install zed writes its sources to ~/.local/share/uf/editors/zed
(under $XDG_DATA_HOME when that is set). Zed has no command-line install, so
then open the command palette, run zed: install dev extension, and select
that directory, or editors/zed in a uf checkout.
The extension starts uf lsp --cwd <worktree>, using the project's
node_modules/.bin/uf before one on PATH. lsp.uf.binary.path in Zed's
settings overrides both. It serves only a worktree with uf.config.js at its
root. Zed's defaults ask every JavaScript server in every JavaScript project,
so in a project without that file the extension declines, with a message
saying why.
Zed: 2. Keep vtsls off this project
uf editor setup zed writes this, keeping anything the project's file already
says. By hand, commit it as .zed/settings.json in the project:
{
"languages": {
"JavaScript": {
"language_servers": ["uf", "!vtsls", "!typescript-language-server", "..."],
"formatter": { "language_server": { "name": "uf" } },
"format_on_save": "on"
}
}
}
language_servers:"uf"turns uf on,"!name"turns a server off, and"..."means every other server registered for the language, so ESLint and Tailwind keep running if the project uses them. Zed's default for JavaScript is["!typescript-language-server", "vtsls", "..."].- Zed's JavaScript is
.js,.jsx,.mjsand.cjs..jsxis not TSX, so this one block covers every file uf reads. formatternames uf explicitly. With the default"auto", Zed would pick Prettier for JavaScript.format_on_saveis"off"by default.- A project's list replaces your user list. Lists in Zed's settings do not merge. That is what makes this per project: a TypeScript project opened in the same Zed still gets vtsls from the defaults.
To stop Zed asking the uf extension in projects that are not uf projects at
all, add "!uf" to languages.JavaScript.language_servers in your user
settings. The project's list above names "uf" and wins.
Zed: 3. Trust the project
Zed's worktree trust (restricted mode) does not start language servers that a
project's .zed/settings.json asks for until you trust the project. Trust it
when Zed asks, or from the restricted-mode banner.
Zed: 4. Check that it worked
- Open a file with
componentormatch. The diagnostics panel lists onlyufandflowentries. - The language server menu in the status bar lists
uffor the buffer and notvtsls. - Save a file, then run
uf fmt --checkin a terminal. If it reports the file, Zed opened a different folder than the one that holdsuf.config.js.
Editing uf.config.js needs editor: restart language server, because the
server reads it once, at start-up.
Zed: What Zed does not get
Zed highlights JavaScript with a tree-sitter grammar that does not know
component, hook, match or renders. No maintained tree-sitter grammar
for modern Flow exists to ship instead, so those lines are highlighted as the
JavaScript grammar parses them. The language features come from uf and are
unaffected.
Zed: Checked against
The setting syntax, the defaults, the merge behaviour and worktree trust were
read from Zed's documentation and source (assets/settings/default.json, the
settings merge, trusted_worktrees) in September 2026. CI tests the extension's
decisions on the host, and packages it with zed-extension, the tool Zed's
extension registry runs, which loads the manifest and compiles the WebAssembly
as Zed does. No Zed runs in CI.
JetBrains IDEs
Use WebStorm or IntelliJ IDEA Ultimate with JavaScript support.
WebStorm and IntelliJ IDEA Ultimate parse and check JavaScript themselves, and no language server can take that away. What you can do is set the IDE's JavaScript to Flow, turn its TypeScript service off, and add uf's diagnostics and formatting through the LSP4IJ plugin. The verification notes below describe the remaining gaps.
JetBrains: 1. Add uf's server through LSP4IJ
JetBrains' own language server API is only for plugins, and there is no JetBrains plugin for uf. LSP4IJ, Red Hat's LSP client plugin, runs any server:
- Settings → Plugins: install LSP4IJ.
- Settings → Languages & Frameworks → Language Servers → +, then
Template → Import from custom template…, and select the template
directory.
uf editor install jetbrainswrites it to~/.local/share/uf/editors/jetbrains/lsp4ij-template, and a uf checkout has it ateditors/jetbrains/lsp4ij-template. - It fills in
uf lsp --cwd $PROJECT_DIR$for*.js,*.mjsand*.cjs(asjavascript) and*.jsx(asjavascriptreact). The README has the same values to type by hand, and the command for a project-pinned uf.
LSP4IJ keeps its servers in IDE-wide settings, not in the project, so each person does this once per IDE. It cannot be committed.
JetBrains: 2. Set the IDE's JavaScript to Flow
Settings → Languages & Frameworks → JavaScript → JavaScript language version:
Flow. This makes the IDE's parser accept Flow's type annotations. The IDE
stores the choice in the project's .idea/misc.xml, under the
JavaScriptSettings component's languageLevel, so it can be committed if you
commit .idea/.
On the same page, leave Flow package or executable empty and Use Flow
server for unchecked. Those options run Meta's flow server, which needs a
.flowconfig and flow-bin. In a uf project, uf is the Flow server.
JetBrains: 3. Turn the TypeScript service off
Settings → Languages & Frameworks → TypeScript: uncheck TypeScript language service. This is per project.
JetBrains: What is not verified
The JavaScript plugin is closed source, and no JetBrains IDE runs in this repository's CI. These points come from JetBrains' documentation (2026.2) and were not checked in an IDE:
- the exact
languageLevelvalue the Flow choice writes; - whether turning off the TypeScript language service also silences it for
.jsfiles, not only.ts; - whether the IDE's Flow parser accepts
component,hook,matchandrenders. If it does not, the IDE marks those lines as syntax errors itself. Those errors are the IDE's, and uf's diagnostics appear beside them rather than in place of them.
What is tested is the server behind LSP4IJ: tests/library/lsp.test.js
drives uf lsp over the protocol, including --cwd.
JetBrains: Check that it worked
Open the LSP console (View → Tool Windows → Language Servers). uf should
be running for the project. A Flow file's problems should include entries from
uf. Run Reformat Code, then uf fmt --check in a terminal: they agree
when the server read the project's uf.config.js. After editing
uf.config.js, restart the server from its context menu in the console.
Neovim and Vim
Neovim 0.8 or later is required; Vim uses the vim-lsp plugin.
Neovim has no JavaScript server of its own, but most configurations start one:
ts_ls or vtsls through nvim-lspconfig. It reads a
Flow file as TypeScript. uf's Neovim file starts uf lsp in a uf
project and keeps those servers off that project's buffers, while every other
project keeps them.
Neovim and Vim: 1. Start uf in a uf project
uf editor install neovim writes uf.lua to ~/.config/nvim/lua/. It will not
replace a file of that name that uf did not write. Or copy editors/neovim/lua/uf.lua
to ~/.config/nvim/lua/uf.lua, then in init.lua:
require("uf").setup({
cmd = { "uf", "lsp" }, -- or { "./node_modules/.bin/uf", "lsp" }
format_on_save = true,
})
A javascript or javascriptreact buffer gets a client when a uf.config.js
is in its directory or above. The server is started in that directory, which is
where uf lsp reads the project's configuration. :UfRestart restarts it after
you edit uf.config.js.
Neovim and Vim: 2. Keep the TypeScript servers off uf projects
setup does this by default. Its exclude option lists the clients to keep
away from uf projects: ts_ls, tsserver, vtsls, denols and
typescript-tools, whichever you have. When one of them attaches to a buffer
in a uf project, uf.lua detaches it again and drops the diagnostics it
publishes for files there. It does this whatever started the client
(nvim-lspconfig, vim.lsp.enable, a plugin). Buffers outside a uf project are
untouched.
require("uf").setup({ exclude = { "ts_ls", "vtsls" } }) -- only these
require("uf").setup({ exclude = {} }) -- keep them all
On Neovim 0.11 and later you can instead stop ts_ls from starting in a uf
project at all. A root_dir function that never calls on_dir does not start
the client for that buffer:
vim.lsp.config("ts_ls", {
root_dir = function(bufnr, on_dir)
if vim.fs.root(bufnr, { "uf.config.js" }) then
return -- a uf project: uf serves it
end
on_dir(vim.fs.root(bufnr, { "tsconfig.json", "jsconfig.json", "package.json", ".git" }))
end,
})
This replaces nvim-lspconfig's own root_dir for ts_ls, including the part
that skips Deno projects. The markers are then yours to maintain.
Neovim and Vim: Per project instead
To keep the configuration in the project, Neovim's exrc option reads a
.nvim.lua from the directory Neovim starts in, after you approve it once with
:trust. With vim.o.exrc = true in your own configuration, commit a
.nvim.lua that calls require("uf").setup(). uf editor setup neovim writes
it.
Neovim and Vim: Vim
Vim has no LSP client of its own. uf's registration for
vim-lsp is
editors/vim/uf.vim.
uf editor install vim puts it in ~/.vim/plugin/. Otherwise, source it
after vim-lsp is loaded. It runs uf lsp --cwd <root> for a uf
project. vim-lsp chooses servers by file type, not by project, so uf.vim
cannot keep typescript-language-server off a uf project. If
vim-lsp-settings registers that server for you, its documented per-server
disabled option, set in the project's local vimrc, turns it off. That option
is not checked here.
Neovim and Vim: Check that it worked
:checkhealth vim.lsp(or:LspInfo) listsuffor a Flow buffer and nots_ls.:lua vim.diagnostic.open_float()on a finding shows sourceuforflow.:lua vim.lsp.buf.format({ name = "uf" }), thenuf fmt --checkin a terminal: they agree when the server read the project'suf.config.js.
Neovim and Vim: Checked against
uf.lua is run in headless Neovim by the Editors workflow, with a stand-in
server in place of uf lsp and ts_ls. The run checks that uf attaches in a
uf project and starts in its root, that ts_ls is detached there and its
diagnostics dropped, that nothing changes outside a uf project, and that
:UfRestart starts a new client. It has been run on Neovim 0.8, 0.11 and
0.12. The root_dir/on_dir contract and exrc were read from Neovim's
lsp.txt and options.txt. uf.vim is not run by CI.
Helix
Helix gives JavaScript and JSX files typescript-language-server and
nothing else, and it reads a Flow file as TypeScript. A project's own
.helix/languages.toml replaces that list with uf for that project
only.
Helix: 1. Commit .helix/languages.toml
uf editor setup helix writes it. If the project already has a
.helix/languages.toml that configures JavaScript, it prints the lines to add
instead of writing. Or copy editors/helix/languages.toml
into the project. Its substance:
[language-server.uf]
command = "uf"
args = ["lsp"]
required-root-patterns = ["uf.config.js"]
[[language]]
name = "javascript"
roots = ["uf.config.js"]
language-servers = ["uf"]
auto-format = true
[[language]]
name = "jsx"
roots = ["uf.config.js"]
language-servers = ["uf"]
auto-format = true
language-serversreplaces the default list. Helix merges a project'slanguages.tomlinto its own by language name, and a list inside it replaces the default list.["uf"]therefore dropstypescript-language-serverfor this project and no other. Bothjavascript(.js,.mjs,.cjs) andjsxneed the line.auto-formatformats on write through the server. There is noformatterline:uf fmtwrites files and does not read a buffer on stdin, so the server is what formats the buffer.required-root-patternsstarts uf only where the workspace root has auf.config.js. In the project's own file that is always true. It matters if the file is merged into~/.config/helix/languages.toml, which is not recommended, because therelanguage-serverswould replace the JavaScript servers of every project.
Start hx from the project root: the server reads uf.config.js from the
directory it starts in.
Helix: 2. Trust the project, on newer Helix
Helix 25.07.1 reads .helix/languages.toml without asking. Helix's master
branch, after 25.07, reads it and starts the servers it names only once you run
:workspace-trust in the project. It asks again after .helix/ changes.
Helix: Check that it worked
hx --health javascript lists uf as the language server. In a Flow file,
Space + d lists only uf and flow diagnostics. Write the file, then run
uf fmt --check in a terminal: they agree when the server read the project's
uf.config.js. :lsp-restart after editing uf.config.js.
Helix: Checked against
The merge rules, required-root-patterns, the default servers and workspace
trust were read from Helix's languages.toml, helix-loader and the book in
September 2026. No Helix runs in CI.
Emacs
Use Emacs 29 or later with built-in Eglot, or lsp-mode.
Eglot and lsp-mode both give JavaScript buffers typescript-language-server by
default, and it reads a Flow file as TypeScript. uf.el makes uf
the server for a uf project's buffers and leaves every other project with the
server it would have had.
Emacs: 1. Load uf.el
uf editor install emacs writes uf.el to ~/.local/share/uf/editors/emacs
and prints the load-path line. uf.el is also in a uf checkout:
(add-to-list 'load-path "/path/to/uf/editors/emacs")
(require 'uf)
(setq uf-format-on-save t) ; optional
(add-hook 'js-mode-hook #'uf-eglot-ensure) ; Eglot: start it in uf projects
(add-hook 'js-ts-mode-hook #'uf-eglot-ensure)
uf.el makes a
directory with uf.config.js a project.el project. That project root is where
Eglot starts the server, and the command names it again with --cwd.
Emacs: 2. How the TypeScript server stays off
- Eglot.
uf.elputs its entry first ineglot-server-programs. Its contact function answersuf lspin a uf project. Anywhere else it answers with whatever the rest ofeglot-server-programsgives the buffer's mode: typescript-language-server on Emacs 30, and on Emacs 31 therass tsalternative it tries first. A TypeScript project therefore keeps its server. - lsp-mode.
uf.elregisters a client,uf, at priority 1, whose activation function accepts a buffer only in a uf project. lsp-mode starts the highest-priority client that accepts a buffer, andts-lsis -2, so a uf project gets uf and every other project is unchanged.
To say it per project for lsp-mode as well, commit a .dir-locals.el
(uf editor setup emacs writes it when the project has none).
lsp-disabled-clients is marked safe as a directory-local variable, so Emacs
applies it without asking:
((js-mode . ((lsp-disabled-clients . (ts-ls jsts-ls tsgo deno-ls))))
(js-ts-mode . ((lsp-disabled-clients . (ts-ls jsts-ls tsgo deno-ls)))))
Setting eglot-server-programs from .dir-locals.el also works, but it is not
marked safe, so Emacs asks every time. uf.el's contact function makes that
unnecessary.
Emacs: Check that it worked
M-x eglot-events-buffer (or M-x lsp-describe-session) shows uf for a
Flow buffer. M-x flymake-show-buffer-diagnostics lists uf and flow
entries. M-x eglot-format-buffer, then uf fmt --check in a terminal: they
agree when the server read the project's uf.config.js. After editing
uf.config.js, M-x eglot-reconnect (or M-x lsp-workspace-restart).
Emacs: Checked against
Eglot's defaults and lsp-mode's client ids, priorities and
lsp-disabled-clients were read from eglot.el (emacs-30 and master) and
lsp-mode's source in September 2026. The Editors workflow byte-compiles
uf.el with warnings as errors and runs its ERT tests in batch Emacs. Those
tests cover the project root, the command, the fallback to another server
outside a uf project, and lsp-mode's activation. Eglot and lsp-mode themselves
are not started by the tests.
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","source.organizeImports"]},"completionProvider":{"triggerCharacters":["\"","@","."]},"definitionProvider":true,"typeDefinitionProvider":true,"referencesProvider":true,"documentHighlightProvider":true,"renameProvider":{"prepareProvider":true},"documentSymbolProvider":true,"signatureHelpProvider":{"triggerCharacters":["(",",","<"],"retriggerCharacters":[","]},"workspaceSymbolProvider":true,"selectionRangeProvider":true,"linkedEditingRangeProvider":true,"foldingRangeProvider":true}}
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 |
| Type errors | Pushed beside the lint diagnostics once typing pauses for 150 ms, for the edited file first and then every other open file, since any of them may import it. The source is flow, the code is Flow's error code (incompatible-type), and each location the message refers to is relatedInformation, labelled [1] string, [2] number as uf check numbers them. None while the file does not parse: the syntax error is the linter's | the checker uf check runs, over the same modules and suppression comments, so the editor and the terminal report the same errors |
| 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 lint diagnostic with a mechanical answer, a "format this document" action where formatting clears a whitespace finding, and a type error whose rewrite Flow can make inside this file. A rewrite that needs another file's signature, or a package, is not offered | the fix catalogue uf lint --fix uses, and Flow's ast_transforms_of_error for the type rewrites |
| Fix all | source.fixAll.uf: every safe lint fix in the file at once. Type rewrites are not part of it | 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, and a key of uf.config.js; anywhere else in a Flow file, the inferred type under the cursor, printed as Flow | the lint rules, uf's module registry, @uniflowed/config, and Flow's own inference |
| Go to definition | Where the name under the cursor is declared: in the same file, in the file it was imported from, in a package under node_modules, or in the project's flow-typed/ library definitions | Flow's inference |
| Go to type definition | Where the named types in the type under the cursor are declared: for const user: User, the type User | Flow's inference |
| Find references | Every place the name under the cursor is written: in this file, and in every project file that reaches its declaration through an import, transitively. A property is found through the type each access has, so user.name in one file finds name in the type User another declares. Packages under node_modules are not searched | Flow's references service, flow_services_references, the one Flow's own server calls |
| Highlights | The uses of the name under the cursor in this file | the same service, over the one file |
| Rename | prepareRename says which identifier would change; rename edits it in every file find references reaches. A shorthand property keeps its key: renaming user in { user } writes { user: account }. Refused, with the reason, for a name a package under node_modules or a library definition declares, and for a new name that is not an identifier | the same service, and Flow's own AST differ and printer for the edits |
| Document symbols | The file's outline — classes and their members, functions, variables, types — nested as it is written. Answered from the parse, so a @noflow file has one too | Flow's document_symbol_provider |
| Signature help | The call or component the cursor is inside, and which argument it is on. ( opens a call, , moves to the next argument, and < opens a component | Flow's flow_services_type_info::signature_help |
| Workspace symbols | The outline of every project file, flattened, filtered by a case-insensitive substring. Packages under node_modules are left out | the same document-symbol provider, over the files find-references searches |
| Organize imports | source.organizeImports: type imports, then relative value imports, then imports from a lowercase module, then require. Named specifiers from one module are combined and sorted. Not part of source.fixAll | Flow's organize_imports |
| Selection range | The ranges the selection can grow through, innermost first | Flow's selection_range_provider, over the parse |
| Linked editing | The opening and closing names of the JSX tag under the cursor. A cursor on either name links both, and a fragment's brackets do the same | Flow's get_linked_locs |
| Folding | Every multi-line declaration in the outline. A declaration on one line is not a fold | the spans document symbols already reports |
| Completion | In uf.config.js: the keys valid at the cursor, with their documentation and type; the values of a key whose type is a fixed set; and in a tool spec like runtime: "node@26", the names its key takes and, after @, that tool's versions, newest first. In any other Flow file: after value., the members of value's type with their types, and elsewhere the names in scope | @uniflowed/config's Flow type, compiled into uf, so it works in a file that is half-typed; versions from the release lists uf caches under $XDG_CACHE_HOME/uf/index, fetched in the background and never on a request's time. Elsewhere, Flow's own completion service |
The type answers come from the checker uf check runs, over the
same modules: the server reads the whole project the way uf check with no
paths does — every source, what those import, the packages under
node_modules they reach, and flow-typed/ — and keeps it in memory. With
app/main.js importing that file:
app/main.js
// @flow
import { flags } from "./flags.js";
const first = flags[0];
textDocument/hover on `first`
const first: Flag
textDocument/definition on `flags`
app/flags.js 3:14
textDocument/typeDefinition on `first`
app/flags.js 2:13
textDocument/completion after `first.`
toString () => string
valueOf () => boolean
textDocument/references on `flags`
app/flags.js 3:14
app/main.js 2:10
app/main.js 3:15
textDocument/rename `flags` to `features`
app/flags.js 3:14 features
app/main.js 2:10 features
app/main.js 3:15 features
It reads the project when the editor says initialized, in the background, so
the first question may wait for that to finish; after it, a hover is answered
from memory. An edit re-infers the edited file when it is next asked about, and
the files that import it when they are — not the project. What the editor has
open and unsaved is what is asked about, not the file on disk.
A file that does not parse — half a keystroke into an edit — has no inference
to ask, so hover, definitions, references and rename answer nothing until it
parses again.
Completion does answer there: value. is the one place a file is most often
broken, and the completion service reads it as an access being typed.
What it does not answer: inlay hints. Flow's inlay path is a ^? comment, not
a hint drawn beside a parameter, so nothing advertises inlayHintProvider, and
a request for one is refused rather than left waiting:
textDocument/inlayHint
{"code":-32601,"message":"uf lsp does not serve `textDocument/inlayHint`"}
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 editor's section above says 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 either starts the server in the project folder or names it
with uf lsp --cwd <dir>; the two say the same thing.
The project's ignore list does not reach the server either: a file you have
opened is linted, even one uf lint would skip.
Where to go next
Flow, the modern parts is next: the component, hook,
renders, match and enum syntax every example in the manual is written in.
Formatting and linting is where the editor's findings and the
formatting come from, and Type checking is where its type
errors come from.
Edit this pagedocs/app/guide/editors/$page.mdx