summaryrefslogtreecommitdiff
path: root/lua/intellij_to_vscode/converter.lua
blob: 5d87cd8fdcf97e37cedff5f10078b1526f47aa59 (plain)
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
local M = {}
local uv = vim.loop
local api = vim.api

local function read_file(path)
	local fd = io.open(path, "r")
	if not fd then
		return nil
	end
	local content = fd:read("*a")
	fd:close()
	return content
end

local function write_file(path, content)
	local dir = path:match("(.*/)[^/]+$")
	if dir then
		vim.fn.mkdir(dir, "p")
	end
	local fd = io.open(path, "w")
	if not fd then
		error("Cannot write " .. path)
	end
	fd:write(content)
	fd:close()
end

-- Minimal XML attribute extractor for a tag like: <option name="MAIN_CLASS_NAME" value="com.example.Main" />
local function extract_attributes(tag)
	local attrs = {}
	for k, v in tag:gmatch('(%w+)%s*=\\s*"([^"]*)"') do
		attrs[k] = v
	end
	return attrs
end

-- Given the whole xml content, return table:
-- { type = "Application", name = "MyApp", options = { [name] = value, ... }, env = {k=v}, method = {...} }
local function parse_run_configuration(xml)
	-- find <configuration ...> ... </configuration>
	local cfg_tag, body = xml:match("<configuration%s+([^>]*)>(.-)</configuration>")
	if not cfg_tag then
		-- sometimes configuration is self-closing: <configuration ... />
		cfg_tag = xml:match("<configuration%s+([^/>]+)/>")
		body = ""
	end
	if not cfg_tag then
		return nil
	end

	local cfg_attrs = extract_attributes(cfg_tag)
	local result = { _attrs = cfg_attrs, options = {}, env = {}, method = {} }

	-- options: <option name="..." value="..."/>
	for option in body:gmatch("<option%s+([^>/]-)/>") do
		local a = extract_attributes(option)
		if a.name and a.value then
			result.options[a.name] = a.value
		end
	end

	-- envs: <envs><env name="K" value="V"/></envs>
	local envs_block = body:match("<envs>(.-)</envs>")
	if envs_block then
		for envtag in envs_block:gmatch("<env%s+([^>/]-)/>") do
			local a = extract_attributes(envtag)
			if a.name and a.value then
				result.env[a.name] = a.value
			end
		end
	end

	-- method (for JUnit): <method v="2"> <option name="..." value="..."/> </method>
	local method_block = body:match("<method%s+([^>]*)>(.-)</method>")
	if method_block then
		local method_tag, method_body = body:match("<method%s+([^>]*)>(.-)</method>")
		if method_tag then
			result.method._attrs = extract_attributes(method_tag)
			result.method.options = {}
			for opt in method_body:gmatch("<option%s+([^>/]-)/>") do
				local a = extract_attributes(opt)
				if a.name and a.value then
					result.method.options[a.name] = a.value
				end
			end
		end
	end

	return result
end

