Goose

Goose in other editors

The compiler is its own language server: goose lsp speaks the Language Server Protocol on stdin and stdout, so any editor with an LSP client can use it. VS Code has an extension that starts it for you. For Neovim, Helix and Emacs, the configuration below is all that is needed.

Every snippet starts goose lsp assuming the compiler is on PATH; if it is not, replace goose with the executable's full path. The server finds the standard library the way goose check does (GOOSE_STDLIB, or the stdlib/ directory near the executable). For a compiler that is not in its source checkout, add --stdlib and the directory to the command: goose lsp --stdlib /path/to/goose/stdlib.

Each open file is analyzed as a program of its own, with the files it imports. To have a module checked as part of the program that imports it (and the errors of that program shown in it), name the program's entry file in the client's initialization options, as an absolute path: { "entryFiles": ["/path/to/project/main.goose"] }. The snippets show where these options go.

The server's semantic tokens color names by what they resolve to (functions, types, fields, variants, parameters, variables). They do not cover keywords, strings or comments, and the TextMate grammar is only part of the VS Code extension.

Neovim

Neovim 0.11 or later, in init.lua:

vim.filetype.add({ extension = { goose = 'goose' } })
vim.lsp.config('goose', {
  cmd = { 'goose', 'lsp' },
  filetypes = { 'goose' },
  root_markers = { '.git' },
  -- init_options = { entryFiles = { '/path/to/project/main.goose' } },
})
vim.lsp.enable('goose')

On Neovim 0.10, start it from the filetype instead:

vim.filetype.add({ extension = { goose = 'goose' } })
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'goose',
  callback = function(ev)
    vim.lsp.start({ name = 'goose', cmd = { 'goose', 'lsp' }, root_dir = vim.fs.root(ev.buf, { '.git' }) })
  end,
})

By default, Neovim shows diagnostics, hover (K), go to definition (CTRL-]), references (grr), rename (grn), document symbols (gO), signature help (CTRL-S in Insert mode), completion (CTRL-X CTRL-O, or as you type with vim.lsp.completion.enable) and semantic highlighting. Inlay hints need vim.lsp.inlay_hint.enable(), folding needs foldmethod=expr and foldexpr=v:lua.vim.lsp.foldexpr(), and code lenses need vim.lsp.codelens.refresh() (called on BufEnter and InsertLeave, for example). The lenses above generic functions count their instantiations. To list the instantiations with vim.lsp.codelens.run(), give Neovim the command and tell the server that the client has it:

vim.lsp.commands['goose.showInstantiations'] = function(command)
  vim.ui.select(command.arguments[2], { prompt = 'Instantiations of ' .. command.arguments[1] }, function() end)
end
-- and in vim.lsp.config('goose', ...):
--   init_options = { commands = { 'goose.showInstantiations' } },

Helix

In languages.toml (in Helix's configuration directory, or .helix/ in the project):

[language-server.goose]
command = "goose"
args = ["lsp"]
# config = { entryFiles = ["/path/to/project/main.goose"] }

[[language]]
name = "goose"
scope = "source.goose"
file-types = ["goose"]
roots = [".git"]
comment-token = "//"
indent = { tab-width = 4, unit = "    " }
language-servers = ["goose"]

(config is what Helix sends as the initialization options.)

Helix shows diagnostics, hover (space k), go to definition (gd), references (gr), rename (space r), document and workspace symbols (space s, space S), signature help and completion as you type, and inlay hints with display-inlay-hints = true under [editor.lsp] in config.toml. Helix highlights with tree-sitter grammars, and Goose has none. Helix also has no semantic tokens, folding or code lenses, so Goose files are shown without colors.

Emacs

With Eglot (built in since Emacs 29), in init.el:

(define-derived-mode goose-mode prog-mode "Goose"
  (setq-local comment-start "// "))
(add-to-list 'auto-mode-alist '("\\.goose\\'" . goose-mode))
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs '(goose-mode "goose" "lsp")))
(add-hook 'goose-mode-hook #'eglot-ensure)

For entry files, the server program takes the options after the command: '(goose-mode "goose" "lsp" :initializationOptions (:entryFiles ["/path/to/project/main.goose"])).

Eglot shows diagnostics through Flymake, hover and signature help in ElDoc, go to definition (M-.), references (M-?), rename (eglot-rename), document symbols in Imenu, workspace symbols (xref-find-apropos), completion (completion-at-point) and inlay hints; the newest Eglot, from GNU ELPA, highlights with semantic tokens too (eglot-semantic-tokens-mode). Eglot has no folding or code lenses.

Formatting

goose format rewrites files in the standard layout: indentation, the spaces between tokens, blank lines and trailing commas, with the line breaks left where they are. As with gofmt, the types and =s of a run of declarations (consecutive consts, a struct's fields) line up in columns. It takes files and directories (a directory stands for the .goose files under it), and with none, or -, formats standard input to standard output. A file that does not parse is left as it is and its errors printed, with exit status 2; goose format --check writes nothing and lists the files that are not formatted, with exit status 1, for a CI step or a commit hook.

The language server formats too (textDocument/formatting), the editor's text rather than the file on disk, with edits to the lines that change only, so the cursor and marks on the others stay. A document that does not parse gets no edits; its diagnostics say why.

In Neovim, vim.lsp.buf.format() formats; to format on save, in the configuration above:

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*.goose',
  callback = function() vim.lsp.buf.format() end,
})

In Helix, :format formats, and auto-format = true in the [[language]] entry above formats on save.

In Emacs, eglot-format-buffer formats; to format on save:

(add-hook 'goose-mode-hook
          (lambda () (add-hook 'before-save-hook #'eglot-format-buffer nil t)))

Without the language server, an editor can run goose format - on the buffer instead: with conform.nvim in Neovim, say,

require('conform').setup({
  formatters = { goose = { command = 'goose', args = { 'format', '-' }, stdin = true } },
  formatters_by_ft = { goose = { 'goose' } },
  format_on_save = { timeout_ms = 2000 },
})

or formatter = { command = "goose", args = ["format", "-"] } in Helix's [[language]] entry.