# Internal-only commands for dealing with the directory prompt. Due to Sublime Text's quirky # design we need to re-issue commands over and over during a single prompt. import sublime from sublime import Region from sublime_plugin import WindowCommand, EventListener, TextCommand import os from os.path import basename, join, isdir, dirname, expanduser map_window_to_ctx = {} # Map from window id that is displaying a prompt to its prompt context object. def start(msg, window, path, callback): """ Starts the prompting process. """ if not path.endswith(os.sep): path += os.sep path = expanduser(path) map_window_to_ctx[window.id()] = PromptContext(msg, path, callback) window.run_command('dired_prompt') class PromptContext: def __init__(self, msg, path, callback): self.msg = msg self.path = path # The path we are completing. This is updated as the user types, so it will be an # invalid path at times. self.callback = callback self.completion_view = None def __repr__(self): return '{} {} view:{}'.format(self.path, bool(self.completion_view)) class DiredPromptCommand(WindowCommand): """ An internal-only command that separates prompt handling from external commands since each tab completion requires another command. A prompt context must already be registered in map_window_to_ctx when this is executed. """ def run(self): ctx = map_window_to_ctx[self.window.id()] self.window.show_input_panel(ctx.msg, ctx.path, self.on_done, self.on_change, self.on_cancel) def on_done(self, value): ctx = map_window_to_ctx.pop(self.window.id(), None) self._close_completions() ctx.callback(ctx.path) def on_cancel(self): self._close_completions() def on_change(self, value): # Keep the path in the ctx up to date in case Tab is pressed. It will cancel this completion # and start another. ctx = map_window_to_ctx.get(self.window.id()) if ctx: ctx.path = value def _close_completions(self): ctx = map_window_to_ctx.pop(self.window.id(), None) if ctx and ctx.completion_view: self.window.focus_view(ctx.completion_view) self.window.run_command('close_file') class DiredEventListener(EventListener): def on_query_context(self, view, key, operator, operand, match_all): if not map_window_to_ctx or not key.startswith('dired_'): return None if key == 'dired_complete': return True return False class DiredCompleteCommand(WindowCommand): """ An internal command executed when the user has pressed Tab in our directory prompt. Since a prompt is already in progress, a completion info must already be registered for this window. Update the path, kill the current prompt, and reprompt with the new path. """ def _needs_sep(self, path): """ Returns True if the current value is a complete directory name without a trailing separator, and there are no other possible completions. """ if not isdir(path) or path.endswith(os.sep): return False partial = basename(path) path = dirname(path) if any(n for n in os.listdir(dirname(path)) if n != partial and n.startswith(partial) and isdir(join(path, n))): # There are other completions. return False return True def _parse_split(self, path): """ Split the path into the directory to search and the prefix to match in that directory. If the path is completely invalid, (None, None) is returned. """ prefix = '' if not path.endswith(os.sep): prefix = basename(path) path = dirname(path) if not isdir(path): return (None, None) return (path, prefix) def _close_completions(self, ctx): if ctx.completion_view: self.window.focus_view(ctx.completion_view) self.window.run_command('close_file') ctx.completion_view = None def run(self): ctx = map_window_to_ctx.get(self.window.id()) if not ctx: return path = expanduser(ctx.path) path, prefix = self._parse_split(path) if path is None: print('Invalid:', ctx.path) return completions = [ n for n in os.listdir(path) if n.startswith(prefix) and isdir(join(path, n)) ] if len(completions) == 0: sublime.status_message('No matches') self._close_completions(ctx) return if len(completions) == 1: ctx.path = join(path, completions[0]) + os.sep self.window.run_command('dired_prompt') self._close_completions(ctx) return common = os.path.commonprefix(completions) if common and common > prefix: ctx.path = join(path, common) self.window.run_command('dired_prompt') self._close_completions(ctx) return # There are multiple possibilities. Display a completion view. if not ctx.completion_view: ctx.completion_view = self.window.new_file() ctx.completion_view.set_scratch(True) ctx.completion_view.set_name('*completions*') ctx.completion_view.run_command('dired_show_completions', { "completions": completions }) self.window.focus_view(ctx.completion_view) class DiredShowCompletionsCommand(TextCommand): def run(self, edit, completions=None): self.view.erase(edit, Region(0, self.view.size())) self.view.insert(edit, 0, '\n'.join(completions))