-- Map a single parsed config to a VSCode launch configuration (as Lua table)
local function map_to_vscode(name, parsed)
	local t = parsed._attrs.type or parsed._attrs.factoryName or ""
	local vscode = {
		name = name or parsed._attrs.name or "Converted",
		request = "launch",
	}

	-- Common Application type
	if t:match("[Aa]pplication") or parsed.options.MAIN_CLASS_NAME then
		vscode.type = "java"
		vscode.mainClass = parsed.options.MAIN_CLASS_NAME or parsed.options.MAIN_CLASS
		if parsed.options.PROGRAM_PARAMETERS and parsed.options.PROGRAM_PARAMETERS ~= "" then
			-- split by space respecting simple quotes (basic)
			local args = {}
			for a in parsed.options.PROGRAM_PARAMETERS:gmatch("%S+") do
				table.insert(args, a)
			end
			vscode.args = args
		end
		if parsed.options.VM_PARAMETERS and parsed.options.VM_PARAMETERS ~= "" then
			vscode.vmArgs = parsed.options.VM_PARAMETERS
		end
		if parsed.options.WORKING_DIRECTORY and parsed.options.WORKING_DIRECTORY ~= "" then
			vscode.cwd = parsed.options.WORKING_DIRECTORY:gsub("%$PROJECT_DIR%$", "${workspaceFolder}")
		else
			vscode.cwd = "${workspaceFolder}"
		end
		if next(parsed.env) then
			vscode.env = parsed.env
		end
		-- If module present, attach projectName (useful for java extension)
		if parsed.options.MODULE_NAME then
			vscode.projectName = parsed.options.MODULE_NAME
		end

		return vscode
	end

	-- JUnit test configuration
	if
		t:match("[Jj]Unit")
		or parsed.method.options and (parsed.method.options.CLASS_NAME or parsed.method.options.METHOD_NAME)
	then
		vscode.type = "java"
		-- Use test runner config via mainClass (JUnit runner) or use builtin test adapters
		if parsed.method.options.CLASS_NAME then
			vscode.name = (parsed._attrs.name or "JUnit:") .. " " .. parsed.method.options.CLASS_NAME
			-- map to a launch that runs the single test class
			vscode.mainClass = parsed.method.options.CLASS_NAME
			vscode.args = {}
		end
		if next(parsed.env) then
			vscode.env = parsed.env
		end
		vscode.cwd = parsed.options.WORKING_DIRECTORY
				and parsed.options.WORKING_DIRECTORY:gsub("%$PROJECT_DIR%$", "${workspaceFolder}")
			or "${workspaceFolder}"
		return vscode
	end

	-- Gradle run config
	if t:match("[Gg]radle") or parsed._attrs.type == "Gradle" or parsed.options.TASK_NAME then
		-- Represent as a 'command' launch using terminal.integrated (fallback)
		vscode.type = "pwa-node" -- generic terminal-ish; user may change
		vscode.name = parsed._attrs.name or "Gradle Task"
		local task = parsed.options.TASK_NAME or parsed.options.GRADLE_TASK
		vscode.request = "launch"
		vscode.runtimeExecutable = "gradle"
		vscode.args = {}
		if task and task ~= "" then
			for tkn in (task .. ""):gmatch("%S+") do
				table.insert(vscode.args, tkn)
			end
		end
		vscode.cwd = parsed.options.WORKING_DIRECTORY
				and parsed.options.WORKING_DIRECTORY:gsub("%$PROJECT_DIR%$", "${workspaceFolder}")
			or "${workspaceFolder}"
		if next(parsed.env) then
			vscode.env = parsed.env
		end
		return vscode
	end

	-- Fallback: produce a shell launch that echos an unsupported type
	return {
		type = "pwa-node",
		name = parsed._attrs.name or "Converted (unsupported)",
		request = "launch",
		program = "${file}",
		cwd = "${workspaceFolder}",
	}
end

-- Main: scan .idea/runConfigurations and convert
function M.convert_all(opts)
	opts = opts or {}
	local pattern = ".run/*.xml"
	local files = vim.tbl_filter(function(p)
		return p ~= ""
	end, vim.fn.glob(pattern, false, true))
	if #files == 0 then
		error(".run not found or no xml files present")
	end

	local configs = {}
	for _, f in ipairs(files) do
		local content = read_file(f)
		if not content then
			vim.notify("Cannot read " .. f, vim.log.levels.WARN)
		end
		local name = f:match("([^/]+)%.xml$") or f
		local parsed = parse_run_configuration(content)
		if parsed then
			local vs = map_to_vscode(name, parsed)
			table.insert(configs, vs)
		else
			vim.notify("Skipping (unrecognized) " .. f, vim.log.levels.DEBUG)
		end
	end

	local launch = { version = "0.2.0", configurations = configs }
	local ok, j = pcall(vim.fn.json_encode, launch)
	if not ok then
		error("Failed to encode launch.json")
	end
	write_file(".vscode/launch.json", j)
	return true
end

return M