Repository navigation
Expand file tree
/
Copy pathutil.lua
More file actions
246 lines (223 loc) · 7.39 KB
/
Copy pathutil.lua
File metadata and controls
246 lines (223 loc) · 7.39 KB
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
local M = {}
M.Windows = "Windows"
M.Linux = "Linux"
M.Mac = "Mac"
M.os_type = function()
local has = vim.fn.has
local t = M.Linux
if has("win32") == 1 or has("win64") == 1 then
t = M.Windows
elseif has("mac") == 1 then
t = M.Mac
end
return t
end
M.is_win = M.os_type() == M.Windows
M.is_linux = M.os_type() == M.Linux
M.is_mac = M.os_type() == M.Mac
M.java_bin = function()
local java_home = vim.env["JAVA_HOME"]
if java_home then
return vim.fn.expand(java_home .. "/bin/java")
end
return "java"
end
M.get_client = function(name)
local clients = vim.lsp.get_clients({ name = name })
if clients and #clients > 0 then
return clients[1]
end
-- vim.notify("Client not found: " .. name, vim.log.levels.ERROR)
return nil
end
M.get_spring_boot_client = function()
local clients = vim.lsp.get_clients({ name = "spring-boot" })
if clients and #clients > 0 then
return clients[1]
end
return nil
end
M._boot_command_co = {}
local function boot_client_execute_command(client, command, param, callback)
local err, resp = M.execute_command(client, command, param, callback)
if err then
print("Error executeCommand: " .. command .. "\n" .. vim.inspect(err))
end
return resp
end
M.boot_ls_init = function(client, _)
-- The settings the reference clients send right after initialization; they
-- also make the server index from source instead of trusting its cache.
-- See |spring_boot.settings|.
require("spring_boot.settings").push(client)
for _, co in ipairs(M._boot_command_co) do
coroutine.resume(co)
end
M._boot_command_co = {}
end
M.boot_execute_command = function(command, param, callback)
local client = M.get_spring_boot_client()
if client then
return boot_client_execute_command(client, command, param, callback)
end
vim.notify("Spring Boot LS is not ready, waiting for start", vim.log.levels.INFO)
local pco = coroutine.running()
local co = coroutine.create(function()
local resp = boot_client_execute_command(M.get_spring_boot_client(), command, param, callback)
coroutine.resume(pco, resp)
return resp
end)
table.insert(M._boot_command_co, co)
return coroutine.yield()
end
M.execute_command = function(client, command, param, callback)
local co
if not callback then
co = coroutine.running()
if co then
callback = function(err, resp)
coroutine.resume(co, err, resp)
end
end
end
-- client.request deprecated
client:request("workspace/executeCommand", {
command = command,
arguments = param,
}, callback, nil)
if co then
return coroutine.yield()
end
end
--- Whether `filename` is a Spring Boot yaml configuration file.
--- Matches on the basename so that e.g. a file living under a directory named
--- `application-something/` is not mistaken for configuration.
---@param filename string
---@return boolean
M.is_application_yml_file = function(filename)
local name = vim.fs.basename(filename)
return name:match("^application.*%.ya?ml$") ~= nil or name:match("^bootstrap.*%.ya?ml$") ~= nil
end
--- Whether `filename` is a Spring Boot properties configuration file.
---@param filename string
---@return boolean
M.is_application_properties_file = function(filename)
local name = vim.fs.basename(filename)
return name:match("^application.*%.properties$") ~= nil or name:match("^bootstrap.*%.properties$") ~= nil
end
--- The `languageId` the language server expects for a buffer. Spring Boot
--- configuration files get their own language ids so that the server treats
--- them as configuration rather than as plain yaml/properties.
---@param bufnr integer
---@param filetype string
---@return string
M.language_id = function(bufnr, filetype)
local filename = vim.api.nvim_buf_get_name(bufnr)
if filetype == "yaml" and M.is_application_yml_file(filename) then
return "spring-boot-properties-yaml"
end
if filetype == "jproperties" and M.is_application_properties_file(filename) then
return "spring-boot-properties"
end
return filetype
end
M.is_application_properties_buf = function(bufnr)
local rfilename = vim.api.nvim_buf_get_name(bufnr)
return M.is_application_properties_file(rfilename)
end
M.is_application_yml_buf = function(bufnr)
local rfilename = vim.api.nvim_buf_get_name(bufnr)
return M.is_application_yml_file(rfilename)
end
--- Build files that can declare a Spring Boot dependency.
local BUILD_FILES = { "pom.xml", "build.gradle", "build.gradle.kts" }
--- Build output directories, which never hold the project's own build files.
--- Dot directories are skipped by the caller.
local IGNORED_DIRS = {
["target"] = true,
["build"] = true,
["out"] = true,
["bin"] = true,
["node_modules"] = true,
}
--- How deep submodules are searched, and how many build files are read before
--- giving up. Reading every pom of a large monorepo on each new workspace
--- would be far too slow.
local MAX_DEPTH = 3
local MAX_BUILD_FILES = 50
--- Markers identifying a Spring Boot dependency in a build file. The Maven and
--- Gradle dependency form is `spring-boot-starter-*` /
--- `spring-boot-starter-parent`, while a Gradle build may only apply the
--- plugin, as `id 'org.springframework.boot'` — hence both spellings.
local SPRING_BOOT_MARKERS = { "spring-boot", "springframework.boot" }
---@param path string
---@return boolean
local function mentions_spring_boot(path)
local ok, lines = pcall(vim.fn.readfile, path)
if not ok then
return false
end
for _, line in ipairs(lines) do
for _, marker in ipairs(SPRING_BOOT_MARKERS) do
if line:find(marker, 1, true) then
return true
end
end
end
return false
end
--- Whether `root_dir` looks like a Spring Boot project, judged by its Maven or
--- Gradle build files.
---
--- Submodules are searched too: an aggregator `pom.xml` often only lists
--- `<modules>` while the dependency itself lives in the modules. The search is
--- breadth-first, so the root's own build files are always read first, and it
--- is bounded in both depth and number of files read.
---@param dir string
---@return string[]? # nil when the directory cannot be listed
local function subdirectories(dir)
local ok, entries = pcall(function()
local found = {}
for entry, kind in vim.fs.dir(dir) do
if kind == "directory" then
found[#found + 1] = entry
end
end
return found
end)
return ok and entries or nil
end
---@param root_dir string
---@return boolean
M.has_spring_boot_dependency = function(root_dir)
if vim.fn.isdirectory(root_dir) == 0 then
return false
end
local queue = { { dir = root_dir, depth = 0 } }
local index = 1
local read = 0
while index <= #queue and read < MAX_BUILD_FILES do
local current = queue[index]
index = index + 1
for _, name in ipairs(BUILD_FILES) do
local path = vim.fs.joinpath(current.dir, name)
if vim.fn.filereadable(path) == 1 then
read = read + 1
if mentions_spring_boot(path) then
return true
end
end
end
if current.depth < MAX_DEPTH then
-- A directory that cannot be listed is skipped rather than raised from:
-- this runs on every `root_dir` call, where raising costs the client.
for _, entry in ipairs(subdirectories(current.dir) or {}) do
if entry:sub(1, 1) ~= "." and not IGNORED_DIRS[entry] then
queue[#queue + 1] = { dir = vim.fs.joinpath(current.dir, entry), depth = current.depth + 1 }
end
end
end
end
return false
end
return M