Development environment

This page lists a few options to set up a productive development environment for working on Forgejo.

VSCodium

VSCodium is an open source version of the Visual Studio Code IDE. The Go integration for Visual Studio Code works with VSCodium and is a viable tool to work on Forgejo.

First, run cp -r contrib/ide/vscode .vscode to create new directory .vscode with the contents of folder contrib/ide/vscode at the root of the repository. Then, open the project directory in VSCodium.

You can now use Ctrl+Shift+B to build the gitea executable and F5 to run it in debug mode.

Tests can be run by clicking on the run test or debug test button above their declaration.

Go code is formatted automatically when saved.

Emacs

GNU Emacs is an open source implementation of the Emacs text editor. The Emacs Go-mode, in combination with the Go language-server and a LSP client can be used to work on Forgejo’s code base.

A prejequisite for working with the language server is installing gopls. After this, you’ll want to install the necessary packages in Emacs using M-x package-install followed by go-mode and your preferred LSP client, so either lsp-mode or eglot.

After the installation, you’ll need to activate the packages in your initialization file. Regardless of your choice of LSP client, you’ll need to add:

(require 'go-mode)

If you would like a full list of the features of go-mode, have a look at this blogpost by it’s creator.

Then, for lsp-mode, add:

(use-package lsp-mode
  :ensure
  :commands lsp
  :custom
  (add-hook 'lsp-mode-hook 'lsp-ui-mode) ;; Optional, see below
  (add-hook 'go-mode-hook #'lsp-deferred))

;; lsp-ui mode is optional. It shows inline hints.
(use-package lsp-ui
  :ensure
  :commands lsp-ui-mode
  :custom
  (lsp-ui-peek-always-show t)
  (lsp-ui-sideline-show-hover t)
  (lsp-ui-doc-enable nil))

;; Set up before-save hooks to format buffer and add/delete imports.
;; Make sure you don't have other gofmt/goimports hooks enabled.
(defun lsp-go-install-save-hooks ()
  (add-hook 'before-save-hook #'lsp-format-buffer t t)
  (add-hook 'before-save-hook #'lsp-organize-imports t t))
(add-hook 'go-mode-hook #'lsp-go-install-save-hooks)

Or for eglot:

(require 'project)

(defun project-find-go-module (dir)
  (when-let ((root (locate-dominating-file dir "go.mod")))
    (cons 'go-module root)))

(cl-defmethod project-root ((project (head go-module)))
  (cdr project))

(add-hook 'project-find-functions #'project-find-go-module)

(require 'eglot)
(add-hook 'go-mode-hook 'eglot-ensure)

;; Optional: install eglot-format-buffer as a save hook.
;; The depth of -10 places this before eglot's willSave notification,
;; so that that notification reports the actual contents that will be saved.
(defun eglot-format-buffer-before-save ()
  (add-hook 'before-save-hook #'eglot-format-buffer -10 t))
(add-hook 'go-mode-hook #'eglot-format-buffer-before-save)

As additional quality of life inprovements, you might consider installing company, flycheck and/or magit. Consider the package website for a complete explanation and installation instructions.

Here is an init example if you just want to use all three packages
;; company
(use-package company
  :ensure
  :custom
  (company-idle-delay 0) ;; how long to wait until popup
  (company-minimum-prefix-length 1)
  ;; (company-begin-commands nil) ;; uncomment to disable popup
  :bind
  (:map company-active-map
	("C-n". company-select-next)
	("C-p". company-select-previous)
	("M-<". company-select-first)
	("M->". company-select-last)
	)
  ;; Tab-override
  (:map company-mode-map
	("<tab>". tab-indent-or-complete)
	("TAB". tab-indent-or-complete)
	)
  )

(add-hook 'after-init-hook 'global-company-mode)


;; flycheck
(use-package flycheck :ensure)


;; magit
(use-package magit :ensure)

Vim

Vim has a Go plugin that can likely be used to work on Forgejo’s code base. Do you know how to configure it properly? Why not document that here?

Neovim

Here’s a minimal example that configures gopls and golangci_lint_ls using the Lazy.nvim plugin manager.

init.lua
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.uv.fs_stat(lazypath) then
	vim.fn.system({
		"git",
		"clone",
		"--filter=blob:none",
		"https://github.com/folke/lazy.nvim.git",
		"--branch=stable", -- latest stable release
		lazypath,
	})
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
	"neovim/nvim-lspconfig",
	{
		"nvim-telescope/telescope.nvim",
		branch = "0.1.x",
		dependencies = {
			"nvim-lua/plenary.nvim",
			{
				"nvim-telescope/telescope-fzf-native.nvim",
				build = "make",
				cond = vim.fn.executable("make") == 1,
			},
		},
	},
})

vim.g.mapleader = " "
vim.g.maplocalleader = " "

local on_attach = function(client, bufno)
	-- depricated since neovim 0.10
	-- vim.api.nvim_buf_set_option(bufno, "omnifunc", "v:lua.vim.lsp.omnifunc")
	vim.api.nvim_set_option_value("omnifunc", "v:lua.vim.lsp.omnifunc", { buf = bufno })

	local ts = require("telescope.builtin")
	local opts = { buffer = bufno }

	vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float)
	vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
	vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
	vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts)
	vim.keymap.set("n", "gd", vim.lsp.buf.definition, opts)
	vim.keymap.set("n", "gtd", vim.lsp.buf.type_definition, opts)
	vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
	vim.keymap.set("n", "gu", ts.lsp_references, opts)
	vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
	vim.keymap.set("n", "<leader>cl", vim.lsp.codelens.run, opts)
	vim.keymap.set("n", "<leader>r", vim.lsp.buf.rename, opts)
	vim.keymap.set("n", "<leader>f", function()
		vim.lsp.buf.format({ async = true })
	end, opts)
end

local capabilities = vim.lsp.protocol.make_client_capabilities()

require("lspconfig")["gopls"].setup({
	capabilities = capabilities,
	settings = {},
	on_attach = on_attach,
})

require("lspconfig")["golangci_lint_ls"].setup({
	capabilities = capabilities,
	settings = {},
	on_attach = on_attach,
})