F# support for Neovim ONLY
An unofficial part of the Ionide plugin suite.
-
A (now disconnected) fork of Ionide-Vim, which is a fork of fsharp/vim-fsharp.
-
Uses FsAutoComplete as a backend.
-
Uses Neovim's built-in LSP client (requires Neovim 0.5+)
Consider this to be beta since it's lacking features compared to Ionide-VSCode and not as battle-tested as that.
That being said, I use this plugin daily so it will someday become feature-rich and stable for sure.
Feel free to request features and/or file bug reports!
-
Neovim - this one will only run on Neovim. I'm simply unable and unwilling to support regular vim.
-
- Required to install and run FsAutoComplete.
- Very useful for command-line development.
- Syntax highlighting
- Auto completions
- Error highlighting, error list, and quick fixes based on errors
- Tooltips
- Codelens
- Go to Definition
- Find all references
- Highlighting usages
- Rename
- Show symbols in file
- Find symbol in workspace
- Show signature in status line
- Integration with F# Interactive
- Integration with FSharpLint (additional hints and quick fixes)
- Integration with Fantomas (the best formatter available for F#)
If you're not relying on Mason, install FsAutoComplete with dotnet tool install
.
If you want to install it as a "global" tool, run dotnet tool install -g fsautocomplete
.
If you want to install it as a project-local tool, run dotnet tool install fsautocomplete
at the root directory of your F# project, and configure ionide.cmd
No LSP client plugin is required.
If you are using neovim/nvim-lspconfig, do not enable fsautocomplete
.
Ionide-Nvim automatically integrates itself into nvim-lspconfig and will register itself as a server.
We recommend using nvim-cmp.
-- this sample mostly from Lazyvim's setup. It's very nice, and uses lazy.nvim for plugin management.-
-- must have a snippet engine for nvim-cmp
-- we recommend luaSnip
{
"L3MON4D3/LuaSnip",
build = (not jit.os:find("Windows"))
and "echo 'NOTE: jsregexp is optional, so not a big deal if it fails to build'; make install_jsregexp"
or nil,
dependencies = {
"rafamadriz/friendly-snippets",
config = function()
require("luasnip.loaders.from_vscode").lazy_load()
end,
},
opts = {
history = true,
delete_check_events = "TextChanged",
},
-- stylua: ignore
keys = {
{
"<tab>",
function()
return require("luasnip").jumpable(1) and "<Plug>luasnip-jump-next" or "<tab>"
end,
expr = true, silent = true, mode = "i",
},
{ "<tab>", function() require("luasnip").jump(1) end, mode = "s" },
{ "<s-tab>", function() require("luasnip").jump(-1) end, mode = { "i", "s" } },
},
},
{
"hrsh7th/nvim-cmp",
version = false, -- last release is way too old
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
"hrsh7th/cmp-path",
"saadparwaiz1/cmp_luasnip",
},
opts = function()
vim.api.nvim_set_hl(0, "CmpGhostText", { link = "Comment", default = true })
local cmp = require("cmp")
local defaults = require("cmp.config.default")()
return {
completion = {
completeopt = "menu,menuone,noinsert",
},
snippet = {
expand = function(args)
require("luasnip").lsp_expand(args.body)
end,
},
mapping = cmp.mapping.preset.insert({
["<C-n>"] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }),
["<C-p>"] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }),
["<C-b>"] = cmp.mapping.scroll_docs(-4),
["<C-f>"] = cmp.mapping.scroll_docs(4),
["<C-Space>"] = cmp.mapping.complete(),
["<C-e>"] = cmp.mapping.abort(),
["<CR>"] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
["<S-CR>"] = cmp.mapping.confirm({
behavior = cmp.ConfirmBehavior.Replace,
select = true,
}), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
}),
sources = cmp.config.sources({
{ name = "nvim_lsp" },
{ name = "luasnip" },
{ name = "buffer" },
{ name = "path" },
}),
experimental = {
ghost_text = {
hl_group = "CmpGhostText",
},
},
sorting = defaults.sorting,
}
end,
}
-- lazy.nvim
{
"WillEhrendreich/Ionide-Nvim",
dependencies = {
{
-- highly recommended to use Mason. very nice for lsp/linter/tool installations.
"williamboman/mason.nvim",
opts = {
-- here we make sure fsautocomplete is downloaded by mason, which Ionide absolutely needs in order to work.
ensure_installed = {
"fsautocomplete",
},
},
{
-- very recommended to use nvim-lspconfig, as it takes care of much of the management of starting Ionide,
"neovim/nvim-lspconfig",
version = false, -- last release is way too old
opts = {
servers = {
---@type IonideOptions
ionide = {
IonideNvimSettings = {
},
cmd = {
vim.fs.normalize(vim.fn.stdpath("data").."/mason/bin/fsautocomplete.cmd"),
},
settings = {
FSharp = {
},
},
},
},
-- you can do any additional lsp server setup here
-- return true if you don't want this server to be setup with lspconfig
---@type table<string, fun(server:string, opts:_.lspconfig.options):boolean?>
setup = {
--- ***VERY IMPORTANT***
--- if you don't wan't both ionide AND fsautocomplete to
---attach themselves to every fsharp file (you don't, trust me), you
--- need to make sure that fsautocomplete doesn't get it's setup function called.
--- from within a lazy.nvim setup it simply means that you do the following:
fsautocomplete = function(_, _)
return true
end,
--- and then pass the opts in from up above.
ionide = function(_, opts)
require("ionide").setup(opts)
end,
},
},
},
},
},
},
}
Clone Ionide-Nvim to some runtimepath, I guess... but, honestly just don't. Too messy this way.
Opening either *.fs
, *.fsi
or *.fsx
files should trigger syntax highlighting and other depending runtime files as well.
Will's fork feature: opening a *.fsproj
file will trigger Ionide to load. Yeah, It's awesome.
To be added as requested for F#-specific features.
- "Resets the current FSI session."
- "Shows the merged config."
- "Shows the workspace folders that fsac has loaded."
- "load additional projects with this if you like"
- "Shows just the IonideNvimSettings portion of the config."
- "Show all currently loaded Project Info"
- "Shows just the project names that have been loaded."
- Loads specified projects (
sln
orfsproj
).
- Allows for the selection of specific [sln's](technically just using it as a new starting point to look for projects, sln files not supported in FSAC) to be loaded in addition to the one that it might have automatically found.
Ionide-Nvim has an integration with F# Interactive.
FSI is displayed using the builtin :terminal
feature in Neovim and can be used like in VSCode.
- "Send Current line's text to FSharp Interactive"
- "Send Current buffer's text to FSharp Interactive"
- "Toggle FSharp Interactive"
- "Quit FSharp Interactive"
Here are all the defaults that end up in a final merged config
ionide ={
--- Settings specific to neovim's built-in LSP client
IonideNvimSettings = {
AutocmdEvents = { "LspAttach", "BufEnter", "BufWritePost", "CursorHold", "CursorHoldI", "InsertEnter", "InsertLeave" },
AutomaticReloadWorkspace = true,
AutomaticWorkspaceInit = true,
FsautocompleteCommand = { "fsautocomplete" },
--- refer to fsac documentation for other possible commands.
FsiCommand = "dotnet fsi",
FsiFocusOnSend = false,
FsiKeymap = "vscode",
-- #### `Alt-Enter`
-- - When in normal mode, sends the current line to FSI.
-- - When in visual mode, sends the selection to FSI.
-- - Sending code to FSI opens FSI window but the cursor does not focus to it.
FsiKeymapSend = "<M-cr>",
-- #### `Alt-@`
-- - Toggles FSI window. FSI windows shown in different tabpages share the same FSI session.
-- - When opened, the cursor automatically focuses to the FSI window (unlike in `Alt-Enter` by default).
FsiKeymapToggle = "<M-@>",
FsiVscodeKeymaps = true,
--##### Customize how FSI window is opened
FsiWindowCommand = "botright 10new",
LspAutoSetup = false,
LspCodelens = true,
--- Enable/disable the default colorscheme for diagnostics
--- *Default:* enabled
--- Neovim's LSP client comes with no default colorscheme, so Ionide-Nvim sets a VSCode-like one for LSP diagnostics by default.
LspRecommendedColorScheme = true,
ShowSignatureOnCursorMove = true,
Statusline = "Ionide",
UseRecommendedServerConfig = false
},
---path to fsautocomplete. if it's installed globally, this should be fine.
--- the project local version would be {"dotnet", "fsautocomplete" }
--- and the mason version would be
--- Windows: vim.fs.normalize(vim.fn.stdpath("data").."/mason/bin/fsautocomplete.cmd"),
--- Non - Windows(I think..): vim.fs.normalize(vim.fn.stdpath("data").."/mason/bin/fsautocomplete"),
cmd = { "fsautocomplete" },
filetypes = { "fsharp" },
handlers = {
-- Ionide registers handlers for these lsp requests by default.
-- do not copy paste this portion, just showing that there is some handling of these here
-- ["fsharp/compilerLocation"] = function(error,result,context,config) end,
-- ["fsharp/documentationSymbol"] = function(error,result,context,config) end,
-- ["fsharp/notifyWorkspace"] = function(error,result,context,config) end,
-- ["fsharp/signature"] = function(error,result,context,config) end,
-- ["fsharp/workspaceLoad"] = function(error,result,context,config) end,
-- ["fsharp/workspacePeek"] = function(error,result,context,config) end,
-- ["textDocument/documentHighlight"] = function(error,result,context,config) end,
-- ["textDocument/hover"] = function(error,result,context,config) end,
},
init_options = {
AutomaticWorkspaceInit = true
},
log_level = 2,
message_level = 2,
name = "ionide",
--- this is what will run on every buffer that the client attaches itself to.
--- it's a function that takes in (LspClient , bufferNumber) and does whatever you want after that
--- it's a good place to set some keymaps for specific lsp things, though in my experience there's a lot
--- that you want set regardless, so it's only conditional stuff that you want to set here exactly.
-- on_attach = your on attach func here.
-- on_init = -- ionide's init function goes here. I can't imagine why you'd want to change this, but feel free to open pr or ask about it.
-- this is the function used to find a root directory to pass to the Lsp.
-- for root dir, it should be fine to leave it as is, but you can change it if you need to.
-- it is a function that gives a function that takes in a string which is the filename being opened.
-- for reference, this is how ionide has implemented this:
-- function M.GitFirstRootDir(n)
-- local root
-- root = util.find_git_ancestor(n)
-- root = root or util.root_pattern("*.sln")(n)
-- root = root or util.root_pattern("*.fsproj")(n)
-- root = root or util.root_pattern("*.fsx")(n)
-- return root
-- end
-- root_dir = M.GitFirstRootDir,
--- this is the settings table. it's what is passed to fsautocomplete on the UpdateSeverConfiguration function call,
--- most importantly, Initialize calls the UpdateSeverConfiguration function with this data,
--- though if you change anything in here at runtime,
--- you're going to have to call the IonideUpdateServerConfiguration user command to to do that.
--- in all likelihood, it's going to be easier to just make the setting change in your setup to ionide, and reload Neovim.
settings = {
FSharp =
{
-- `addFsiWatcher`,
addFsiWatcher = false,
-- `addPrivateAccessModifier`,
addPrivateAccessModifier = false,
-- `autoRevealInExplorer`,
autoRevealInExplorer = "sameAsFileExplorer",
-- `disableFailedProjectNotifications`,
disableFailedProjectNotifications = false,
-- `enableMSBuildProjectGraph`,
enableMSBuildProjectGraph = true,
-- `enableReferenceCodeLens`,
enableReferenceCodeLens = true,
-- `enableTouchBar`,
enableTouchBar = true,
-- `enableTreeView`,
enableTreeView = true,
-- `fsiSdkFilePath`,
fsiSdkFilePath = "",
-- `infoPanelReplaceHover`,
-- Not relevant to Neovim, currently
-- if there's a big demand I'll consider making one.
infoPanelReplaceHover = false,
-- `infoPanelShowOnStartup`,
infoPanelShowOnStartup = false,
-- `infoPanelStartLocked`,
infoPanelStartLocked = false,
-- `infoPanelUpdate`,
infoPanelUpdate = "onCursorMove",
-- `inlineValues`,
inlineValues = { enabled = true, prefix = " // " },
-- `msbuildAutoshow`,
-- Not relevant to Neovim, currently
msbuildAutoshow = false,
-- `notifications`,
notifications = { trace = true, traceNamespaces = { "BoundModel.TypeCheck", "BackgroundCompiler." } },
-- `openTelemetry`,
openTelemetry = { enabled = false },
-- `pipelineHints`,
pipelineHints = { enabled = true, prefix = " // " },
-- `saveOnSendLastSelection`,
saveOnSendLastSelection = false,
-- `showExplorerOnStartup`,
-- Not relevant to Neovim, currently
showExplorerOnStartup = false,
-- `showProjectExplorerIn`,
-- Not relevant to Neovim, currently
showProjectExplorerIn = "fsharp",
-- `simplifyNameAnalyzerExclusions`,
-- Not relevant to Neovim, currently
simplifyNameAnalyzerExclusions = { ".*\\.g\\.fs", ".*\\.cg\\.fs" },
-- `smartIndent`,
-- Not relevant to Neovim, currently
smartIndent = true,
-- `suggestGitignore`,
suggestGitignore = true,
-- `trace`,
trace = { server = "off" },
-- `unusedDeclarationsAnalyzerExclusions`,
unusedDeclarationsAnalyzerExclusions = { ".*\\.g\\.fs", ".*\\.cg\\.fs" },
-- `unusedOpensAnalyzerExclusions`,
unusedOpensAnalyzerExclusions = { ".*\\.g\\.fs", ".*\\.cg\\.fs" },
-- `verboseLogging`,
verboseLogging = false,
-- `workspacePath`
workspacePath = "",
-- `TestExplorer` = "",
-- Not relevant to Neovim, currently
TestExplorer = { AutoDiscoverTestsOnLoad = true },
-- { AutomaticWorkspaceInit: bool option AutomaticWorkspaceInit = false
-- WorkspaceModePeekDeepLevel: int option WorkspaceModePeekDeepLevel = 2
workspaceModePeekDeepLevel = 4,
fsac = {
attachDebugger = false,
cachedTypeCheckCount = 200,
conserveMemory = true,
silencedLogs = {},
parallelReferenceResolution = true,
-- "FSharp.fsac.sourceTextImplementation": {
-- "default": "NamedText",
-- "description": "EXPERIMENTAL. Enables the use of a new source text implementation. This may have better memory characteristics. Requires restart.",
-- "enum": [
-- "NamedText",
-- "RoslynSourceText"
-- ]
-- },
sourceTextImplementation = "RoslynSourceText",
dotnetArgs = {},
netCoreDllPath = "",
gc = {
conserveMemory = 0,
heapCount = 2,
noAffinitize = true,
server = true,
},
},
enableAdaptiveLspServer = true,
-- ExcludeProjectDirectories: string[] option = [||]
excludeProjectDirectories = { "paket-files", ".fable", "packages", "node_modules" },
-- KeywordsAutocomplete: bool option false
keywordsAutocomplete = true,
-- ExternalAutocomplete: bool option false
externalAutocomplete = false,
-- Linter: bool option false
linter = true,
-- IndentationSize: int option 4
indentationSize = 2,
-- UnionCaseStubGeneration: bool option false
unionCaseStubGeneration = true,
-- UnionCaseStubGenerationBody: string option """failwith "Not Implemented" """
unionCaseStubGenerationBody = 'failwith "Not Implemented"',
-- RecordStubGeneration: bool option false
recordStubGeneration = true,
-- RecordStubGenerationBody: string option "failwith \"Not Implemented\""
recordStubGenerationBody = 'failwith "Not Implemented"',
-- InterfaceStubGeneration: bool option false
interfaceStubGeneration = true,
-- InterfaceStubGenerationObjectIdentifier: string option "this"
interfaceStubGenerationObjectIdentifier = "this",
-- InterfaceStubGenerationMethodBody: string option "failwith \"Not Implemented\""
interfaceStubGenerationMethodBody = 'failwith "Not Implemented"',
-- UnusedOpensAnalyzer: bool option false
unusedOpensAnalyzer = true,
-- UnusedDeclarationsAnalyzer: bool option false
unusedDeclarationsAnalyzer = true,
-- SimplifyNameAnalyzer: bool option false
simplifyNameAnalyzer = true,
-- ResolveNamespaces: bool option false
resolveNamespaces = true,
-- EnableAnalyzers: bool option false
enableAnalyzers = true,
-- AnalyzersPath: string[] option
analyzersPath = { "packages/Analyzers", "analyzers" },
-- DisableInMemoryProjectReferences: bool option false|
-- disableInMemoryProjectReferences = false,
-- LineLens: LineLensConfig option
lineLens = { enabled = "always", prefix = "ll//" },
-- enables the use of .Net Core SDKs for script file type-checking and evaluation,
-- otherwise the .Net Framework reference lists will be used.
-- Recommended default value: `true`.
--
useSdkScripts = true,
suggestSdkScripts = true,
-- DotNetRoot - the path to the dotnet sdk. usually best left alone, the compiler searches for this on it's own,
dotnetRoot = "",
-- FSIExtraParameters: string[]
-- an array of additional runtime arguments that are passed to FSI.
-- These are used when typechecking scripts to ensure that typechecking has the same context as your FSI instances.
-- An example would be to set the following parameters to enable Preview features (like opening static classes) for typechecking.
-- defaults to {}
fsiExtraParameters = {},
-- FSICompilerToolLocations: string[]|nil
-- passes along this list of locations to compiler tools for FSI to the FSharpCompilerServiceChecker
-- to this function in fsautocomplete
-- https://github.com/fsharp/FsAutoComplete/blob/main/src/FsAutoComplete/LspServers/AdaptiveFSharpLspServer.fs#L99
-- which effectively just prepends "--compilertool:" to each entry and tells the FSharpCompilerServiceChecker about it and the fsiExtraParameters
fsiCompilerToolLocations = {},
-- TooltipMode: string option
-- TooltipMode can be one of the following:
-- "full" -> this provides the most verbose output
-- "summary" -> this is a slimmed down version of the tooltip
-- "" or nil -> this is the old or default way, and calls TipFormatter.FormatCommentStyle.Legacy on the lsp server... *shrug*
tooltipMode = "full",
-- GenerateBinlog
-- if true, binary logs will be generated and placed in the directory specified. They will have names of the form `{directory}/{project_name}.binlog`
-- defaults to false
generateBinlog = false,
abstractClassStubGeneration = true,
abstractClassStubGenerationObjectIdentifier = "this",
abstractClassStubGenerationMethodBody = 'failwith "Not Implemented"',
-- configures which parts of the CodeLens are enabled, if any
-- defaults to both signature and references being true
codeLenses = {
signature = { enabled = true },
references = { enabled = true },
},
inlayHints = {
--do these really annoy anyone? why not have em on?
enabled = true,
typeAnnotations = true,
-- Defaults to false, the more info the better, right?
disableLongTooltip = false,
parameterNames = true,
},
debug = {
dontCheckRelatedFiles = false,
checkFileDebouncerTimeout = 250,
logDurationBetweenCheckFiles = false,
logCheckFileDuration = false,
},
}
}
Default: not set
Ionide-Nvim does not provide default keybindings for various LSP features, so you will have to set them yourself.
- If you are using neovim's built-in LSP client, see here.
Default: enabled
By default, Ionide-Nvim creates an AutoCommand so that CodeLens gets refreshed automatically.
You can disable this by setting the below option:
ionide={
IonideNvimSettings={
---defaults to true
AutomaticCodeLensRefresh = false,
},
settings={
FSharp={
codeLenses={
references= {enabled = true},
signature= {enabled = true},
},
},
},
}
- Some of the settings may not work in Ionide-Nvim as it is lacking the corresponding feature of Ionide-VSCode.
Default: enabled
ionide={
IonideNvimSettings={
---defaults to true
AutomaticWorkspaceInit = false,
},
},
Default: `4'
ionide={
settings={
FSharp={
workspaceModePeekDeepLevel = 4,
},
},
}
Default: empty
ionide={
settings={
FSharp={
excludeProjectDirectories = { "paket-files", ".fable", "packages", "node_modules" },
},
},
}
Linting (other than the basic ones described above) and formatting is powered by independent tools, FSharpLint and Fantomas respectively.
Both uses their own JSON file for configuration and Ionide-Nvim does not control them. See their docs about configuration: FSharpLint and Fantomas.
- The primary maintainer for this repository is @WillEhrendreich.