-
Notifications
You must be signed in to change notification settings - Fork 80
/
Copy pathapi.lua
536 lines (459 loc) · 12.5 KB
/
api.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
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
local versions = require("codeium.versions")
local config = require("codeium.config")
local io = require("codeium.io")
local log = require("codeium.log")
local update = require("codeium.update")
local notify = require("codeium.notify")
local util = require("codeium.util")
local enums = require("codeium.enums")
local api_key = nil
local status = {
api_key_error = nil,
}
local function find_port(manager_dir, start_time)
local files = io.readdir(manager_dir)
for _, file in ipairs(files) do
local number = tonumber(file.name, 10)
if file.type == "file" and number and io.stat_mtime(manager_dir .. "/" .. file.name) >= start_time then
return number
end
end
return nil
end
local cookie_generator = 1
local function next_cookie()
cookie_generator = cookie_generator + 1
return cookie_generator
end
local function get_request_metadata(request_id)
return {
api_key = api_key,
ide_name = "neovim",
ide_version = versions.nvim,
extension_name = "neovim",
extension_version = versions.extension,
request_id = request_id or next_cookie(),
}
end
local Server = {}
Server.__index = Server
function Server.check_status()
return status
end
function Server.load_api_key()
local json, err = io.read_json(config.options.config_path)
if err or type(json) ~= "table" then
if err == "ENOENT" then
-- Allow any UI plugins to load
local message = "please log in with :Codeium Auth"
status.api_key_error = message
vim.defer_fn(function()
notify.info(message)
end, 100)
else
local message = "failed to load the api key"
status.api_key_error = message
notify.info(message)
end
api_key = nil
return
end
status.api_key_error = nil
api_key = json.api_key
end
function Server.save_api_key()
local _, result = io.write_json(config.options.config_path, {
api_key = api_key,
})
status.api_key_error = nil
if result then
local message = "failed to save the api key"
status.api_key_error = message
notify.error(message, result)
end
end
function Server.authenticate()
local attempts = 0
local uuid = io.generate_uuid()
local url = "https://"
.. config.options.api.portal_url
.. "/profile?response_type=token&redirect_uri=vim-show-auth-token&state="
.. uuid
.. "&scope=openid%20profile%20email&redirect_parameters_type=query"
local prompt
local function on_submit(value)
if not value then
return
end
local endpoint = "https://api.codeium.com/register_user/"
if config.options.enterprise_mode then
endpoint = "https://" .. config.options.api.host .. ":" .. config.options.api.port
if config.options.api.path then
endpoint = endpoint .. "/" .. config.options.api.path:gsub("^/", "")
end
endpoint = endpoint .. "/exa.seat_management_pb.SeatManagementService/RegisterUser"
end
io.post(endpoint, {
headers = {
accept = "application/json",
},
body = {
firebase_id_token = value,
},
callback = function(body, err)
if err and not err.response then
notify.error("failed to validate token", err)
return
end
local ok, json = pcall(vim.fn.json_decode, body)
if not ok then
notify.error("failed to decode json", json)
return
end
if json and json.api_key and json.api_key ~= "" then
api_key = json.api_key
Server.save_api_key()
notify.info("api key saved")
return
end
attempts = attempts + 1
if attempts == 3 then
notify.error("too many failed attempts")
return
end
notify.error("api key is incorrect")
prompt()
end,
})
end
prompt = function()
require("codeium.views.auth-menu")(url, on_submit)
end
prompt()
end
function Server:new()
local m = {}
setmetatable(m, self)
local o = {}
setmetatable(o, m)
local port = nil
local job = nil
local current_cookie = nil
local workspaces = {}
local healthy = false
local last_heartbeat = nil
local last_heartbeat_error = nil
local function request(fn, payload, callback)
local url = "http://127.0.0.1:" .. port .. "/exa.language_server_pb.LanguageServerService/" .. fn
io.post(url, {
body = payload,
callback = callback,
})
end
local function do_heartbeat()
request("Heartbeat", {
metadata = get_request_metadata(),
}, function(_, err)
last_heartbeat = os.time()
last_heartbeat_error = nil
if err then
notify.warn("heartbeat failed", err)
last_heartbeat_error = err
else
healthy = true
end
end)
end
function m.is_healthy()
return healthy
end
function m.checkhealth(logger)
logger.info("Checking server status")
if m.is_healthy() then
logger.ok("Server is healthy on port: " .. port)
else
logger.warn("Server is unhealthy")
end
logger.info("Language Server binary: " .. update.get_bin_info().bin)
if last_heartbeat == nil then
logger.warn("No heartbeat executed")
else
logger.info("Last heartbeat: " .. os.date("%D %H:%M:%S", last_heartbeat))
if last_heartbeat_error ~= nil then
logger.error(last_heartbeat_error)
else
logger.ok("Heartbeat ok")
end
end
end
function m.start()
m.shutdown()
current_cookie = next_cookie()
if not api_key then
io.timer(1000, 0, m.start)
return
end
local manager_dir = config.manager_path
if not manager_dir then
manager_dir = io.tempdir("codeium/manager")
vim.fn.mkdir(manager_dir, "p")
end
local start_time = io.touch(manager_dir .. "/start")
local function on_exit(_, err)
if not current_cookie then
return
end
healthy = false
if err then
job = nil
current_cookie = nil
notify.error("codeium server crashed", err)
io.timer(1000, 0, function()
log.debug("restarting server after crash")
m.start()
end)
end
end
local function on_output(_, v, j)
log.debug(j.pid .. ": " .. v)
end
local api_server_url = "https://"
.. config.options.api.host
.. ":"
.. config.options.api.port
.. (config.options.api.path and "/" .. config.options.api.path:gsub("^/", "") or "")
local job_args = {
update.get_bin_info().bin,
"--api_server_url",
api_server_url,
"--manager_dir",
manager_dir,
"--file_watch_max_dir_count",
config.options.file_watch_max_dir_count,
enable_handlers = true,
enable_recording = false,
on_exit = on_exit,
on_stdout = on_output,
on_stderr = on_output,
}
if config.options.enable_chat then
table.insert(job_args, "--enable_chat_web_server")
table.insert(job_args, "--enable_chat_client")
end
if config.options.enable_local_search then
table.insert(job_args, "--enable_local_search")
end
if config.options.enable_index_service then
table.insert(job_args, "--enable_index_service")
table.insert(job_args, "--search_max_workspace_file_count")
table.insert(job_args, config.options.search_max_workspace_file_count)
end
if config.options.api.portal_url then
table.insert(job_args, "--portal_url")
table.insert(job_args, "https://" .. config.options.api.portal_url)
end
if config.options.enterprise_mode then
table.insert(job_args, "--enterprise_mode")
end
if config.options.detect_proxy ~= nil then
table.insert(job_args, "--detect_proxy=" .. tostring(config.options.detect_proxy))
end
local job = io.job(job_args)
job:start()
local function start_heartbeat()
io.timer(100, 5000, function(cancel_heartbeat)
if not current_cookie then
cancel_heartbeat()
else
do_heartbeat()
end
end)
end
io.timer(100, 500, function(cancel)
if not current_cookie then
cancel()
return
end
port = find_port(manager_dir, start_time)
if port then
cancel()
start_heartbeat()
end
end)
end
local function noop(...) end
local pending_request = { 0, noop }
function m.request_completion(document, editor_options, other_documents, callback)
pending_request[2](true)
local metadata = get_request_metadata()
local this_pending_request
local complete
complete = function(...)
complete = noop
this_pending_request(false)
callback(...)
end
this_pending_request = function(is_complete)
if pending_request[1] == metadata.request_id then
pending_request = { 0, noop }
end
this_pending_request = noop
request("CancelRequest", {
metadata = get_request_metadata(),
request_id = metadata.request_id,
}, function(_, err)
if err then
log.warn("failed to cancel in-flight request", err)
end
end)
if is_complete then
complete(false, nil)
end
end
pending_request = { metadata.request_id, this_pending_request }
request("GetCompletions", {
metadata = metadata,
editor_options = editor_options,
document = document,
other_documents = other_documents,
}, function(body, err)
if err then
if err.status == 503 or err.status == 504 or err.status == 408 then
-- Service Unavailable or Timeout error
return complete(false, nil)
end
local ok, json = pcall(vim.fn.json_decode, err.response.body)
if ok and json then
if json.state and json.state.state == "CODEIUM_STATE_INACTIVE" then
if json.state.message then
log.debug("completion request failed", json.state.message)
end
return complete(false, nil)
end
if json.code == "canceled" then
log.debug("completion request cancelled at the server", json.message)
return complete(false, nil)
end
end
notify.error("completion request failed", err)
complete(false, nil)
return
end
local ok, json = pcall(vim.fn.json_decode, body)
if not ok then
notify.error("completion request failed", "invalid JSON:", json)
return
end
log.trace("completion: ", json)
complete(true, json)
end)
return function()
this_pending_request(true)
end
end
function m.accept_completion(completion_id)
request("AcceptCompletion", {
metadata = get_request_metadata(),
completion_id = completion_id,
}, noop)
end
function m.refresh_context()
-- bufnr for current buffer is 0
local bufnr = 0
local line_ending = util.get_newline(bufnr)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true)
-- Ensure that there is always a newline at the end of the file
table.insert(lines, "")
local text = table.concat(lines, line_ending)
local filetype = vim.bo.filetype
local language = enums.languages[filetype] or enums.languages.unspecified
local doc = {
editor_language = filetype,
language = language,
cursor_offset = 0,
text = text,
line_ending = line_ending,
absolute_uri = util.get_uri(vim.api.nvim_buf_get_name(bufnr)),
workspace_uri = util.get_uri(util.get_project_root()),
}
request("RefreshContextForIdeAction", {
active_document = doc,
}, function(_, err)
if err then
notify.error("failed refresh context: " .. err.out)
return
end
end)
end
function m.add_workspace()
local project_root = util.get_project_root()
-- workspace already tracked by server
if workspaces[project_root] then
return
end
-- unable to track hidden path
for entry in project_root:gmatch("[^/]+") do
if entry:sub(1, 1) == "." then
return
end
end
request("AddTrackedWorkspace", { workspace = project_root }, function(_, err)
if err then
notify.error("failed to add workspace: " .. err.out)
return
end
workspaces[project_root] = true
end)
end
function m.get_chat_ports()
request("GetProcesses", {
metadata = get_request_metadata(),
}, function(body, err)
if err then
notify.error("failed to get chat ports", err)
return
end
local ports = vim.fn.json_decode(body)
local url = "http://127.0.0.1:"
.. ports.chatClientPort
.. "?api_key="
.. api_key
.. "&has_enterprise_extension="
.. (config.options.enterprise_mode and "true" or "false")
.. "&web_server_url=ws://127.0.0.1:"
.. ports.chatWebServerPort
.. "&ide_name=neovim"
.. "&ide_version="
.. versions.nvim
.. "&app_name=codeium.nvim"
.. "&extension_name=codeium.nvim"
.. "&extension_version="
.. versions.extension
.. "&ide_telemetry_enabled=true"
.. "&has_index_service="
.. (config.options.enable_index_service and "true" or "false")
.. "&locale=en_US"
-- cross-platform solution to open the web app
local os_info = io.get_system_info()
if os_info.os == "linux" then
os.execute("xdg-open '" .. url .. "'")
elseif os_info.os == "macos" then
os.execute("open '" .. url .. "'")
elseif os_info.os == "windows" then
os.execute(string.format('start "" "%s"', url))
else
notify.error("Unsupported operating system")
end
end)
end
function m.shutdown()
current_cookie = nil
if job then
job.on_exit = nil
job:shutdown()
end
end
m.__index = m
return o
end
return Server