-
Notifications
You must be signed in to change notification settings - Fork 6
/
cwdhistory.lua
570 lines (500 loc) · 18.7 KB
/
cwdhistory.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
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
--------------------------------------------------------------------------------
-- CWD History
--
-- Maintains a list of recent current working directories. Also provides some
-- directory completion commands.
--
-- The "cwdhistory.limit" setting specifies how many recently used current
-- working directories will be remembered. The default limit is 100.
--
-- The CLINK_HISTORY_LABEL environment variable affects directory history the
-- same way it affects command history.
--
-- History:
--
-- Shift-PgUp is the default key binding to show a popup list of recent
-- directories, unless it has already been bound to something else.
--
-- Completion:
--
-- Ctrl-\ is the default key binding to cycle through directory matches for
-- the word at the cursor. Ctrl-Shift-\ cycles in reverse order.
--
-- Alt-Ctrl-\ is the default key binding to perform interactive completion
-- for the word at the cursor, showing possible directory completions.
--
-- Customize key bindings:
--
-- To bind different keys, add a key bindings for the appropriate commands
-- to your .inputrc file. For information on customizing key bindings see
-- https://chrisant996.github.io/clink/clink.html#customizing-key-bindings
--
-- "luafunc:cwdhistory_popup"
-- Show popup list of recent current working directories. Press
-- Enter to `cd` to the selected directory, or press Shift-Enter
-- or Ctrl-Enter to `pushd` to the selected directory.
--
-- "luafunc:cwdhistory_menucomplete"
-- "luafunc:cwdhistory_menucomplete_backward"
-- Cycle through directory matches for the word at the cursor.
-- These behave like the "old-menu-complete" and
-- "old-menu-complete-backward" commands.
--
-- "luafunc:cwdhistory_complete"
-- Perform completion for directory matches for the word at the
-- cursor. Behaves like the "complete" command.
--
-- "luafunc:cwdhistory_selectcomplete"
-- Perform completion by selecting from an interactive list of
-- directory matches. Behaves like the "clink-select-complete"
-- command.
--------------------------------------------------------------------------------
if not io.sopen then
print("cwdhistory.lua requires a newer version of Clink; please upgrade.")
return
end
--------------------------------------------------------------------------------
settings.add("cwdhistory.limit", 100, "Limit the cwd history",
"At most this many recently used current working directories will be remembered.")
settings.add("cwdhistory.restore", false, "Restore the most recent cwd on startup",
"When this is 'true', when Clink is injected it automatically changes to the\n"..
"most recent cwd in the history.")
--------------------------------------------------------------------------------
local cwd_history_list = {}
local deletion_list
local using_history_file
--------------------------------------------------------------------------------
local function reset_cache()
cwd_history_list = {}
deletion_list = nil
using_history_file = nil
end
--------------------------------------------------------------------------------
local function add_and_update_index(list, entry, index, force)
local key = clink.lower(entry.dir)
local present = index[key]
if force or present then
table.insert(list, entry)
end
if present then
index[key] = nil
end
end
--------------------------------------------------------------------------------
local function need_quote(word)
return word and word:find("[ &()[%]{}^=;!%'+,`~]") and true
end
--------------------------------------------------------------------------------
local function maybe_quote(word)
if need_quote(word) then
word = '"' .. word .. '"'
end
return word
end
--------------------------------------------------------------------------------
local function get_history_filename()
local profile_dir = os.getenv("=clink.profile")
if profile_dir and profile_dir ~= "" then
local filename = path.join(profile_dir, "cwd_history")
local label = os.getenv("CLINK_HISTORY_LABEL") or ""
label = label:gsub("%p", "")
if #label > 0 then
label = "-" .. label
end
return filename .. label
end
end
--------------------------------------------------------------------------------
local function read_history(file)
local list = {}
if file then
for line in file:lines() do
local dir, time = line:match("^([^|]*)%|(.*)$")
if dir and #dir > 0 then
table.insert(list, { dir=dir, time=time })
elseif #line > 0 then
table.insert(list, { dir=line })
end
end
end
return list
end
--------------------------------------------------------------------------------
local function merge_nodups(file, nocwd)
local persisted_list = read_history(file)
local limit = settings.get("cwdhistory.limit")
-- Build index of dirs, for removing duplicates.
local index = {}
for _, entry in ipairs(persisted_list) do
index[clink.lower(entry.dir)] = true
end
-- Build list of dirs added in session history.
local new_dirs = {}
for _, entry in ipairs(cwd_history_list) do
if entry.keep and not index[clink.lower(entry.dir)] then
table.insert(new_dirs, entry)
end
end
-- Build reversed list with duplicates removed.
local reversed = {}
if not nocwd then
local cwd_entry = { dir=os.getcwd(), time=os.time(), keep=true }
add_and_update_index(reversed, cwd_entry, index, true--[[force]])
end
for i = #new_dirs, 1, -1 do
table.insert(reversed, new_dirs[i])
end
for i = #persisted_list, 1, -1 do
add_and_update_index(reversed, persisted_list[i], index)
end
-- Apply deletions.
if deletion_list then
for i = #reversed, 1, -1 do
if deletion_list[clink.lower(reversed[i].dir)] then
table.remove(reversed, i)
end
end
deletion_list = nil
end
-- Prune to the limit, if any.
if limit and limit > 0 then
for i = #reversed, limit + 1, -1 do
table.remove(reversed, i)
end
end
-- Reverse the list again.
local output = {}
for i = #reversed, 1, -1 do
table.insert(output, reversed[i])
end
return output, persisted_list
end
--------------------------------------------------------------------------------
local function update_history_internal(history_filename, nocwd)
local f
local binmode = io.truncate and "" or "b"
-- Create the history file if it doesn't exist yet.
if not os.isfile(history_filename) then
f = io.sopen(history_filename, "wx"..binmode, "rw")
if not f then
return
end
f:close()
end
-- Retry opening the history file until there is no sharing violation.
-- Try for up to 2 seconds, and then give up.
local start_clock = os.clock()
repeat
f = io.sopen(history_filename, "r+"..binmode, "rw")
until f or os.clock() - start_clock > 2
if not f then
log.info("cwdhistory unable to access '"..history_filename.."'.")
return
end
-- Get the file size. Necessary until Clink has a way to truncate a file.
local file_size = f:seek()
-- Merge the in-memory list with the persisted list.
local loaded_list
cwd_history_list, loaded_list = merge_nodups(f, nocwd)
-- Be nice to SSD lifetime, and to performance! Do not rewrite the file
-- unless it has changed. But do not rewrite the file if the only change is
-- to the timestamp of the last entry.
local rewrite = #cwd_history_list ~= #loaded_list
if not rewrite then
for i = 1, #loaded_list, 1 do
if cwd_history_list[i].dir ~= loaded_list[i].dir then
rewrite = true
break
elseif cwd_history_list[i].time ~= loaded_list[i].time then
if i < #loaded_list then
rewrite = true
break
end
end
end
end
if rewrite and f:seek("set") ~= nil then
for _, entry in ipairs(cwd_history_list) do
if entry.time then
f:write(entry.dir.."|"..tostring(entry.time).."\n")
else
f:write(entry.dir.."\n")
end
end
if io.truncate then
io.truncate(f)
else
local truncate = f:seek()
local excess = file_size - truncate
local fill = string.rep("\n", 512)
while excess >= #fill do
f:write(fill)
excess = excess - #fill
end
if excess > 0 then
f:write(fill:sub(1, excess))
end
end
end
f:close()
end
--------------------------------------------------------------------------------
local function update_history(nocwd)
local file = get_history_filename()
if using_history_file then
update_history_internal(using_history_file, nocwd)
end
if using_history_file ~= file then
reset_cache()
using_history_file = file
update_history_internal(using_history_file, nocwd)
end
end
--------------------------------------------------------------------------------
function cwdhistory_popup(rl_buffer) -- luacheck: no global
local items = {}
local time_format = "%Y-%m-%d %H:%M:%S"
for _, entry in ipairs(cwd_history_list) do
local time_str
if entry.time then
time_str = os.date(time_format, tonumber(entry.time))
end
table.insert(items, { value=entry.dir.." ", description=time_str.."\t" })
end
-- If the most recent is the same as the current directory, then remove it;
-- there's little point in switching to the same directory.
if #items > 0 then
local top = items[#items].value:gsub(" +$", "")
if string.equalsi(os.getcwd(), top) then
table.remove(items, #items)
end
end
local update
local function del_callback(index)
if cwd_history_list[index] then
deletion_list = deletion_list or {}
deletion_list[clink.lower(cwd_history_list[index].dir)] = true
table.remove(cwd_history_list, index)
update = true
return true
end
end
items.reverse = true
local value, shifted, index = clink.popuplist("Recently Used Directories", items, #items, del_callback) -- luacheck: no unused, no max line length
if update then
update_history()
end
if not value then
rl_buffer:ding()
return
end
value = value:gsub(" *$", "").."\\"
rl_buffer:beginundogroup()
rl_buffer:remove(1, -1)
if shifted then
rl_buffer:insert("pushd ")
end
rl_buffer:insert(maybe_quote(value))
rl_buffer:endundogroup()
rl.invokecommand("accept-line")
end
--------------------------------------------------------------------------------
local use_dir_generator
local dir_generator = clink.generator(-999999)
--------------------------------------------------------------------------------
local function get_cursor_word(line_state)
local info = line_state:getwordinfo(line_state:getwordcount())
if not info then
return ""
end
local start = info.offset
local cursor = line_state:getcursor()
if cursor < start then
return ""
end
return line_state:getline():sub(start, cursor - 1)
end
--------------------------------------------------------------------------------
function dir_generator:generate(line_state, builder) -- luacheck: no unused
if not use_dir_generator then
return
end
local word = get_cursor_word(line_state)
local drive = path.getdrive(word)
if line_state:getwordcount() <= 1 then
local restrict_drive
if not drive then
local dir = path.getdirectory(word)
if dir and dir:sub(1, 1) == "\\" then
local fullword = os.getfullpathname(word)
if fullword and fullword ~= "" then
word = fullword
drive = path.getdrive(fullword)
restrict_drive = true
end
end
end
if word == "" or drive then
local color = settings.get("color.doskey")
if not color or color == "" then
color = rl.getmatchcolor("", "dir") or ""
else
color = "\x1b[0;"..color.."m"
end
for i = #cwd_history_list, 1, -1 do
local dir = path.join(cwd_history_list[i].dir, "")
if restrict_drive then
if path.getdrive(dir) ~= drive then
dir = nil
else
dir = dir:sub(#drive + 1)
end
end
if dir then
builder:addmatch({ match=dir, display=color..dir, type="word", suppressappend=true })
end
end
builder:setnosort()
end
end
for _, m in ipairs(clink.dirmatches(line_state:getendword())) do
builder:addmatch(m)
end
return true
end
--------------------------------------------------------------------------------
-- luacheck: globals cwdhistory_menucomplete
function cwdhistory_menucomplete(rl_buffer) -- luacheck: no unused
use_dir_generator = true
rl.invokecommand("old-menu-complete")
use_dir_generator = nil
end
--------------------------------------------------------------------------------
-- luacheck: globals cwdhistory_menucomplete_backward
function cwdhistory_menucomplete_backward(rl_buffer) -- luacheck: no unused
use_dir_generator = true
rl.invokecommand("old-menu-complete-backward")
use_dir_generator = nil
end
--------------------------------------------------------------------------------
-- luacheck: globals cwdhistory_complete
function cwdhistory_complete(rl_buffer) -- luacheck: no unused
use_dir_generator = true
rl.invokecommand("complete")
use_dir_generator = nil
end
--------------------------------------------------------------------------------
-- luacheck: globals cwdhistory_selectcomplete
function cwdhistory_selectcomplete(rl_buffer) -- luacheck: no unused
use_dir_generator = true
rl.invokecommand("clink-select-complete")
use_dir_generator = nil
end
--------------------------------------------------------------------------------
function cwdhistory_remove_dir(dir) -- luacheck: no global
deletion_list = deletion_list or {}
deletion_list[clink.lower(dir)] = true
end
--------------------------------------------------------------------------------
local function do_restore()
if not settings.get("cwdhistory.restore") then
return
end
local norestore = os.getenv("CWDHISTORY_NORESTORE")
if norestore and tonumber(norestore) ~= 0 then
return
end
return true
end
--------------------------------------------------------------------------------
local function is_in_history_labels_dir(dir, history_labels_dir)
dir = clink.lower(path.normalise(path.join(dir, "")))
history_labels_dir = clink.lower(path.normalise(path.join(history_labels_dir, "")))
if dir:find(history_labels_dir, 1, true) == 1 then
return true
end
end
--------------------------------------------------------------------------------
local restore_dir
clink.oninject(function ()
os.setenv("=cwdhistory_injected", "1")
if do_restore() then
local nocwd = true
update_history(nocwd)
if cwd_history_list then
local last = #cwd_history_list
local dir = cwd_history_list[last].dir
local history_labels_dir = os.getenv("=history_labels_dir")
if not history_labels_dir or is_in_history_labels_dir(dir, history_labels_dir) then
restore_dir = dir
end
end
end
end)
--------------------------------------------------------------------------------
local function need_cd_drive(dir)
local drive = path.getdrive(dir)
if drive then
local cwd = os.getcwd()
if cwd then
local cwd_drive = path.getdrive(cwd)
if cwd_drive and cwd_drive:lower() == drive:lower() then
return
end
end
end
return drive
end
--------------------------------------------------------------------------------
clink.onprovideline(function ()
if restore_dir then
local dir = restore_dir
local drive = need_cd_drive(dir)
restore_dir = nil
-- Ideally this could use CD /D, but that only works if command
-- extensions are enabled.
if drive then
return " " .. drive .. " & cd " .. dir
else
return " cd " .. dir
end
end
end)
--------------------------------------------------------------------------------
local initialized
clink.onbeginedit(function ()
if initialized then
return
end
-- This tries to ensure update_history() runs as the last onbeginedit event
-- handler, in case any set CLINK_HISTORY_LABEL (like history_labels.lua
-- does). Adding an event handler from inside an existing event handler
-- adds the new handler at the end of the list, so the new handler gets run
-- last (it isn't skipped).
initialized = true
clink.onbeginedit(function ()
update_history(restore_dir)
end)
end)
--------------------------------------------------------------------------------
if rl.setbinding then
if not rl.getbinding([["\e[5;2~"]]) then
rl.setbinding([["\e[5;2~"]], [["luafunc:cwdhistory_popup"]])
end
if not rl.getbinding([["\e\C-\"]]) then
rl.setbinding([["\e\C-\"]], [["luafunc:cwdhistory_selectcomplete"]])
end
if not rl.getbinding([["\C-\"]]) then
rl.setbinding([["\C-\"]], [["luafunc:cwdhistory_menucomplete"]])
end
if not rl.getbinding([["\e[27;6;220~"]]) then
rl.setbinding([["\e[27;6;220~"]], [["luafunc:cwdhistory_menucomplete_backward"]])
end
if rl.describemacro then
rl.describemacro([["luafunc:cwdhistory_popup"]], "Show popup list of recent directories")
rl.describemacro([["luafunc:cwdhistory_menucomplete"]], "Replace word with next directory match")
rl.describemacro([["luafunc:cwdhistory_menucomplete_backward"]], "Replace word with previous directory match")
rl.describemacro([["luafunc:cwdhistory_complete"]], "Complete word as a directory")
rl.describemacro([["luafunc:cwdhistory_selectcomplete"]], "Complete word as a directory from an interactive list") -- luacheck: no max line length
end
end