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
|