forked from micro-editor/filetree-plugin
-
Notifications
You must be signed in to change notification settings - Fork 16
/
filemanager.lua
1359 lines (1191 loc) · 39.8 KB
/
filemanager.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
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
VERSION = "3.4.0"
-- Let the user disable showing of dotfiles like ".editorconfig" or ".DS_STORE"
if GetOption("filemanager-showdotfiles") == nil then
AddOption("filemanager-showdotfiles", true)
end
-- Let the user disable showing files ignored by the VCS (i.e. gitignored)
if GetOption("filemanager-showignored") == nil then
AddOption("filemanager-showignored", true)
end
-- Let the user disable going to parent directory via left arrow key when file selected (not directory)
if GetOption("filemanager-compressparent") == nil then
AddOption("filemanager-compressparent", true)
end
-- Let the user choose to list sub-folders first when listing the contents of a folder
if GetOption("filemanager-foldersfirst") == nil then
AddOption("filemanager-foldersfirst", true)
end
-- Lets the user have the filetree auto-open any time Micro is opened
-- false by default, as it's a rather noticable user-facing change
if GetOption("filemanager-openonstart") == nil then
AddOption("filemanager-openonstart", false)
end
-- Clear out all stuff in Micro's messenger
local function clear_messenger()
messenger:Reset()
messenger:Clear()
end
-- Holds the CurView() we're manipulating
local tree_view = nil
-- Keeps track of the current working directory
local current_dir = WorkingDirectory()
-- Keep track of current highest visible indent to resize width appropriately
local highest_visible_indent = 0
-- Holds a table of paths -- objects from new_listobj() calls
local scanlist = {}
-- Get a new object used when adding to scanlist
local function new_listobj(p, d, o, i)
return {
["abspath"] = p,
["dirmsg"] = d,
["owner"] = o,
["indent"] = i,
-- Since decreasing/increasing is common, we include these with the object
["decrease_owner"] = function(self, minus_num)
self.owner = self.owner - minus_num
end,
["increase_owner"] = function(self, plus_num)
self.owner = self.owner + plus_num
end
}
end
-- Repeats a string x times, then returns it concatenated into one string
local function repeat_str(str, len)
-- Do NOT try to concat in a loop, it freezes micro...
-- instead, use a temporary table to hold values
local string_table = {}
for i = 1, len do
string_table[i] = str
end
-- Return the single string of repeated characters
return table.concat(string_table)
end
-- A check for if a path is a dir
local function is_dir(path)
-- Used for checking if dir
local golib_os = import("os")
-- Returns a FileInfo on the current file/path
local file_info, stat_error = golib_os.Stat(path)
-- Wrap in nil check for file/dirs without read permissions
if file_info ~= nil then
-- Returns true/false if it's a dir
return file_info:IsDir()
else
-- Couldn't stat the file/dir, usually because no read permissions
messenger:Error("Error checking if is dir: ", stat_error)
-- Nil since we can't read the path
return nil
end
end
-- Returns a list of files (in the target dir) that are ignored by the VCS system (if exists)
-- aka this returns a list of gitignored files (but for whatever VCS is found)
local function get_ignored_files(tar_dir)
-- True/false if the target dir returns a non-fatal error when checked with 'git status'
local function has_git()
local git_rp_results = RunShellCommand('git -C "' .. tar_dir .. '" rev-parse --is-inside-work-tree')
return git_rp_results:match("^true%s*$")
end
local readout_results = {}
-- TODO: Support more than just Git, such as Mercurial or SVN
if has_git() then
-- If the dir is a git dir, get all ignored in the dir
local git_ls_results =
RunShellCommand('git -C "' .. tar_dir .. '" ls-files . --ignored --exclude-standard --others --directory')
-- Cut off the newline that is at the end of each result
for split_results in string.gmatch(git_ls_results, "([^\r\n]+)") do
-- git ls-files adds a trailing slash if it's a dir, so we remove it (if it is one)
readout_results[#readout_results + 1] =
(string.sub(split_results, -1) == "/" and string.sub(split_results, 1, -2) or split_results)
end
end
-- Make sure we return a table
return readout_results
end
-- Returns the basename of a path (aka a name without leading path)
local function get_basename(path)
if path == nil then
messenger:AddLog("Bad path passed to get_basename")
return nil
else
-- Get Go's path lib for a basename callback
local golib_path = import("filepath")
return golib_path.Base(path)
end
end
-- Returns true/false if the file is a dotfile
local function is_dotfile(file_name)
-- Check if the filename starts with a dot
if string.sub(file_name, 1, 1) == "." then
return true
else
return false
end
end
-- Structures the output of the scanned directory content to be used in the scanlist table
-- This is useful for both initial creation of the tree, and when nesting with uncompress_target()
local function get_scanlist(dir, ownership, indent_n)
local golib_ioutil = import("ioutil")
-- Gets a list of all the files in the current dir
local dir_scan, scan_error = golib_ioutil.ReadDir(dir)
-- dir_scan will be nil if the directory is read-protected (no permissions)
if dir_scan == nil then
messenger:Error("Error scanning dir: ", scan_error)
return nil
end
-- The list of files to be returned (and eventually put in the view)
local results = {}
local files = {}
local function get_results_object(file_name)
local abs_path = JoinPaths(dir, file_name)
-- Use "+" for dir's, "" for files
local dirmsg = (is_dir(abs_path) and "+" or "")
return new_listobj(abs_path, dirmsg, ownership, indent_n)
end
-- Save so we don't have to rerun GetOption a bunch
local show_dotfiles = GetOption("filemanager-showdotfiles")
local show_ignored = GetOption("filemanager-showignored")
local folders_first = GetOption("filemanager-foldersfirst")
-- The list of VCS-ignored files (if any)
-- Only bother getting ignored files if we're not showing ignored
local ignored_files = (not show_ignored and get_ignored_files(dir) or {})
-- True/false if the file is an ignored file
local function is_ignored_file(filename)
for i = 1, #ignored_files do
if ignored_files[i] == filename then
return true
end
end
return false
end
-- Hold the current scan's filename in most of the loops below
local filename
for i = 1, #dir_scan do
local showfile = true
filename = dir_scan[i]:Name()
-- If we should not show dotfiles, and this is a dotfile, don't show
if not show_dotfiles and is_dotfile(filename) then
showfile = false
end
-- If we should not show ignored files, and this is an ignored file, don't show
if not show_ignored and is_ignored_file(filename) then
showfile = false
end
if showfile then
-- This file is good to show, proceed
if folders_first and not is_dir(JoinPaths(dir, filename)) then
-- If folders_first and this is a file, add it to (temporary) files
files[#files + 1] = get_results_object(filename)
else
-- Otherwise, add to results
results[#results + 1] = get_results_object(filename)
end
end
end
if #files > 0 then
-- Append any files to results, now that all folders have been added
-- files will be > 0 only if folders_first and there are files
for i = 1, #files do
results[#results + 1] = files[i]
end
end
-- Return the list of scanned files
return results
end
-- A short "get y" for when acting on the scanlist
-- Needed since we don't store the first 3 visible indicies in scanlist
local function get_safe_y(optional_y)
-- Default to 0 so we can check against and see if it's bad
local y = 0
-- Make the passed y optional
if optional_y == nil then
-- Default to cursor's Y loc if nothing was passed, instead of declaring another y
optional_y = tree_view.Buf.Cursor.Loc.Y
end
-- 0/1/2 would be the top "dir, separator, .." so check if it's past
if optional_y > 2 then
-- -2 to conform to our scanlist, since zero-based Go index & Lua's one-based
y = tree_view.Buf.Cursor.Loc.Y - 2
end
return y
end
-- Joins the target dir's leading path to the passed name
local function dirname_and_join(path, join_name)
-- The leading path to the dir we're in
local leading_path = DirectoryName(path)
-- Joins with OS-specific slashes
return JoinPaths(leading_path, join_name)
end
-- Hightlights the line when you move the cursor up/down
local function select_line(last_y)
-- Make last_y optional
if last_y ~= nil then
-- Don't let them move past ".." by checking the result first
if last_y > 1 then
-- If the last position was valid, move back to it
tree_view.Buf.Cursor.Loc.Y = last_y
end
elseif tree_view.Buf.Cursor.Loc.Y < 2 then
-- Put the cursor on the ".." if it's above it
tree_view.Buf.Cursor.Loc.Y = 2
end
-- Puts the cursor back in bounds (if it isn't) for safety
tree_view.Buf.Cursor:Relocate()
-- Makes sure the cursor is visible (if it isn't)
-- (false) means no callback
tree_view:Center(false)
-- Highlight the current line where the cursor is
tree_view.Buf.Cursor:SelectLine()
end
-- Simple true/false if scanlist is currently empty
local function scanlist_is_empty()
if next(scanlist) == nil then
return true
else
return false
end
end
local function refresh_view()
clear_messenger()
-- If it's less than 30, just use 30 for width. Don't want it too small
if tree_view.Width < 30 then
tree_view.Width = 30
end
-- Delete everything in the view/buffer
tree_view.Buf:remove(tree_view.Buf:Start(), tree_view.Buf:End())
-- Insert the top 3 things that are always there
-- Current dir
tree_view.Buf:insert(Loc(0, 0), current_dir .. "\n")
-- An ASCII separator
tree_view.Buf:insert(Loc(0, 1), repeat_str("─", tree_view.Width) .. "\n")
-- The ".." and use a newline if there are things in the current dir
tree_view.Buf:insert(Loc(0, 2), (#scanlist > 0 and "..\n" or ".."))
-- Holds the current basename of the path (purely for display)
local display_content
-- NOTE: might want to not do all these concats in the loop, it can get slow
for i = 1, #scanlist do
-- The first 3 indicies are the dir/separator/"..", so skip them
if scanlist[i].dirmsg ~= "" then
-- Add the + or - to the left to signify if it's compressed or not
-- Add a forward slash to the right to signify it's a dir
display_content = scanlist[i].dirmsg .. " " .. get_basename(scanlist[i].abspath) .. "/"
else
-- Use the basename from the full path for display
-- Two spaces to align with any directories, instead of being "off"
display_content = " " .. get_basename(scanlist[i].abspath)
end
if scanlist[i].owner > 0 then
-- Add a space and repeat it * the indent number
display_content = repeat_str(" ", 2 * scanlist[i].indent) .. display_content
end
-- Newlines are needed for all inserts except the last
-- If you insert a newline on the last, it leaves a blank spot at the bottom
if i < #scanlist then
display_content = display_content .. "\n"
end
-- Insert line-by-line to avoid out-of-bounds on big folders
-- +2 so we skip the 0/1/2 positions that hold the top dir/separator/..
tree_view.Buf:insert(Loc(0, i + 2), display_content)
end
-- Resizes all views after messing with ours
tabs[curTab + 1]:Resize()
end
-- Moves the cursor to the ".." in tree_view
local function move_cursor_top()
-- 2 is the position of the ".."
tree_view.Buf.Cursor.Loc.Y = 2
-- select the line after moving
select_line()
end
local function refresh_and_select()
-- Save the cursor position before messing with the view..
-- because changing contents in the view causes the Y loc to move
local last_y = tree_view.Buf.Cursor.Loc.Y
-- Actually refresh
refresh_view()
-- Moves the cursor back to it's original position
select_line(last_y)
end
-- Find everything nested under the target, and remove it from the scanlist
local function compress_target(y, delete_y)
-- Can't compress the top stuff, or if there's nothing there, so exit early
if y == 0 or scanlist_is_empty() then
return
end
-- Check if the target is a dir, since files don't have anything to compress
-- Also make sure it's actually an uncompressed dir by checking the gutter message
if scanlist[y].dirmsg == "-" then
local target_index, delete_index
-- Add the original target y to stuff to delete
local delete_under = {[1] = y}
local new_table = {}
local del_count = 0
-- Loop through the whole table, looking for nested content, or stuff with ownership == y...
-- and delete matches. y+1 because we want to start under y, without actually touching y itself.
for i = 1, #scanlist do
delete_index = false
-- Don't run on y, since we don't always delete y
if i ~= y then
-- On each loop, check if the ownership matches
for x = 1, #delete_under do
-- Check for something belonging to a thing to delete
if scanlist[i].owner == delete_under[x] then
-- Delete the target if it has an ownership to our delete target
delete_index = true
-- Keep count of total deleted (can't use #delete_under because it's for deleted dir count)
del_count = del_count + 1
-- Check if an uncompressed dir
if scanlist[i].dirmsg == "-" then
-- Add the index to stuff to delete, since it holds nested content
delete_under[#delete_under + 1] = i
end
-- See if we're on the "deepest" nested content
if scanlist[i].indent == highest_visible_indent and scanlist[i].indent > 0 then
-- Save the lower indent, since we're minimizing/deleting nested dirs
highest_visible_indent = highest_visible_indent - 1
end
-- Nothing else to do, so break this inner loop
break
end
end
end
if not delete_index then
-- Save the index in our new table
new_table[#new_table + 1] = scanlist[i]
end
end
scanlist = new_table
if del_count > 0 then
-- Ownership adjusting since we're deleting an index
for i = y + 1, #scanlist do
-- Don't touch root file/dirs
if scanlist[i].owner > y then
-- Minus ownership, on everything below i, the number deleted
scanlist[i]:decrease_owner(del_count)
end
end
end
-- If not deleting, then update the gutter message to be + to signify compressed
if not delete_y then
-- Update the dir message
scanlist[y].dirmsg = "+"
end
elseif GetOption("filemanager-compressparent") and not delete_y then
goto_parent_dir()
-- Prevent a pointless refresh of the view
return
end
-- Put outside check above because we call this to delete targets as well
if delete_y then
local second_table = {}
-- Quickly remove y
for i = 1, #scanlist do
if i == y then
-- Reduce everything's ownership by 1 after y
for x = i + 1, #scanlist do
-- Don't touch root file/dirs
if scanlist[x].owner > y then
-- Minus 1 since we're just deleting y
scanlist[x]:decrease_owner(1)
end
end
else
-- Put everything but y into the temporary table
second_table[#second_table + 1] = scanlist[i]
end
end
-- Put everything (but y) back into scanlist, with adjusted ownership values
scanlist = second_table
end
if tree_view.Width > (30 + highest_visible_indent) then
-- Shave off some width
tree_view.Width = 30 + highest_visible_indent
end
refresh_and_select()
end
-- Prompts the user for deletion of a file/dir when triggered
-- Not local so Micro can access it
function prompt_delete_at_cursor()
local y = get_safe_y()
-- Don't let them delete the top 3 index dir/separator/..
if y == 0 or scanlist_is_empty() then
messenger:Error("You can't delete that")
-- Exit early if there's nothing to delete
return
end
local yes_del, no_del =
messenger:YesNoPrompt(
"Do you want to delete the " .. (scanlist[y].dirmsg ~= "" and "dir" or "file") .. ' "' .. scanlist[y].abspath .. '"? '
)
if yes_del and not no_del then
-- Use Go's os.Remove to delete the file
local go_os = import("os")
-- Delete the target (if its a dir then the children too)
local remove_log = go_os.RemoveAll(scanlist[y].abspath)
if remove_log == nil then
messenger:Message("Filemanager deleted: ", scanlist[y].abspath)
-- Remove the target (and all nested) from scanlist[y + 1]
-- true to delete y
compress_target(get_safe_y(), true)
else
messenger:Error("Failed deleting file/dir: ", remove_log)
end
else
messenger:Message("Nothing was deleted")
end
end
-- Changes the current dir in the top of the tree..
-- then scans that dir, and prints it to the view
local function update_current_dir(path)
-- Clear the highest since this is a full refresh
highest_visible_indent = 0
-- Set the width back to 30
tree_view.Width = 30
-- Update the current dir to the new path
current_dir = path
-- Get the current working dir's files into our list of files
-- 0 ownership because this is a scan of the base dir
-- 0 indent because this is the base dir
local scan_results = get_scanlist(path, 0, 0)
-- Safety check with not-nil
if scan_results ~= nil then
-- Put in the new scan stuff
scanlist = scan_results
else
-- If nil, just empty it
scanlist = {}
end
refresh_view()
-- Since we're going into a new dir, move cursor to the ".." by default
move_cursor_top()
end
-- (Tries to) go back one "step" from the current directory
local function go_back_dir()
-- Use Micro's dirname to get everything but the current dir's path
local one_back_dir = DirectoryName(current_dir)
-- Try opening, assuming they aren't at "root", by checking if it matches last dir
if one_back_dir ~= current_dir then
-- If DirectoryName returns different, then they can move back..
-- so we update the current dir and refresh
update_current_dir(one_back_dir)
end
end
-- Tries to open the current index
-- If it's the top dir indicator, or separator, nothing happens
-- If it's ".." then it tries to go back a dir
-- If it's a dir then it moves into the dir and refreshes
-- If it's actually a file, open it in a new vsplit
-- THIS EXPECTS ZERO-BASED Y
local function try_open_at_y(y)
-- 2 is the zero-based index of ".."
if y == 2 then
go_back_dir()
elseif y > 2 and not scanlist_is_empty() then
-- -2 to conform to our scanlist "missing" first 3 indicies
y = y - 2
if scanlist[y].dirmsg ~= "" then
-- if passed path is a directory, update the current dir to be one deeper..
update_current_dir(scanlist[y].abspath)
else
-- If it's a file, then open it
messenger:Message("Filemanager opened ", scanlist[y].abspath)
-- Opens the absolute path in new vertical view
CurView():VSplitIndex(NewBufferFromFile(scanlist[y].abspath), 1)
-- Resizes all views after opening a file
tabs[curTab + 1]:Resize()
end
else
messenger:Error("Can't open that")
end
end
-- Opens the dir's contents nested under itself
local function uncompress_target(y)
-- Exit early if on the top 3 non-list items
if y == 0 or scanlist_is_empty() then
return
end
-- Only uncompress if it's a dir and it's not already uncompressed
if scanlist[y].dirmsg == "+" then
-- Get a new scanlist with results from the scan in the target dir
local scan_results = get_scanlist(scanlist[y].abspath, y, scanlist[y].indent + 1)
-- Don't run any of this if there's nothing in the dir we scanned, pointless
if scan_results ~= nil then
-- Will hold all the old values + new scan results
local new_table = {}
-- By not inserting in-place, some unexpected results can be avoided
-- Also, table.insert actually moves values up (???) instead of down
for i = 1, #scanlist do
-- Put the current val into our new table
new_table[#new_table + 1] = scanlist[i]
if i == y then
-- Fill in the scan results under y
for x = 1, #scan_results do
new_table[#new_table + 1] = scan_results[x]
end
-- Basically "moving down" everything below y, so ownership needs to increase on everything
for inner_i = y + 1, #scanlist do
-- When root not pushed by inserting, don't change its ownership
-- This also has a dual-purpose to make it not effect root file/dirs
-- since y is always >= 3
if scanlist[inner_i].owner > y then
-- Increase each indicies ownership by the number of scan results inserted
scanlist[inner_i]:increase_owner(#scan_results)
end
end
end
end
-- Update our scanlist with the new values
scanlist = new_table
end
-- Change to minus to signify it's uncompressed
scanlist[y].dirmsg = "-"
-- Check if we actually need to resize, or if we're nesting at the same indent
-- Also check if there's anything in the dir, as we don't need to expand on an empty dir
if scan_results ~= nil then
if scanlist[y].indent > highest_visible_indent and #scan_results >= 1 then
-- Save the new highest indent
highest_visible_indent = scanlist[y].indent
-- Increase the width to fit the new nested content
tree_view.Width = tree_view.Width + scanlist[y].indent
end
end
refresh_and_select()
end
end
-- Stat a path to check if it exists, returning true/false
local function path_exists(path)
local go_os = import("os")
-- Stat the file/dir path we created
-- file_stat should be non-nil, and stat_err should be nil on success
local file_stat, stat_err = go_os.Stat(path)
-- Check if what we tried to create exists
if stat_err ~= nil then
-- true/false if the file/dir exists
return go_os.IsExist(stat_err)
elseif file_stat ~= nil then
-- Assume it exists if no errors
return true
end
return false
end
-- Prompts for a new name, then renames the file/dir at the cursor's position
-- Not local so Micro can use it
function rename_at_cursor(new_name)
if CurView() ~= tree_view then
messenger:Message("Rename only works with the cursor in the tree!")
return
end
-- Safety check they actually passed a name
if new_name == nil then
messenger:Error('When using "rename" you need to input a new name')
return
end
-- +1 since Go uses zero-based indices
local y = get_safe_y()
-- Check if they're trying to rename the top stuff
if y == 0 then
-- Error since they tried to rename the top stuff
messenger:Message("You can't rename that!")
return
end
-- The old file/dir's path
local old_path = scanlist[y].abspath
-- Join the path into their supplied rename, so that we have an absolute path
local new_path = dirname_and_join(old_path, new_name)
-- Use Go's os package for renaming the file/dir
local golib_os = import("os")
-- Actually rename the file
local log_out = golib_os.Rename(old_path, new_path)
-- Output the log, if any, of the rename
if log_out ~= nil then
messenger:AddLog("Rename log: ", log_out)
end
-- Check if the rename worked
if not path_exists(new_path) then
messenger:Error("Path doesn't exist after rename!")
return
end
-- NOTE: doesn't alphabetically sort after refresh, but it probably should
-- Replace the old path with the new path
scanlist[y].abspath = new_path
-- Refresh the tree with our new name
refresh_and_select()
end
-- Prompts the user for the file/dir name, then creates the file/dir using Go's os package
local function create_filedir(filedir_name, make_dir)
if CurView() ~= tree_view then
messenger:Message("You can't create a file/dir if your cursor isn't in the tree!")
return
end
-- Safety check they passed a name
if filedir_name == nil then
messenger:Error('You need to input a name when using "touch" or "mkdir"!')
return
end
-- The target they're trying to create on top of/in/at/whatever
local y = get_safe_y()
-- Holds the path passed to Go for the eventual new file/dir
local filedir_path
-- A true/false if scanlist is empty
local scanlist_empty = scanlist_is_empty()
-- Check there's actually anything in the list, and that they're not on the ".."
if not scanlist_empty and y ~= 0 then
-- If they're inserting on a folder, don't strip its path
if scanlist[y].dirmsg ~= "" then
-- Join our new file/dir onto the dir
filedir_path = JoinPaths(scanlist[y].abspath, filedir_name)
else
-- The current index is a file, so strip its name and join ours onto it
filedir_path = dirname_and_join(scanlist[y].abspath, filedir_name)
end
else
-- if nothing in the list, or cursor is on top of "..", use the current dir
filedir_path = JoinPaths(current_dir, filedir_name)
end
-- Check if the name is already taken by a file/dir
if path_exists(filedir_path) then
messenger:Error("You can't create a file/dir with a pre-existing name")
return
end
-- Use Go's os package for creating the files
local golib_os = import("os")
-- Create the dir or file
if make_dir then
-- Creates the dir
golib_os.Mkdir(filedir_path, golib_os.ModePerm)
messenger:AddLog("Filemanager created directory: " .. filedir_path)
else
-- Creates the file
golib_os.Create(filedir_path)
messenger:AddLog("Filemanager created file: " .. filedir_path)
end
-- If the file we tried to make doesn't exist, fail
if not path_exists(filedir_path) then
messenger:Error("The file/dir creation failed")
return
end
-- Creates a sort of default object, to be modified below
-- If creating a dir, use a "+"
local new_filedir = new_listobj(filedir_path, (make_dir and "+" or ""), 0, 0)
-- Refresh with our new value(s)
local last_y
-- Only insert to scanlist if not created into a compressed dir, since it'd be hidden if it was
-- Wrap the below checks so a y=0 doesn't break something
if not scanlist_empty and y ~= 0 then
-- +1 so it's highlighting the new file/dir
last_y = tree_view.Buf.Cursor.Loc.Y + 1
-- Only actually add the object to the list if it's not created on an uncompressed folder
if scanlist[y].dirmsg == "+" then
-- Exit early, since it was created into an uncompressed folder
return
elseif scanlist[y].dirmsg == "-" then
-- Check if created on top of an uncompressed folder
-- Change ownership to the folder it was created on top of..
-- otherwise, the ownership would be incorrect
new_filedir.owner = y
-- We insert under the folder, so increment the indent
new_filedir.indent = scanlist[y].indent + 1
else
-- This triggers if the cursor is on top of a file...
-- so we copy the properties of it
new_filedir.owner = scanlist[y].owner
new_filedir.indent = scanlist[y].indent
end
-- A temporary table for adding our new object, and manipulation
local new_table = {}
-- Insert the new file/dir, and update ownership of everything below it
for i = 1, #scanlist do
-- Don't use i as index, as it will be off by one on the next pass after below "i == y"
new_table[#new_table + 1] = scanlist[i]
if i == y then
-- Insert our new file/dir (below the last item)
new_table[#new_table + 1] = new_filedir
-- Increase ownership of everything below it, since we're inserting
-- Basically "moving down" everything below y, so ownership needs to increase on everything
for inner_i = y + 1, #scanlist do
-- When root not pushed by inserting, don't change its ownership
-- This also has a dual-purpose to make it not effect root file/dirs
-- since y is always >= 3
if scanlist[inner_i].owner > y then
-- Increase each indicies ownership by 1 since we're only inserting 1 file/dir
scanlist[inner_i]:increase_owner(1)
end
end
end
end
-- Update the scanlist with the new object & updated ownerships
scanlist = new_table
else
-- The scanlist is empty (or cursor is on ".."), so we add on our new file/dir at the bottom
scanlist[#scanlist + 1] = new_filedir
-- Add current position so it takes into account where we are
last_y = #scanlist + tree_view.Buf.Cursor.Loc.Y
end
refresh_view()
select_line(last_y)
end
-- Triggered with "touch filename"
function new_file(input_name)
-- False because not a dir
create_filedir(input_name, false)
end
-- Triggered with "mkdir dirname"
function new_dir(input_name)
-- True because dir
create_filedir(input_name, true)
end
-- open_tree setup's the view
local function open_tree()
-- Open a new Vsplit (on the very left)
CurView():VSplitIndex(NewBuffer("", "filemanager"), 0)
-- Save the new view so we can access it later
tree_view = CurView()
-- Set the width of tree_view to 30% & lock it
tree_view.Width = 30
tree_view.LockWidth = true
-- Set the type to unsavable (A "vtScratch" ViewType)
tree_view.Type.Kind = 2
tree_view.Type.Readonly = true
tree_view.Type.Scratch = true
-- Set the various display settings, but only on our view (by using SetLocalOption instead of SetOption)
-- NOTE: Micro requires the true/false to be a string
-- Softwrap long strings (the file/dir paths)
SetLocalOption("softwrap", "true", tree_view)
-- No line numbering
SetLocalOption("ruler", "false", tree_view)
-- Is this needed with new non-savable settings from being "vtLog"?
SetLocalOption("autosave", "false", tree_view)
-- Don't show the statusline to differentiate the view from normal views
SetLocalOption("statusline", "false", tree_view)
SetLocalOption("scrollbar", "false", tree_view)
-- Fill the scanlist, and then print its contents to tree_view
update_current_dir(WorkingDirectory())
end
-- close_tree will close the tree plugin view and release memory.
local function close_tree()
if tree_view ~= nil then
tree_view:Quit(false)
tree_view = nil
clear_messenger()
end
end
-- toggle_tree will toggle the tree view visible (create) and hide (delete).
function toggle_tree()
if tree_view == nil then
open_tree()
else
close_tree()
end
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Functions exposed specifically for the user to bind
-- Some are used in callbacks as well
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function uncompress_at_cursor()
if CurView() == tree_view then
uncompress_target(get_safe_y())
end
end
function compress_at_cursor()
if CurView() == tree_view then
-- False to not delete y
compress_target(get_safe_y(), false)
end
end
-- Goes up 1 visible directory (if any)
-- Not local so it can be bound
function goto_prev_dir()
if CurView() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
-- If they try to run it on the ".." do nothing
if cur_y ~= 0 then
local move_count = 0
for i = cur_y - 1, 1, -1 do
move_count = move_count + 1
-- If a dir, stop counting
if scanlist[i].dirmsg ~= "" then
-- Jump to its parent (the ownership)
tree_view.Buf.Cursor:UpN(move_count)
select_line()
break
end
end
end
end
-- Goes down 1 visible directory (if any)
-- Not local so it can be bound
function goto_next_dir()
if CurView() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
local move_count = 0
-- If they try to goto_next on "..", pretends the cursor is valid
if cur_y == 0 then
cur_y = 1
move_count = 1
end
-- Only do anything if it's even possible for there to be another dir
if cur_y < #scanlist then
for i = cur_y + 1, #scanlist do
move_count = move_count + 1
-- If a dir, stop counting
if scanlist[i].dirmsg ~= "" then
-- Jump to its parent (the ownership)
tree_view.Buf.Cursor:DownN(move_count)
select_line()
break
end
end
end
end
-- Goes to the parent directory (if any)
-- Not local so it can be keybound
function goto_parent_dir()
if CurView() ~= tree_view or scanlist_is_empty() then
return
end
local cur_y = get_safe_y()
-- Check if the cursor is even in a valid location for jumping to the owner
if cur_y > 0 then
-- Jump to its parent (the ownership)
tree_view.Buf.Cursor:UpN(cur_y - scanlist[cur_y].owner)
select_line()
end
end
function try_open_at_cursor()
if CurView() ~= tree_view or scanlist_is_empty() then
return
end
try_open_at_y(tree_view.Buf.Cursor.Loc.Y)
end
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Shorthand functions for actions to reduce repeat code
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-- Used to fail certain actions that we shouldn't allow on the tree_view
local function false_if_tree(view)
if view == tree_view then
return false
end
end
-- Select the line at the cursor
local function selectline_if_tree(view)
if view == tree_view then
select_line()
end
end
-- Move the cursor to the top, but don't allow the action
local function aftermove_if_tree(view)
if view == tree_view then
if tree_view.Buf.Cursor.Loc.Y < 2 then
-- If it went past the "..", move back onto it
tree_view.Buf.Cursor:DownN(2 - tree_view.Buf.Cursor.Loc.Y)
end
select_line()
end
end
local function clearselection_if_tree(view)
if view == tree_view then
-- Clear the selection when doing a find, so it doesn't copy the current line
tree_view.Buf.Cursor:ResetSelection()