Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ Documentation comments belong in the signature file only, never in both. A `///`
alongside one in the `.fsi` is a second copy to keep in step, and the one readers and tooling see
is the signature.

In a `match`, put the shortest arm first:

```fsharp
match tool with
| None -> ValueNone
| Some(_, version) -> ValueSome(FantomasVersion(version.ToLowerInvariant()))
```

The short arm is nearly always the one that gets out of the way, and reading it first says what the
rest of the expression is not about. It is also the order `fsharp_experimental_keep_indent_in_branch`
wants: with the long arm last, its body can hold the indentation of the match instead of stepping in
another level. Nothing here is formatted with that setting on, but writing the arms in the order
that suits it costs nothing.

Never write `<|`. Parenthesise instead:

```fsharp
oneAtATimePerFile request.FilePath (fun () -> task { ... })
```

It reads against the direction everything around it is written in, and it puts no visible boundary
where the argument starts.

## Changelog

When updating `CHANGELOG.md`, add new entries to the **end** of the relevant section (e.g. `### Fixed`), not the top. One entry per issue.
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
- Breaking: a single file that `.fantomasignore` matches is reported the same way as an ignored file found while walking a folder. `fantomas A.fs` on an ignored file printed nothing unless `--verbosity d` was given, where `fantomas ./folder` containing only that file printed `A.fs was ignored.` Both now print it. A `--check` run reports the files it ignored at detailed verbosity whether they were named directly or found in a folder, where before only a directly named file was reported. [#3406](https://github.com/fsprojects/fantomas/pull/3406)
- At detailed verbosity, a run writing to `--out` now notes a file it found already formatted, as a run formatting in place always did. Both paths decide what formatting came to in the same place, so they say the same things about it. [#3406](https://github.com/fsprojects/fantomas/pull/3406)
- Breaking: `Fantomas.Core` is no longer binary compatible with `v7`. Several discriminated unions are structs now, which changes nothing about how they are written or matched, but an assembly compiled against `v7` has to be rebuilt. [#3407](https://github.com/fsprojects/fantomas/pull/3407)
- A setting in `.editorconfig` that carries the `fsharp_` prefix but is not a Fantomas setting now warns instead of being silently ignored, and where the intent is obvious the warning names the spelling that works. This catches a misspelling such as `fsharp_multiline_brackets_style`, and catches prefixing one of the four settings editorconfig itself defines, where `fsharp_max_line_length` never applied and `max_line_length` is the one that does. Settings without the `fsharp_` prefix belong to other tools and are left alone. Every problem in one `.editorconfig` is reported together and only once per run, rather than once per formatted file, and `--verbosity d` writes out every setting the running version supports. [#3401](https://github.com/fsprojects/fantomas/pull/3401)
- `.editorconfig` keys and values are both matched without regard to case, as the editorconfig specification defines them. `FSHARP_MAX_RECORD_WIDTH` and `fsharp_experimental_elmish = True` used to be ignored. [#3401](https://github.com/fsprojects/fantomas/pull/3401)
- The daemon sends a `fantomas/configurationWarning` notification for every format request, naming the settings in the resolved configuration it could not act on, and sending an empty list when there are none so an editor can clear what it showed earlier. Additive to the JSON-RPC contract: a client that does not handle the method ignores it and keeps working. See the [Fantomas.Client changelog](https://github.com/fsprojects/fantomas/blob/main/src/Fantomas.Client/CHANGELOG.md) for the client-side API. [#3401](https://github.com/fsprojects/fantomas/pull/3401)

### Fixed

Expand All @@ -31,6 +34,10 @@
- Given several input paths, a folder whose name contains a dot, such as `fantomas my.stuff src`, was taken for a file and reported as `Failed to format file`. A file with no extension was taken for a folder. Which one a path is, is now asked of the file system rather than guessed from the name. A single input path was already classified this way. [#3406](https://github.com/fsprojects/fantomas/pull/3406)
- A `.fantomasignore` pattern that cannot be matched against a path reported the raw exception, with its stack trace, through `%A`. It now names the file and the ignore file that could not be told apart, and keeps the exception for detailed verbosity. [#3406](https://github.com/fsprojects/fantomas/pull/3406)
- `--profile` reported `Line count: 0` for a file whose line endings are not the ones the platform uses, because it counted occurrences of the platform's newline rather than line breaks. A file saved with line feeds counted nothing on Windows, and one saved with carriage returns counted nothing elsewhere. [#3406](https://github.com/fsprojects/fantomas/pull/3406)
- The daemon held its JSON-RPC message loop while reading an `.editorconfig`, so a request that arrived during it was not read until that finished. It hands the loop back first now, and serves one request at a time per file so that the configuration warnings for a file still arrive in the order the requests did. [#3401](https://github.com/fsprojects/fantomas/pull/3401)
- A value that meant something to one setting decided the outcome for every other setting, because each value was tried against every parser in turn. `fsharp_max_record_width = cr` failed the whole run with `Carriage returns are not valid for F# code`; it is now reported as a value that setting does not accept. A value is only read as the type its own setting has. [#3401](https://github.com/fsprojects/fantomas/pull/3401)
- A misspelling of one of the four settings editorconfig itself defines was silently ignored, where a misspelling of a `fsharp_` setting was reported. `max_line_lenght = 100` now says so. Only names within two edits of a setting Fantomas has are read this way, so settings belonging to other tools, `indent_style` among them, stay silent. [#3401](https://github.com/fsprojects/fantomas/pull/3401)
- A negative number was accepted for any setting that takes one, so `fsharp_max_record_width = -5` formatted to nonsense widths without saying anything. It is now reported like any other value the setting does not accept. [#3401](https://github.com/fsprojects/fantomas/pull/3401)

## [8.0.0-alpha-014] - 2026-08-20

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
2 changes: 1 addition & 1 deletion docs/docs/end-users/Configuration.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ let formatCode input (settings: string) =
Some(parts.[0], parts.[1]))
|> readOnlyDict

parseOptionsFromEditorConfig FormatConfig.Default editorConfigProperties
parseOptionsFromEditorConfig FormatConfig.Default editorConfigProperties |> fst

let! result = CodeFormatter.FormatDocumentAsync(false, input, config)
printf $"%s{result.Code}"
Expand Down
186 changes: 186 additions & 0 deletions docs/docs/end-users/FantomasClient.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
category: End-users
categoryindex: 1
index: 14
---
# Formatting from an editor with Fantomas.Client

The [Fantomas.Client](https://www.nuget.org/packages/Fantomas.Client) NuGet package is for tools that
format someone else's code: editor extensions, language servers, custom build tooling.

It exists because of a versioning problem. If your editor extension referenced
[Fantomas.Core](https://www.nuget.org/packages/Fantomas.Core) directly, everyone using that extension
would be formatting with whatever version you happened to compile against. A repository that pins
Fantomas 6 in its `dotnet-tools.json` would silently be formatted by Fantomas 8, and the diff would
be enormous.

`Fantomas.Client` avoids this. For each file you ask it to format, it looks for the Fantomas the
*user* installed, starts that version as a background daemon, and talks to it over JSON-RPC. Your
tool stays on one version of `Fantomas.Client`, while the formatting is done by the version the
repository asked for.

> The code on this page is illustrative rather than runnable. It is meant to show the shape of the
> API, not to be pasted into a script.

## Formatting a document

```fsharp
open Fantomas.Client.Contracts
open Fantomas.Client.LSPFantomasService

// One service for the lifetime of your tool. It caches a daemon per Fantomas version, mapping each
// folder to the version it resolved, so two folders pinning the same version share one process.
// Creating a service per request would start a new one every time.
let service: FantomasService = new LSPFantomasService()

let request =
{ SourceCode = "let a = 1"
// Must be absolute, and must exist on disk. Its folder is what decides which Fantomas
// version gets used, so a path inside the user's repository is what you want here.
FilePath = "/home/me/MyProject/Library.fs"
// None means "use the .editorconfig that applies to FilePath".
Config = None
Cursor = None }

let response = service.FormatDocumentAsync(request).Result

if response.Code = int FantomasResponseCode.Formatted then
// Content holds the formatted code.
printfn "%s" (Option.defaultValue "" response.Content)
elif response.Code = int FantomasResponseCode.UnChanged then
// The file was already formatted. Content is None, so do not overwrite anything.
()
else
// Error, Ignored, ToolNotFound, FileNotFound, FilePathIsNotAbsolute, DaemonCreationFailed, ...
// Content carries a message explaining what went wrong.
eprintfn "%s" (Option.defaultValue "" response.Content)
```

Every call answers with the same `FantomasResponse`, and `Code` is what you branch on. It is an
`int` rather than a union so the type survives the wire; compare it against `FantomasResponseCode`.
Nothing throws for an unusable path or a missing tool, so there is exactly one place to handle
failure.

## Reacting to a bad `.editorconfig`

A `.editorconfig` can name a setting Fantomas does not have, usually a typo such as
`fsharp_multiline_brackets_style`, or give a setting a value it cannot parse. Formatting still
succeeds, using defaults for whatever could not be read, so without being told the user sees the
setting quietly not apply.

Subscribe to `ConfigurationWarnings` to surface it:

```fsharp
service.ConfigurationWarnings.Add(fun warning ->
if Array.isEmpty warning.Problems then
// Nothing is wrong any more, so clear whatever you showed for this file earlier.
clearWarnings warning.FilePath
else
warning.Problems
|> Array.map (fun problem ->
if problem.Code = int ConfigurationProblemCode.UnknownSetting then
$"%s{problem.Setting} is not a Fantomas setting"
else
$"%s{problem.Setting} does not accept the value %s{problem.Value}")
|> showWarnings warning.FilePath warning.EditorConfigFiles)
```

Worth knowing:

- The event is raised for **every** format request, and before that request answers, with an
empty `Problems` array when the configuration is fine. That is what lets you clear a warning
once the user fixes it.
- You can have as many format requests in flight as you like. Warnings for one file arrive in the
order the daemon received them, because the daemon serves one request at a time per file, so an empty
one never overtakes problems that are still current. Requests for different files are served
concurrently and their warnings interleave; `FilePath` is what tells them apart.
- Nothing is coalesced. Several requests queued for one file each run to completion in turn, so a
tool that fires a burst of them will see latency grow with the length of the queue. None of them
is served stale input, because each request carries its own `SourceCode`, but if you format on
every keystroke you want to be dropping your own superseded requests rather than sending them.
- `EditorConfigFiles` holds the absolute paths of the `.editorconfig` files that contributed. Which
one a given problem came from is not knowable, because editorconfig merges the whole chain into a
single set of properties before Fantomas sees it. There is no line number either, so name the
setting rather than trying to point at it.
- `Source` tells you whether the setting came from a `.editorconfig` on disk or from the `Config`
dictionary your own tool sent along with the request.
- Only Fantomas 8 daemons send these. Against an older one the event simply never fires, so no
version check is needed.
- The event is raised on whichever thread the daemon's message arrived on, never on the thread that
asked for the formatting. Marshal before touching a UI. A handler that throws is swallowed rather
than allowed to fault the connection, so nothing is lost but nothing is reported either.

If you are talking to the daemon yourself rather than through `Fantomas.Client`, the notification
arrives on `fantomas/configurationWarning` carrying one object. The shape of that object is below.
The framing around it is StreamJsonRpc's default, which is JSON-RPC with `Content-Length` headers,
the same as the Language Server Protocol uses, so this is what the payload looks like and not what
goes on the wire byte for byte:

```json
{
"FilePath": "/home/me/MyProject/Library.fs",
"EditorConfigFiles": ["/home/me/MyProject/.editorconfig"],
"Problems": [
{ "Code": 1, "Source": 1, "Setting": "fsharp_multiline_brackets_style", "Value": null },
{ "Code": 2, "Source": 2, "Setting": "fsharp_experimental_elmish", "Value": "not_a_bool" }
]
}
```

`Code` is `1` for a setting Fantomas does not have and `2` for a value it cannot parse; `Source` is
`1` for a `.editorconfig` on disk and `2` for the `Config` dictionary sent with the request.
`Value` is `null` for `Code` `1`, because no value was ever read. `EditorConfigFiles` is empty when
`Problems` is empty.

## Formatting a selection

```fsharp
let selectionRequest =
{ SourceCode = sourceCode
FilePath = "/home/me/MyProject/Library.fs"
Config = None
// Same semantics as the F# compiler's range: one-based lines, zero-based columns.
Range = FormatSelectionRange(1, 0, 3, 12) }

let selectionResponse = service.FormatSelectionAsync(selectionRequest).Result
```

The range that actually got formatted comes back in `SelectedRange`, which can differ from what you
asked for when the selection had leading or trailing whitespace. Use the returned range when you
splice the result back into the document, not the one you sent.

## Keeping the cursor in place

Pass `Cursor` on a format request and the response tells you where that position ended up after
formatting, so the caret does not jump when you format on save.

```fsharp
let request =
{ SourceCode = sourceCode
FilePath = "/home/me/MyProject/Library.fs"
Config = None
Cursor = Some(FormatCursorPosition(4, 2)) }
```

## Discovering settings

`ConfigurationAsync` returns the settings schema of the Fantomas version resolved for a file: every
setting name, its type, default, and description. Because it comes from the daemon rather than from
whatever you compiled against, it is the authoritative list for that repository, and it is what you
want backing a settings UI or a `.editorconfig` completion list.

```fsharp
let schema = service.ConfigurationAsync("/home/me/MyProject/Library.fs").Result
// schema.Content holds JSON describing every setting.
```

## Lifetime

```fsharp
// Stop every daemon this service started. Call it when your tool shuts down.
service.Dispose()
```

`ClearCache` throws away the daemons without disposing the service, which is what you want after the
user changes the Fantomas version in their `dotnet-tools.json`. Otherwise the old process keeps
serving requests for the rest of the session.
2 changes: 1 addition & 1 deletion docs/docs/end-users/Recipes.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ let formatCode input (settings: string) =
Some(parts.[0], parts.[1]))
|> readOnlyDict

parseOptionsFromEditorConfig FormatConfig.Default editorConfigProperties
parseOptionsFromEditorConfig FormatConfig.Default editorConfigProperties |> fst

let! result = CodeFormatter.FormatDocumentAsync(false, input, config)
printf $"%s{result.Code}"
Expand Down
Loading