Skip to content

Latest commit

 

History

History
667 lines (566 loc) · 23.5 KB

README.mkd

File metadata and controls

667 lines (566 loc) · 23.5 KB

Ionide-Nvim

F# support for Neovim ONLY

An unofficial part of the Ionide plugin suite.

About Ionide-Nvim

Development Status

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!

Requirements

  • Neovim - this one will only run on Neovim. I'm simply unable and unwilling to support regular vim.

  • .NET Core SDK

    • Required to install and run FsAutoComplete.
    • Very useful for command-line development.

Features

  • 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#)

Getting Started

Install FsAutoComplete

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

Install a LSP Client

For Neovim 0.8+

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.

Install an autocompletion plugin

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,
}

Install Ionide-Nvim

-- 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,
            },
          },
        },
      },
    },
  },
}

Installing manually

Clone Ionide-Nvim to some runtimepath, I guess... but, honestly just don't. Too messy this way.

Usage

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.

Commands

To be added as requested for F#-specific features.

  • "Resets the current FSI session."

:IonideShowConfigs

  • "Shows the merged config."

:IonideShowWorkspaceFolders

  • "Shows the workspace folders that fsac has loaded."

:IonideLoadProjects

  • "load additional projects with this if you like"

:IonideShowNvimSettings

  • "Shows just the IonideNvimSettings portion of the config."

:IonideShowAllLoadedProjectInfo

  • "Show all currently loaded Project Info"

:IonideShowLoadedProjects

  • "Shows just the project names that have been loaded."

:IonideLoadProjects <files>+

  • Loads specified projects (sln or fsproj).

:IonideWorkspacePeek

  • 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.

Working with F# Interactive

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.

:IonideSendCurrentLineToFSI

  • "Send Current line's text to FSharp Interactive"

:IonideSendWholeBufferToFSI

  • "Send Current buffer's text to FSharp Interactive"

:IonideToggleFSI

  • "Toggle FSharp Interactive"

:IonideQuitFSI

  • "Quit FSharp Interactive"

:IonideResetFSI

Settings

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,
  },
}
}
Set the keybindings for LSP features

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.
Enable/disable automatic refreshing CodeLens

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},
      },
    },
  },
}

FsAutoComplete Settings

  • Some of the settings may not work in Ionide-Nvim as it is lacking the corresponding feature of Ionide-VSCode.
Enable/disable automatic loading of the workspace on opening F# files

Default: enabled

ionide={
  IonideNvimSettings={
    ---defaults to true 
    AutomaticWorkspaceInit = false,
  },
},
Set the deep level of directory hierarchy when searching for sln/fsprojs

Default: `4'

ionide={
  settings={
    FSharp={
      workspaceModePeekDeepLevel = 4,
    },
  },
}
Ignore specific directories when loading a workspace

Default: empty

ionide={
  settings={
    FSharp={
      excludeProjectDirectories = { "paket-files", ".fable", "packages", "node_modules" },
    },
  },
}

Linter & Formatter Settings

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.

Maintainers