A tree-sitter-based Emacs major mode for F# development.
Requires Emacs 29.1+ with tree-sitter support.
The package is available on MELPA and MELPA Stable.
M-x package-install RET fsharp-ts-mode RET
Or with use-package:
(use-package fsharp-ts-mode
:ensure t)To install the development version directly from GitHub:
(use-package fsharp-ts-mode
:vc (:url "https://github.com/bbatsov/fsharp-ts-mode" :rev :newest))Clone the repository and add it to your load-path:
(add-to-list 'load-path "/path/to/fsharp-ts-mode")
(require 'fsharp-ts-mode)Install the required F# tree-sitter grammars:
M-x fsharp-ts-mode-install-grammars
This installs both the fsharp grammar (for .fs and .fsx files) and
the fsharp-signature grammar (for .fsi files) from ionide/tree-sitter-fsharp.
- Syntax highlighting (font-lock) via tree-sitter, organized into 4 levels
- Indentation via tree-sitter
- Imenu support with fully-qualified names
- Navigation (
beginning-of-defun,end-of-defun,forward-sexp) - F# Interactive (REPL) with tree-sitter highlighting for input
- .NET API documentation lookup at point
- Compilation error parsing for
dotnet buildoutput - Prettify symbols (
->to→,funtoλ, etc.) - Eglot integration for the F# Language Server
- Switch between
.fsand.fsifiles withC-c C-a - Shift region left/right for quick re-indentation
- Auto-detect indentation offset from file contents
- dotnet CLI integration (build, test, run, clean, format, restore, watch mode)
- Build directory awareness (prompts to switch from
bin//obj/to source) - Outline mode integration (Emacs 30+)
- Bug report helpers
;; Change indentation offset (default: 4)
(setq fsharp-ts-indent-offset 2)
;; Auto-guess the indent offset from file contents (default: nil)
(setq fsharp-ts-guess-indent-offset t)
;; Enable prettify-symbols-mode
(add-hook 'fsharp-ts-mode-hook #'prettify-symbols-mode)Syntax highlighting is organized into 4 levels, controlled by
treesit-font-lock-level (default: 3):
| Level | Features |
|---|---|
| 1 | Comments, definitions (function/value/type/member names) |
| 2 | Keywords, strings, type annotations, DU constructors |
| 3 | Attributes, builtins, constants (true/false), numbers, escape sequences |
| 4 | Operators, brackets, delimiters, all variables, properties, function calls |
;; Maximum highlighting (includes operators, all variables, function calls)
(setq treesit-font-lock-level 4)You can also toggle individual font-lock features without changing the level. Each level is a group of named features -- you can enable or disable them selectively:
;; Enable function call highlighting (level 4) while keeping level 3 default
(add-hook 'fsharp-ts-mode-hook
(lambda () (treesit-font-lock-recompute-features '(function) nil)))
;; Disable operator highlighting
(add-hook 'fsharp-ts-mode-hook
(lambda () (treesit-font-lock-recompute-features nil '(operator))))The available feature names for .fs/.fsx files are: comment,
definition, keyword, string, type, attribute, builtin,
constant, escape-sequence, number, operator, bracket,
delimiter, variable, property, function.
Note: Signature files (.fsi) use a separate tree-sitter grammar with
a reduced set of font-lock rules. Only comment, definition, keyword,
string, type, bracket, delimiter, and variable are available for
.fsi buffers. Face customizations via hooks need to target both modes if
you want them to apply everywhere:
(dolist (hook '(fsharp-ts-mode-hook fsharp-ts-signature-mode-hook))
(add-hook hook #'my-fsharp-faces))Tree-sitter modes use the standard font-lock-*-face faces. You can
customize them globally or locally for F# buffers:
;; Globally change how function names look
(set-face-attribute 'font-lock-function-name-face nil :weight 'bold)
;; Override faces only in fsharp-ts-mode buffers
(defun my-fsharp-faces ()
(face-remap-add-relative 'font-lock-keyword-face :foreground "#ff6600")
(face-remap-add-relative 'font-lock-type-face :foreground "#2aa198"))
(add-hook 'fsharp-ts-mode-hook #'my-fsharp-faces)fsharp-ts-mode works with Eglot out of the box. For basic usage, install
FsAutoComplete manually and
enable Eglot:
(add-hook 'fsharp-ts-mode-hook #'eglot-ensure)For a richer experience, load fsharp-ts-eglot which provides automatic
server installation, custom LSP commands, and fine-grained feature toggles:
(require 'fsharp-ts-eglot)
(add-hook 'fsharp-ts-mode-hook #'eglot-ensure)FsAutoComplete will be downloaded automatically on first use. To pin a specific version instead of always fetching the latest:
(setq fsharp-ts-eglot-server-version "0.76.0")Individual FsAutoComplete features can be toggled via defcustoms:
;; Disable the linter
(setq fsharp-ts-eglot-linter nil)
;; Enable pipeline type hints (off by default)
(setq fsharp-ts-eglot-pipeline-hints t)
;; Disable inlay hints
(setq fsharp-ts-eglot-inlay-hints nil)
;; Enable the simplify-name analyzer
(setq fsharp-ts-eglot-simplify-name-analyzer t)Available toggles: fsharp-ts-eglot-linter,
fsharp-ts-eglot-unused-opens-analyzer,
fsharp-ts-eglot-unused-declarations-analyzer,
fsharp-ts-eglot-simplify-name-analyzer,
fsharp-ts-eglot-enable-analyzers,
fsharp-ts-eglot-code-lenses,
fsharp-ts-eglot-inlay-hints,
fsharp-ts-eglot-pipeline-hints.
| Key / Command | Description |
|---|---|
fsharp-ts-eglot-signature-at-point |
Display type signature of symbol at point |
fsharp-ts-eglot-f1-help |
Open MSDN docs for symbol (falls back to .NET search) |
fsharp-ts-eglot-generate-doc-comment |
Generate XML doc comment stub |
File ordering matters in F# projects. These commands manipulate the current
file's position in the .fsproj:
| Command | Description |
|---|---|
fsharp-ts-eglot-fsproj-move-file-up |
Move file up in compilation order |
fsharp-ts-eglot-fsproj-move-file-down |
Move file down in compilation order |
fsharp-ts-eglot-fsproj-add-file |
Add current file to the project |
fsharp-ts-eglot-fsproj-remove-file |
Remove current file from the project |
fsharp-ts-lens.el shows inferred type signatures as inline overlays after
function definitions, similar to Ionide's LineLens:
(require 'fsharp-ts-lens)
(add-hook 'fsharp-ts-mode-hook #'fsharp-ts-lens-mode)Overlays are refreshed on save and can be updated manually with
M-x fsharp-ts-lens-refresh. The overlay prefix (default " // ") and face
are customizable via fsharp-ts-lens-prefix and fsharp-ts-lens-face.
FsAutoComplete can show intermediate types at each step of |> pipeline
chains, as well as parameter names and type annotations as inlay hints.
These use the standard LSP inlay hints protocol and are rendered by eglot's
built-in eglot-inlay-hints-mode:
;; Enable pipeline type hints (off by default)
(setq fsharp-ts-eglot-pipeline-hints t)
;; Enable inlay hints display
(add-hook 'fsharp-ts-mode-hook #'eglot-inlay-hints-mode)This shows types inline as you write pipeline chains:
[1; 2; 3] // int list
|> List.map string // string list
|> String.concat ", " // string
fsharp-ts-info.el provides a persistent documentation panel that shows
rich type information for the symbol at point -- signature, documentation
comment, constructors, interfaces, fields, functions, and attributes.
(require 'fsharp-ts-info)
;; Show documentation for symbol at point (opens side window)
M-x fsharp-ts-info-show
;; Auto-update the panel as you navigate code
M-x fsharp-ts-info-modeThe panel updates automatically after fsharp-ts-info-idle-delay seconds
(default 0.5) of idle time when fsharp-ts-info-mode is active and the
panel window is visible. Requires an active eglot connection.
When fsharp-ts-eglot is loaded, the echo area shows F#-specific type
signatures for the symbol at point (via fsharp/signature), providing richer
information than the standard LSP hover.
The mode-line shows F#[ProjectName] when the buffer belongs to a .fsproj
project. Disable with (setq fsharp-ts-show-project-name nil).
fsharp-ts-repl.el provides integration with dotnet fsi. The REPL buffer
gets tree-sitter syntax highlighting for input (via comint-fontify-input-mode)
and regex-based highlighting for output.
;; Enable the REPL minor mode in F# buffers
(add-hook 'fsharp-ts-mode-hook #'fsharp-ts-repl-minor-mode)From a source buffer with fsharp-ts-repl-minor-mode active:
| Key | Command | Description |
|---|---|---|
C-c C-z |
fsharp-ts-repl-switch-to-repl |
Start or switch to the REPL |
C-c C-c |
fsharp-ts-repl-send-definition |
Send definition at point |
C-c C-r |
fsharp-ts-repl-send-region |
Send region |
C-c C-b |
fsharp-ts-repl-send-buffer |
Send entire buffer |
C-c C-l |
fsharp-ts-repl-load-file |
Load file via #load directive |
C-c C-p |
fsharp-ts-repl-send-project-references |
Send project references to REPL |
C-c C-i |
fsharp-ts-repl-interrupt |
Interrupt the REPL process |
C-c C-k |
fsharp-ts-repl-clear-buffer |
Clear the REPL buffer |
The ;; expression terminator is appended automatically when missing. Input
history is persisted across sessions.
Project references: C-c C-p resolves assembly references and source files
from the nearest .fsproj and sends #r/#load directives to FSI, making
project types available in the REPL. Uses FsAutoComplete via eglot when
available (instant), falls back to dotnet msbuild (standalone).
M-x fsharp-ts-repl-generate-references-file writes the directives to a buffer
for inspection instead.
;; Customize the REPL command (default: "dotnet" with args "fsi" "--readline-")
(setq fsharp-ts-repl-program-name "/path/to/fsi")
(setq fsharp-ts-repl-program-args '("--readline-"))F# is indentation-sensitive, so shifting blocks of code is a common operation.
| Key | Command | Description |
|---|---|---|
C-c > |
fsharp-ts-mode-shift-region-right |
Indent region by one level |
C-c < |
fsharp-ts-mode-shift-region-left |
Dedent region by one level |
Both commands accept a prefix argument to shift by multiple levels (e.g.,
C-u 2 C-c > shifts right by 2 levels).
M-x fsharp-ts-mode-guess-indent-offset scans the buffer and sets
fsharp-ts-indent-offset to match the file's convention. Set
fsharp-ts-guess-indent-offset to t to run this automatically on file open.
| Key | Command | Description |
|---|---|---|
| Key | Command | Description |
| ----------- | ---------------------------------------- | ------------------------------------------ |
C-c C-d |
fsharp-ts-mode-doc-at-point |
Look up symbol at point in .NET API docs |
This opens the Microsoft .NET API reference with a search for the identifier at point. Works for any .NET type or function, not just FSharp.Core.
M-x fsharp-ts-mode-browse-fsharp-docs opens the F# documentation
home page.
M-x fsharp-ts-mode-search-by-signature searches the
FSDN database by type signature -- useful
for finding functions when you know the type you need (e.g., string -> int).
fsharp-ts-dotnet.el provides a minor mode for running dotnet commands from
F# buffers. All commands run in the project root (detected by walking up to the
nearest .sln, .fsproj, or Directory.Build.props).
;; Enable the dotnet minor mode in F# buffers
(add-hook 'fsharp-ts-mode-hook #'fsharp-ts-dotnet-mode)All keybindings use the C-c C-d prefix:
| Key | Command | Description |
|---|---|---|
C-c C-d b |
fsharp-ts-dotnet-build |
Build project |
C-c C-d t |
fsharp-ts-dotnet-test |
Run tests |
C-c C-d r |
fsharp-ts-dotnet-run |
Run project |
C-c C-d c |
fsharp-ts-dotnet-clean |
Clean build output |
C-c C-d R |
fsharp-ts-dotnet-restore |
Restore NuGet packages |
C-c C-d f |
fsharp-ts-dotnet-format |
Format code |
C-c C-d n |
fsharp-ts-dotnet-new |
New project from template |
C-c C-d d |
fsharp-ts-dotnet-command |
Run arbitrary command |
C-c C-d p |
fsharp-ts-dotnet-find-project-file |
Find nearest .fsproj |
C-c C-d s |
fsharp-ts-dotnet-find-solution-file |
Find nearest .sln |
Watch mode: Use C-u prefix with build, test, or run to switch to
dotnet watch (e.g., C-u C-c C-d b runs dotnet watch build). The watch
process stays alive in a comint buffer and rebuilds on file changes.
F# is an indentation-sensitive language -- the tree-sitter grammar needs correct whitespace to parse the code. This has a few practical consequences:
- Pasting unindented code: If you paste a block of F# with all indentation
stripped,
indent-regionwon't fix it because the parser can't make sense of the flat structure. Paste code with its indentation intact, or re-indent it manually. - Script files (.fsx): Shebang lines (
#!/usr/bin/env dotnet fsi) are handled automatically. Mixingletbindings with bare expressions works, though the grammar may occasionally produce unexpected results in complex scripts. - Incremental editing works well: When you're writing code line by line, the parser has enough context from preceding lines to indent correctly.
See doc/DESIGN.md for technical details on these limitations and the overall architecture.
Base mode (always active in F# buffers):
| Key | Command | Description |
|---|---|---|
C-c C-a |
ff-find-other-file |
Switch between .fs and .fsi |
C-c C-c |
compile |
Run compilation |
C-c C-d |
fsharp-ts-mode-doc-at-point |
Look up symbol in .NET docs |
C-c > |
fsharp-ts-mode-shift-region-right |
Indent region |
C-c < |
fsharp-ts-mode-shift-region-left |
Dedent region |
REPL minor mode (when fsharp-ts-repl-minor-mode is active):
| Key | Command | Description |
|---|---|---|
C-c C-z |
fsharp-ts-repl-switch-to-repl |
Start or switch to REPL |
C-c C-c |
fsharp-ts-repl-send-definition |
Send definition at point |
C-c C-r |
fsharp-ts-repl-send-region |
Send region |
C-c C-b |
fsharp-ts-repl-send-buffer |
Send buffer |
C-c C-l |
fsharp-ts-repl-load-file |
Load file (#load) |
C-c C-i |
fsharp-ts-repl-interrupt |
Interrupt REPL |
C-c C-k |
fsharp-ts-repl-clear-buffer |
Clear REPL buffer |
fsharp-mode is the long-standing
Emacs package for F# editing, maintained by the F# Software Foundation.
fsharp-ts-mode is a new, independent package built from scratch on top of
tree-sitter. The two can coexist -- only one will be active for a given buffer
based on auto-mode-alist ordering.
| fsharp-mode | fsharp-ts-mode | |
|---|---|---|
| Syntax highlighting | Regex-based (font-lock-keywords) |
Tree-sitter queries (structural, 4 levels) |
| Indentation | SMIE + custom heuristics | Tree-sitter indent rules |
| Min Emacs version | 25 | 29.1 (tree-sitter support) |
| REPL | Built-in (inf-fsharp-mode) |
Built-in (fsharp-ts-repl) with tree-sitter input highlighting |
| Eglot/LSP | Via separate eglot-fsharp |
Built-in (fsharp-ts-eglot) with auto-install + custom commands |
| Compilation | fsc/msbuild patterns |
dotnet build patterns |
| Imenu | Basic | Fully-qualified names (e.g., Module.func) |
| forward-sexp | Syntax-table | Tree-sitter + syntax-table hybrid |
| .fsi support | Same mode | Separate fsharp-ts-signature-mode |
- TRAMP / remote server support --
eglot-fsharpwraps the server command for remote access via TRAMP.fsharp-ts-eglotdoesn't handle this yet.
If you want fsharp-ts-mode to take priority, just make sure it's loaded after
fsharp-mode (or don't load fsharp-mode at all). fsharp-ts-mode registers
itself for .fs, .fsx, and .fsi files via auto-mode-alist, and the last
registration wins.
;; If you previously had:
(use-package fsharp-mode)
;; Replace with:
(use-package fsharp-ts-mode
:ensure t)This package was inspired by neocaml, my
tree-sitter-based OCaml mode. After spending time in the OCaml community I got
curious about its .NET cousin and wanted a modern Emacs editing experience for
F# as well. I strongly considered naming this package "Fa Dièse" (French for
F sharp -- because naming things after spending time with OCaml does that to
you), but ultimately chickened out and went with the boring-but-obvious
fsharp-ts-mode. Naming is hard!
Copyright (C) 2026 Bozhidar Batsov
Distributed under the GNU General Public License, version 3.