Ghidra Extension Development in Neovim With Lsp and Treesitter


No more wrestling with jdtls with project-local configuration :))

All config here will be placed in a project-local .nvim.lua file. Feel free to place it in your vim.fn.stdpath("config") if you don’t like repetition.

Ghidra Extension template

This setup is tested to work with Ghidra Extension Development, it shouldn’t differ much for other workflows. For this purpose, we’ll use the sample extension in $GHIDRA_INSTALL_DIR/Extensions/Ghidra/Skeleton copied to a folder BetterSidebar.

Before doing anything, copy your buildTemplate.gradle to build.gradle.

treesitter

Install treesitter parser for java, and enable it in your config. That’s enough to get great syntax highlighting. This is simple enough to be added to your global nvim config.

LSP (jdtls)

This is the tricky part. jdtls requires specific java version and GHIDRA_INSTALL_DIR environment variable to work.

Copy over the config from https://raw.githubusercontent.com/neovim/nvim-lspconfig/292f44408498103c47996ff5c18fd366293840d8/lsp/jdtls.lua into your .nvim.lua

note

The built-in jdtls configuration in nvim-lspconfig already handles project detection and workspace management. We only need to customize how the server is launched, so we’ll copy that configuration instead of writing one from scratch.

Make the following changes:

  1. Instead of returning the config, save it as a local variable
DIFF
1
2
3
4
5
6
7
8
9
@@ -71,7 +72,7 @@ local root_markers2 = {
 }

 ---@type vim.lsp.Config
-return {
+local jdtls_conf = {
 	---@param dispatchers? vim.lsp.rpc.Dispatchers
 	---@param config vim.lsp.ClientConfig
 	cmd = function(dispatchers, config)
  1. Set your jdtls installation’s path in config_cmd
DIFF
1
2
3
4
5
6
7
8
9
+		local jdtls_bin = vim.fn.stdpath("data") .. "/jdtls/jdt-language-server-1.60.0-202606262232/bin/jdtls"
+
 		local config_cmd = {
-			'jdtls',
+			jdtls_bin,
 			'-data',
 			data_dir,
 			get_jdtls_jvm_args(),
 		}
  1. Set the JAVA_HOME and GHIDRA_INSTALL_DIR environment variables.
DIFF
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
-		return vim.lsp.rpc.start(config_cmd, dispatchers, {
+		local env = {
+			JAVA_HOME = "/home/<your_user>/.sdkman/candidates/java/21.0.2-tem/",
+			GHIDRA_INSTALL_DIR = "/home/<your_user>/repos/rev-pwn/ghidra_12.1.2_PUBLIC",
+		}
+
+		local spawn_params = {
 			cwd = config.cmd_cwd,
-			env = config.cmd_env,
+			env = env,
 			detached = config.detached,
-		})
+		}
  1. Configure and enable the lsp
DIFF
1
2
+vim.lsp.config("jdtls", jdtls_conf)
+vim.lsp.enable("jdtls")

Next time you open nvim, it will ask you to trust the .nvim.lua file. View it and trust it to enable nvim config.

Full confiugration

LUA
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
--- {{{
local function get_jdtls_cache_dir()
	return vim.fn.stdpath('cache') .. '/jdtls'
end

local function get_jdtls_workspace_dir()
	return get_jdtls_cache_dir() .. '/workspace'
end

local function get_jdtls_jvm_args()
	local env = os.getenv('JDTLS_JVM_ARGS')
	local args = {}
	for a in string.gmatch((env or ''), '%S+') do
		local arg = string.format('--jvm-arg=%s', a)
		table.insert(args, arg)
	end
	return unpack(args)
end

local root_markers1 = {
	-- Multi-module projects
	'mvnw',             -- Maven
	'gradlew',          -- Gradle
	'settings.gradle',  -- Gradle
	'settings.gradle.kts', -- Gradle
	-- Use git directory as last resort for multi-module maven projects
	-- In multi-module maven projects it is not really possible to determine what is the parent directory
	-- and what is submodule directory. And jdtls does not break if the parent directory is at higher level than
	-- actual parent pom.xml so propagating all the way to root git directory is fine
	'.git',
}
local root_markers2 = {
	-- Single-module projects
	'build.xml',     -- Ant
	'pom.xml',       -- Maven
	'build.gradle',  -- Gradle
	'build.gradle.kts', -- Gradle
}

---@type vim.lsp.Config
local jdtls_conf = {
	---@param dispatchers? vim.lsp.rpc.Dispatchers
	---@param config vim.lsp.ClientConfig
	cmd = function(dispatchers, config)
		local workspace_dir = get_jdtls_workspace_dir()
		local data_dir = workspace_dir

		if config.root_dir then
			data_dir = data_dir .. '/' .. vim.fn.fnamemodify(config.root_dir, ':p:h:t')
		end

		local jdtls_bin = vim.fn.stdpath("data") .. "/jdtls/jdt-language-server-1.60.0-202606262232/bin/jdtls"

		local config_cmd = {
			jdtls_bin,
			'-data',
			data_dir,
			get_jdtls_jvm_args(),
		}

		local env = {
			JAVA_HOME = "/home/<your_user>/.sdkman/candidates/java/21.0.2-tem/", -- DO NOT USE "~"
			GHIDRA_INSTALL_DIR = "/home/<your_user>/repos/rev-pwn/ghidra_12.1.2_PUBLIC", -- DO NOT USE "~"
		}

		local spawn_params = {
			cwd = config.cmd_cwd,
			env = env,
			detached = config.detached,
		}

		return vim.lsp.rpc.start(config_cmd, dispatchers, spawn_params)
	end,
	filetypes = { 'java' },
	root_markers = vim.fn.has('nvim-0.11.3') == 1 and { root_markers1, root_markers2 }
		or vim.list_extend(root_markers1, root_markers2),
	init_options = {},
}
--- }}}

vim.lsp.config("jdtls", jdtls_conf)
vim.lsp.enable("jdtls")