forked from rosshemsley/iOpener
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathi_opener.py
572 lines (440 loc) · 19.2 KB
/
i_opener.py
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
"""
A package to make opening files in Sublime Text a little bit less painful.
Licensed under GPL V2.
Written by Ross Hemsley and other collaborators 2013.
"""
import sublime, sublime_plugin, time, re, logging, sys
from os.path import isdir, isfile, expanduser, split, relpath, join, commonprefix, normpath
from os import listdir, sep, makedirs
from sys import version_info
# Version specific import. Not sure if this is required due to differences in
# the way ST v2 and v3 load packages or due to the different Python versions.
# Do not use the "major" attribute, Python v2.7+ only, ST2 uses v2.6.
python_version_major = version_info[0]
if python_version_major == 3:
from .matching import complete_path, COMPLETION_TYPE, get_matches
from .paths import directory_listing_with_slahes, get_path_to_home, get_path_relative_to_home
elif python_version_major == 2:
from matching import complete_path, COMPLETION_TYPE, get_matches
from paths import directory_listing_with_slahes, get_path_to_home, get_path_relative_to_home
try:
from dired.show import show
except ImportError:
support_dired = False
else:
support_dired = True
rePath = re.compile(r'(\(.*\) )([~|/].*)')
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
# Locations of settings files.
HISTORY_FILE = 'i_opener_history.sublime-settings'
SETTINGS_FILE = 'i_opener.sublime-settings'
STATUS_MESSAGES = {
COMPLETION_TYPE.CompleteButNotUnique: 'Complete, but not unique',
COMPLETION_TYPE.NoMatch: 'No match',
COMPLETION_TYPE.Complete: None,
}
def load_settings():
global OPEN_FOLDERS_IN_NEW_WINDOW
global OPEN_FOLDERS_IN_DIRED
global HISTORY_ENTRIES
global CASE_SENSITIVE
global FOLDER_SEQUENCE
global USER_FOLDERS
settings = sublime.load_settings(SETTINGS_FILE)
OPEN_FOLDERS_IN_NEW_WINDOW = settings.get('open_folders_in_new_window')
OPEN_FOLDERS_IN_DIRED = settings.get('open_folders_in_dired')
CASE_SENSITIVE = settings.get('case_sensitive')
HISTORY_ENTRIES = settings.get('history_entries')
FOLDER_SEQUENCE = settings.get('folder_sequence', [])
USER_FOLDERS = settings.get('folders', [])
def is_sublime_text_2():
return 2000 <= int(sublime.version()) <= 2999
def is_sublime_text_3():
return 3000 <= int(sublime.version()) <= 3999
def get_completion(path):
"""
Function to find and return longest possible completion for a path p from a
list of candidates l. Returns new_path, status, completed.
Find filename and directory.
"""
directory, filename = split(path)
if not isdir(expanduser(directory)):
return path, COMPLETION_TYPE.NoMatch
directory_listing = listdir(expanduser(directory))
new_filename, completion_type = complete_path(filename, directory_listing, CASE_SENSITIVE)
if new_filename != '' and isdir(expanduser(join(directory, new_filename))) and completion_type != COMPLETION_TYPE.CompleteButNotUnique:
new_filename += sep
return join(directory, new_filename), completion_type
class iOpenerPathInput():
"""
This class encapsulates the behaviors relating to the file open panel used
by the package. We create an instance when the panel is open, and destroy
it when the panel is closed.
"""
def __init__(self):
# If the user presses tab, and nothing happens, remember it.
# If they press tab again, we show them a list of files.
self.last_completion_failed = False
self.path_cache = None
# Define and populate the various folders and folder lists.
self.home_folder = None
self.file_folder = None
self.project_folders = []
self.user_folders = []
self.folder_sequence_folders = []
self.set_folders()
# self.folder_cache holds whatever folder list is currently active.
# Set it to initially hold the the folders list created from the
# settings file "folder_sequence" setting.
self.folder_index = 0
self.folder_cache = self.folder_sequence_folders
# Set the first folder to be displayed.
display_folder = self.folder_cache[0]
# We only reload the history each time the input window is opened.
self.history_cache = self.get_history()[0]
# Store displayed folder at end of history cache and 'select' it.
self.history_cache.append(display_folder)
self.history_index = len(self.history_cache) - 1
self.view = sublime.active_window().show_input_panel(
"Find file: ",
display_folder,
self.open_file,
self.update,
self.cancel
)
def update(self,text):
"""
If the user updates the input, reset the 'failed completion' flag.
"""
logging.debug("Update:", text)
if text[-2:] == '//' or text[-2:] == '/~':
self.set_text('(' + text[:-1] + ') ' + text[-1])
elif text[-2:] == ') ':
self.set_text(text[1:-2])
self.last_completion_failed = False
def set_folders(self):
"""
Populate the class folder variables.
"""
active_window = sublime.active_window()
active_view = active_window.active_view()
self.home_folder = get_path_to_home()
view_filename = active_view.file_name()
if view_filename is not None:
view_folder = split(view_filename)[0]
self.file_folder = get_path_relative_to_home(view_folder)
folders = active_window.folders()
for folder in folders:
path = get_path_relative_to_home(folder)
self.project_folders.append(path)
for folder in USER_FOLDERS:
path = get_path_relative_to_home(folder)
self.user_folders.append(path)
self.folder_sequence_folders = self.get_folder_sequence_folders()
def get_folder_sequence_folders(self):
"""
Build a list of folders using the keys specified in the settings file
"folder_sequence" setting. The order in which folders are added to the
list will be the order in which they are displayed.
"""
folders = []
for item in FOLDER_SEQUENCE:
if item == "home":
if self.home_folder not in folders:
folders.append(self.home_folder)
elif item == "file":
if self.file_folder is not None:
if self.file_folder not in folders:
folders.append(self.file_folder)
elif item == "project":
for folder in self.project_folders:
if folder not in folders:
folders.append(folder)
elif item == "user":
for folder in self.user_folders:
if folder not in folders:
folders.append(folder)
# Fail-safe: If empty then use the home folder as a fall back.
if len(folders) == 0:
folders.append(self.home_folder)
return folders
def show_home_folder(self):
self.folder_cache = []
self.folder_cache.append(self.home_folder)
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
sublime.status_message("Changing to: home folder")
def show_file_folder(self):
if self.file_folder is not None:
self.folder_cache = []
self.folder_cache.append(self.file_folder)
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
sublime.status_message("Changing to: folder of open file")
else:
sublime.status_message("No open file folder available")
def show_project_folders(self):
if len(self.project_folders) > 0:
self.folder_cache = self.project_folders
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
sublime.status_message("Changing to: project folder(s)")
else:
sublime.status_message("No project folders available")
def show_user_folders(self):
if len(self.user_folders) > 0:
self.folder_cache = self.user_folders
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
sublime.status_message("Changing to: user defined folder(s)")
else:
sublime.status_message("No user defined folders available")
def show_folder_sequence_folders(self):
self.folder_cache = self.folder_sequence_folders
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
sublime.status_message("Changing to: folder sequence folder(s)")
def goto_prev_folder(self):
self.folder_cache[self.folder_index] = self.get_text()
self.folder_index -= 1
if self.folder_index < 0:
sublime.status_message("Reached start of folder list")
self.folder_index = 0
self.set_text(self.folder_cache[self.folder_index])
def goto_next_folder(self):
self.folder_cache[self.folder_index] = self.get_text()
self.folder_index += 1
if self.folder_index == len(self.folder_cache):
sublime.status_message("Reached end of folder list")
self.folder_index = len(self.folder_cache) - 1
self.set_text(self.folder_cache[self.folder_index])
def goto_prev_history(self):
"""
Temporarily store any changes in cache, as bash does.
"""
self.history_cache[self.history_index] = self.get_text()
self.history_index -= 1
if self.history_index < 0:
sublime.status_message("Reached start of history")
self.history_index = 0
self.set_text( self.history_cache [ self.history_index] )
def goto_next_history(self):
# Temporarily store any changes in cache, as bash does.
self.history_cache[self.history_index] = self.get_text()
self.history_index += 1
if self.history_index == len(self.history_cache):
sublime.status_message("Reached end of history")
self.history_index = len(self.history_cache)-1
self.set_text( self.history_cache [ self.history_index] )
def add_to_history(self,path):
file_history, history_settings = self.get_history()
# Trim the history to the correct length and add latest entry.
if HISTORY_ENTRIES > 1:
file_history = file_history[-HISTORY_ENTRIES+1:]
file_history.append(path)
elif HISTORY_ENTRIES == 1:
file_history = [path]
elif HISTORY_ENTRIES == 0:
file_history = []
# Save updated history.
history_settings.set("file_history", file_history)
sublime.save_settings(HISTORY_FILE)
def get_history(self):
history_settings = sublime.load_settings(HISTORY_FILE)
file_history = history_settings.get("file_history")
# Trim history.
if not file_history:
file_history = []
else:
file_history = file_history[ -HISTORY_ENTRIES :]
return file_history, history_settings
def cancel(self):
"""
Method called when we exit from the input panel without opening a file.
Cancel implies removing panel object.
"""
iOpenerCommand.input_panel = None
def get_text(self):
"""
Get current text being displayed by input panel.
"""
path = self.view.substr(sublime.Region(0, self.view.size()))
m = rePath.match(path)
if m:
path = m.group(2, 1)
else:
path = (path, '')
logging.debug("Get:", path)
return path
def open_file(self, path):
"""
Open the given path. Can be a directory OR a file.
"""
logging.debug("Open File:", path)
m = rePath.match(path)
if m:
path = m.group(2)
path = expanduser(path)
# Ignore empty paths.
if not path:
sublime.status_message("Warning: Ignoring empty path.")
return
self.add_to_history(path)
directory = ""
filename = ""
# If the user enters a path without a filename.
if path[-1] == sep: directory = path
else: directory,filename = split(path)
# Path doesn't exist, ask the user if they want to create it.
if not isdir(directory):
create = sublime.ok_cancel_dialog("The path you entered does not exist, create it?",
"Yes")
if not create:
return
else:
try: makedirs(directory)
except OSError as e:
sublime.error_message("Failed to create path with error: " + str(e))
return
if isdir(path):
if is_sublime_text_2():
# Project folders can not be added using the ST2 API.
sublime.status_message("Warning: Opening folders requires ST v3.")
elif OPEN_FOLDERS_IN_NEW_WINDOW:
sublime.run_command("new_window")
project_data = dict(folders=[dict(follow_symlinks=True, path=path)])
sublime.active_window().set_project_data(project_data)
elif OPEN_FOLDERS_IN_DIRED and support_dired:
show(sublime.active_window(), directory)
else:
project_data = sublime.active_window().project_data() or {}
project_folders = project_data.get('folders') or []
folder = dict(path=path, follow_symlinks=True, folder_exclude_patterns=['.*'])
if all(folder['path'] != path for folder in project_folders):
project_data.setdefault('folders', []).append(folder)
sublime.active_window().set_project_data(project_data)
else:
# If file doesn't exist, add a message in the status bar.
if not isfile(path):
sublime.status_message("Created new buffer '"+filename+"'")
sublime.active_window().open_file(path)
iOpenerCommand.input_panel = None
def set_text(self, s):
"""
Set the text in the file open input panel.
"""
logging.debug("Set:",s)
self.view.run_command("i_opener_update", {"append": False, "text": s})
def show_completions(self):
"""
Show a quick panel containing the possible completions.
"""
active_window = sublime.active_window()
path, old_path = self.get_text()
directory, filename = split(path)
directory_listing = directory_listing_with_slahes(expanduser(directory))
self.path_cache = get_matches(filename, directory_listing, CASE_SENSITIVE)
if len(self.path_cache) == 0:
show_completion_message(COMPLETION_TYPE.NoMatch)
else:
active_window.show_quick_panel(self.path_cache, self.on_done)
def on_done(self, i):
if self.path_cache is None:
return
elif i != -1:
path, old_path = self.get_text()
directory, _ = split(path)
new_path = join(directory, self.path_cache[i])
self.path_cache = None
if isdir(expanduser(new_path)):
self.set_text(old_path + new_path)
sublime.active_window().focus_view(self.view)
else:
self.open_file(new_path)
sublime.active_window().run_command("hide_panel", {"cancel": True})
else:
sublime.active_window().focus_view(self.view)
def append_text(self, s):
logging.debug("Append:",s)
self.view.run_command("i_opener_update", {"append": True, "text": s})
def show_completion_message(completion_type):
status = STATUS_MESSAGES.get(completion_type)
if status is not None:
sublime.status_message(status)
##
# Commands and listeners.
##
class iOpenerEventListener(sublime_plugin.EventListener):
"""
Event listener to allow querying of context. All our events (for now) only
need to know if the plugin is active. If so, Sublime Text calls the correct
commands.
"""
def on_query_context(self, view, key, operator, operand, match_all):
return (key == 'i_opener'
and iOpenerCommand.input_panel != None
and iOpenerCommand.input_panel.view.id() == view.id()
)
class iOpenerUpdateCommand(sublime_plugin.TextCommand):
"""
The edit command used for editing the text in the input panel.
"""
def run(self, edit, append, text):
logging.debug("Update Command:", edit, append, text)
if append: self.view.insert(edit, self.view.size(), text)
else: self.view.replace(edit, sublime.Region(0,self.view.size()), text)
class iOpenerCompleteCommand(sublime_plugin.WindowCommand):
"""
The command called by tapping tab in the open panel.
"""
def run(self):
input_panel = iOpenerCommand.input_panel
if input_panel.last_completion_failed:
input_panel.last_completion_failed = False
input_panel.show_completions()
else:
path, old_path = input_panel.get_text()
completion, completion_type = get_completion(path)
show_completion_message(completion_type)
input_panel.set_text(old_path + completion)
completion_failed = completion_type != COMPLETION_TYPE.Complete
input_panel.last_completion_failed = completion_failed
class iOpenerCycleHistoryCommand(sublime_plugin.WindowCommand):
"""
Receive requests to cycle history.
"""
def run(self, direction):
if direction == "up": iOpenerCommand.input_panel.goto_prev_history()
elif direction == "down": iOpenerCommand.input_panel.goto_next_history()
class iOpenerFolderListCommand(sublime_plugin.WindowCommand):
"""
Receive requests for folder list operations.
"""
def run(self, operation):
if operation == "scroll_up":
iOpenerCommand.input_panel.goto_next_folder()
elif operation == "scroll_down":
iOpenerCommand.input_panel.goto_prev_folder()
elif operation == "show_home_folder":
iOpenerCommand.input_panel.show_home_folder()
elif operation == "show_file_folder":
iOpenerCommand.input_panel.show_file_folder()
elif operation == "show_project_folders":
iOpenerCommand.input_panel.show_project_folders()
elif operation == "show_user_folders":
iOpenerCommand.input_panel.show_user_folders()
elif operation == "show_folder_sequence_folders":
iOpenerCommand.input_panel.show_folder_sequence_folders()
class iOpenerCommand(sublime_plugin.WindowCommand):
"""
This is the command called by the UI. input_panel contains an instance of
the class iOpenerPathInput when the input is active, otherwise it contains
None.
"""
input_panel = None
def run(self):
if (is_sublime_text_2() or is_sublime_text_3()):
load_settings()
iOpenerCommand.input_panel = iOpenerPathInput()
else:
print("iOpener plugin is only for Sublime Text v2 and v3.")