# coding: utf-8 '''Main module; launch and navigation related stuff''' from __future__ import print_function import sublime from sublime import Region from sublime_plugin import WindowCommand, TextCommand import os from os.path import basename, dirname, isdir, exists, join ST3 = int(sublime.version()) >= 3000 if ST3: from .common import DiredBaseCommand, print, set_proper_scheme, calc_width, get_group, hijack_window, emit_event, NT, PARENT_SYM from . import prompt from .show import show from .jumping import jump_names else: # ST2 imports from common import DiredBaseCommand, print, set_proper_scheme, calc_width, get_group, hijack_window, emit_event, NT, PARENT_SYM import prompt from show import show from jumping import jump_names def reuse_view(): return sublime.load_settings('dired.sublime-settings').get('dired_reuse_view', False) def plugin_loaded(): if len(sublime.windows()) == 1 and len(sublime.windows()[0].views()) == 0: hijack_window() window = sublime.active_window() if not ST3: global recursive_plugin_loaded # recursion limit is 1000 generally, so it will try to refresh for 100*1000 ms (100 s) # if no active_window in 100 s, then no refresh # if view still loading, refresh fail because view cant be edited if not window or any(view.is_loading() for view in window.views()): recursive_plugin_loaded += 1 try: return sublime.set_timeout(plugin_loaded, 100) except RuntimeError: print('\ndired.plugin_loaded run recursively %d time(s); and failed to refresh\n' % recursive_plugin_loaded) return for w in sublime.windows(): for v in w.views(): if v.settings() and v.settings().get("dired_path"): # reset sels because dired_index not exists yet, so we cant restore sels v.run_command("dired_refresh", {"reset_sels": True}) import sys dfsobserver = '%s0_dired_fs_observer' % ('FileBrowser.' if ST3 else '') if dfsobserver not in sys.modules or sys.modules[dfsobserver].Observer is None: return sublime.error_message( u'FileBrowser:\n\n' u'watchdog module is not importable, hence we cannot know about ' u'changes on file system, and auto-refresh will not work.\n\n' u'Despite that, FileBrowser is fully usable without auto-refresh, ' u'you can just ignore this message and manually refresh view with r key.\n\n' u'But if you want working auto-refresh:\n' u' • if you install manually, then look at Readme how to install it,\n' u' • if you install via Package Control, report an issue.') sublime.load_settings('dired.sublime-settings').add_on_change('dired_autorefresh', lambda: emit_event(u'toggle_watch_all', sublime.load_settings('dired.sublime-settings').get('dired_autorefresh', None))) # if not ST3: # print('\ndired.plugin_loaded run recursively %d time(s); and call refresh command\n'%recursive_plugin_loaded) def plugin_unloaded(): sublime.load_settings('dired.sublime-settings').clear_on_change('dired_autorefresh') if not ST3: unload_handler = plugin_unloaded recursive_plugin_loaded = 1 plugin_loaded() class DiredCommand(WindowCommand, DiredBaseCommand): """ Prompt for a directory to display and display it. """ def run(self, immediate=False, single_pane=False, project=False, other_group=False): path, goto = self._determine_path() if project: folders = self.window.folders() if len(folders) == 1: path = folders[0] elif folders: names = [basename(f) for f in folders] longest_name = max([len(n) for n in names]) for i, f in enumerate(folders): name = names[i] offset = ' ' * (longest_name - len(name) + 1) names[i] = u'%s%s%s' % (name, offset, self.display_path(f)) self.window.show_quick_panel(names, lambda i: self._show_folder(i, path, goto, single_pane, other_group), sublime.MONOSPACE_FONT) return if immediate: show(self.window, path, goto=goto, single_pane=single_pane, other_group=other_group) else: prompt.start('Directory:', self.window, path, self._show) def _show_folder(self, index, path, goto, single_pane, other_group): if index != -1: choice = self.window.folders()[index] if path == choice: show(self.window, path, goto=goto, single_pane=single_pane, other_group=other_group) else: show(self.window, choice, single_pane=single_pane, other_group=other_group) def _show(self, path): show(self.window, path) def _determine_path(self): '''Return (path, fname) so goto=fname to set cursor''' # Use the current view's directory if it has one. view = self.window.active_view() path = view and view.file_name() folders = self.window.folders() if path: for f in folders: # e.g. ['/a', '/aa'], to open '/aa/f' we need '/aa/' if path.startswith(u''.join([f, os.sep])): return (f, path) return os.path.split(path) # Use the first project folder if there is one. data = self.window.project_data() if ST3 else None if data and 'folders' in data: folders = data['folders'] if folders: return (folders[0]['path'], '') # Use window folder if possible if len(folders) > 0: return (folders[0], '') # Use the user's home directory. return (os.path.expanduser('~'), '') class DiredRefreshCommand(TextCommand, DiredBaseCommand): """ Populates or repopulates a dired view. self.index is a representation of view lines list contains full path of each item in a view, except header ['', ''] and parent_dir [PARENT_SYM] self.index shall be updated according to view modifications (refresh, expand single directory, fold) and stored in view settings as 'dired_index' The main reason for index is access speed to item path because we can self.index[self.view.rowcol(region.a)[0]] to get full path, instead of grinding with substr thru entire view substr is slow: https://github.com/SublimeTextIssues/Core/issues/882 """ def run(self, edit, goto='', to_expand=None, toggle=None, reset_sels=None): """ goto Optional filename to put the cursor on; used only from "dired_up" to_expand List of relative paths for direcories which shall be expanded toggle If true, marked/selected directories shall switch state, i.e. expand/collapse reset_sels If True, previous selections & marks shan’t be restored """ # after restart ST, callback seems to disappear, so reset callback on each refresh for more reliability self.view.settings().clear_on_change('color_scheme') self.view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(self.view)) path = self.path names = [] if path == 'ThisPC\\': path, names = '', self.get_disks() if path and not exists(path): if sublime.ok_cancel_dialog(u'FileBrowser:\n\nDirectory does not exist:\n\n\t%s\n\nTry to go up?' % path, u'Go'): self.view.run_command('dired_up') return emit_event(u'start_refresh', (self.view.id(), path), view=self.view) self.expanded = expanded = self.view.find_all(u'^\s*▾') if not reset_sels else [] self.show_hidden = self.view.settings().get('dired_show_hidden_files', True) self.goto = goto if os.sep in goto: to_expand = self.expand_goto(to_expand) self.number_line = 0 if reset_sels and not to_expand: self.index, self.marked, self.sels = [], None, None self.populate_view(edit, path, names) else: if not reset_sels: self.index = self.get_all() self.marked = self.get_marked() self.sels = (self.get_selected(), list(self.view.sel())) else: self.marked, self.sels = None, None self.re_populate_view(edit, path, names, expanded, to_expand, toggle) emit_event(u'finish_refresh', (self.view.id(), self.expanded + ([path] if path else [])), view=self.view) def expand_goto(self, to_expand): '''e.g. self.goto = "a/b/c/d/", then to put cursor onto d, it should be to_expand = ["a/", "a/b/", "a/b/c/"] (items order in list dont matter) ''' to_expand = to_expand or [] goto = self.goto while len(goto.split(os.sep)) > 2: parent = dirname(goto) + os.sep to_expand.append(parent) goto = parent.rstrip(os.sep) return to_expand def re_populate_view(self, edit, path, names, expanded, to_expand, toggle): '''Called when we know that some directories were (or/and need to be) expanded''' root = path for i, r in enumerate(expanded): name = self.get_fullpath_for(r) expanded[i] = name if toggle and to_expand: merged = list(set(expanded + to_expand)) expanded = [e for e in merged if not (e in expanded and e in to_expand)] else: expanded.extend(to_expand or []) self.expanded = expanded # we need prev index to setup expanded list — done, so reset index self.index = [] tree = self.traverse_tree(root, root, '', names, expanded) if not tree: return self.populate_view(edit, path, names) self.set_status() items = self.correcting_index(path, tree) self.write(edit, items) self.restore_selections(path) self.view.run_command('dired_call_vcs', {'path': path}) def populate_view(self, edit, path, names): '''Called when no directories were (or/and need to be) expanded''' if not path and names: # open ThisPC self.continue_populate(edit, path, names) return items, error = self.try_listing_directory(path) if error: self.view.run_command("dired_up") self.view.set_read_only(False) self.view.insert(edit, self.view.line(self.view.sel()[0]).b, u'\t<%s>' % error) self.view.set_read_only(True) else: self.continue_populate(edit, path, items) def continue_populate(self, edit, path, names): '''Called if there is no exception in self.populate_view''' self.sel = None self.set_status() items = self.correcting_index(path, self.prepare_filelist(names, path, '', '')) self.write(edit, items) self.restore_selections(path) self.view.run_command('dired_call_vcs', {'path': path}) def traverse_tree(self, root, path, indent, tree, expanded): '''Recursively build list of filenames for self.re_populate_view''' if not path: # special case for ThisPC, path is empty string items = [u'%s\\' % d for d in tree] tree = [] else: if indent: # this happens during recursive call, i.e. path in expanded # basename return funny results for c:\\ so it is tricky bname = os.path.basename(os.path.abspath(path)) or path.rstrip(os.sep) tree.append(u'%s▾ %s%s' % (indent[:-1], bname.rstrip(os.sep), os.sep)) self.index.append(u'%s' % path) items, error = self.try_listing_directory(path) if error: tree[~0] += u'\t<%s>' % error return if not items: if path == root: return [] # expanding empty folder, so notify that it is empty tree[~0] += '\t<empty>' return files = [] index_files = [] for f in items: new_path = join(path, f) dir_path = u'%s%s' % (new_path.rstrip(os.sep), os.sep) check = isdir(new_path) if check and dir_path in expanded: self.traverse_tree(root, dir_path, indent + '\t', tree, expanded) elif check: self.index.append(dir_path) tree.append(u'%s▸ %s%s' % (indent, f.rstrip(os.sep), os.sep)) else: index_files.append(new_path) files.append(u'%s≡ %s' % (indent, f)) self.index += index_files tree += files return tree def set_title(self, path): '''Update name of tab and return tuple of two elements text list of two unicode obj (will be inserted before filenames in view) or empty list header boolean, value of dired_header setting ''' header = self.view.settings().get('dired_header', False) name = jump_names().get(path or self.path) caption = u"{0} → {1}".format(name, path) if name else path or self.path text = [caption, len(caption)*(u'—')] if header else [] icon = self.view.name()[:2] if not path: title = u'%s%s' % (icon, name or 'This PC') else: norm_path = path.rstrip(os.sep) if self.view.settings().get('dired_show_full_path', False): title = u'%s%s (%s)' % (icon, name or basename(norm_path), norm_path) else: title = u'%s%s' % (icon, name or basename(norm_path)) self.view.set_name(title) return (text, header) def write(self, edit, fileslist): '''apply changes to view''' self.view.set_read_only(False) self.view.erase(edit, Region(0, self.view.size())) self.view.insert(edit, 0, '\n'.join(fileslist)) self.view.set_read_only(True) fileregion = self.fileregion() count = len(self.view.lines(fileregion)) if fileregion else 0 self.view.settings().set('dired_count', count) self.view.settings().set('dired_index', self.index) def correcting_index(self, path, fileslist): '''Add leading elements to self.index (if any), we need conformity of elements in self.index and line numbers in view Return list of unicode objects that ready to be inserted in view ''' text, header = self.set_title(path) if path and (not fileslist or self.show_parent()): text.append(PARENT_SYM) self.index = [PARENT_SYM] + self.index self.number_line += 1 if header: self.index = ['', ''] + self.index self.number_line += 2 return text + fileslist def restore_selections(self, path): '''Set cursor(s) and mark(s)''' self.restore_marks(self.marked) if self.goto: if self.goto[~0] != os.sep: self.goto += (os.sep if isdir(join(path, self.goto)) else '') self.sels = ([self.goto.replace(path, '', 1)], None) self.restore_sels(self.sels) def get_disks(self): '''create list of disks on Windows for ThisPC folder''' names = [] for s in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': disk = '%s:' % s if isdir(disk): names.append(disk) return names # NAVIGATION ##################################################### class DiredNextLineCommand(TextCommand, DiredBaseCommand): def run(self, edit, forward=None): self.move(forward) class DiredMoveCommand(TextCommand, DiredBaseCommand): def run(self, edit, to="bof"): self.move_to_extreme(to) class DiredSelect(TextCommand, DiredBaseCommand): '''Common command for opening file/directory in existing view''' def run(self, edit, new_view=0, other_group=0, and_close=0): ''' new_view if True, open directory in new view, rather than existing one other_group if True, create a new group (if need) and open file in this group and_close if True, close FileBrowser view after file was open ''' self.index = self.get_all() filenames = (self.get_selected(full=True) if not new_view else self.get_marked(full=True) or self.get_selected(full=True)) window = self.view.window() if self.goto_directory(filenames, window, new_view): return dired_view = self.view if other_group: self.focus_other_group(window) self.last_created_view = None for fqn in filenames: self.open_item(fqn, window, new_view) if and_close: window.focus_view(dired_view) window.run_command("close") if self.last_created_view: window.focus_view(self.last_created_view) def goto_directory(self, filenames, window, new_view): '''If reuse view is turned on and the only item is a directory, refresh the existing view''' if new_view and reuse_view(): return False fqn = filenames[0] if len(filenames) == 1 and isdir(fqn): show(self.view.window(), fqn, view_id=self.view.id()) return True elif fqn == PARENT_SYM: self.view.window().run_command("dired_up") return True return False def open_item(self, fqn, window, new_view): if isdir(fqn): show(window, fqn, ignore_existing=new_view) elif exists(fqn): # ignore 'item <error>' self.last_created_view = window.open_file(fqn) else: sublime.status_message(u'File does not exist (%s)' % (basename(fqn.rstrip(os.sep)) or fqn)) def focus_other_group(self, window): '''call it when preview open in other group''' target_group = self._other_group(window, window.active_group()) # set_view_index and focus are not very reliable # just focus target_group should do what we want window.focus_group(target_group) def _other_group(self, w, nag): ''' creates new group if need and return index of the group where files shall be opened ''' groups = w.num_groups() if groups == 1: width = calc_width(self.view) w.set_layout({"cols": [0.0, width, 1.0], "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1], [1, 0, 2, 1]]}) group = get_group(groups, nag) return group class DiredPreviewCommand(DiredSelect): '''Open file as a preview, so focus remains in FileBrowser view''' def run(self, edit): self.index = self.get_all() filenames = self.get_selected(full=True) if not filenames: return sublime.status_message(u'Nothing to preview') fqn = filenames[0] if isdir(fqn) or fqn == PARENT_SYM: if not ST3: return sublime.status_message(u'No preview for directories') self.view.run_command('dired_preview_directory', {'fqn': fqn}) return if exists(fqn): if ST3: self.view.run_command('dired_file_properties', {'fqn': fqn}) window = self.view.window() dired_view = self.view self.focus_other_group(window) window.open_file(fqn, sublime.TRANSIENT) window.focus_view(dired_view) else: sublime.status_message(u'File does not exist (%s)' % (basename(fqn.rstrip(os.sep)) or fqn)) class DiredExpand(TextCommand, DiredBaseCommand): '''Open directory(s) inline, aka treeview''' def run(self, edit, toggle=False): ''' toggle if True, state of directory(s) will be toggled (i.e. expand/collapse) ''' self.index = self.get_all() filenames = self.get_marked(full=True) or self.get_selected(parent=False, full=True) if len(filenames) == 1 and filenames[0][~0] == os.sep: return self.expand_single_directory(edit, filenames[0], toggle) elif filenames: # working with several selections at once is very tricky, thus for reliability we should # recreate the entire tree, despite it is supposedly slower, but not really, because # one view.replace/insert() call is faster than multiple ones self.view.run_command('dired_refresh', {'to_expand': filenames, 'toggle': toggle}) return else: return sublime.status_message('Item cannot be expanded') def expand_single_directory(self, edit, filename, toggle): '''Expand one directory is save and fast, thus we do it here, but for many directories calling refresh command''' marked = self.get_marked() seled = self.get_selected() if toggle and self.try_to_fold(marked): return self.view.run_command('dired_fold', {'update': True, 'index': self.index}) self.index = self.get_all() # fold changed index, get a new one self.show_hidden = self.view.settings().get('dired_show_hidden_files', True) self.sel = self.view.get_regions('marked')[0] if marked else list(self.view.sel())[0] line = self.view.line(self.sel) # number of next line to make slicing work properly self.number_line = 1 + self.view.rowcol(line.a)[0] # line may have inline error msg after os.sep root = self.view.substr(line).split(os.sep)[0].replace(u'▸', u'▾', 1) + os.sep items, error = self.try_listing_directory(filename) if error: replacement = [u'%s\t<%s>' % (root, error)] elif items: replacement = [root] + self.prepare_filelist(items, '', filename, '\t') dired_count = self.view.settings().get('dired_count', 0) self.view.settings().set('dired_count', dired_count + len(items)) else: # expanding empty folder, so notify that it is empty replacement = [u'%s\t<empty>' % root] self.view.set_read_only(False) self.view.replace(edit, line, '\n'.join(replacement)) self.view.set_read_only(True) self.view.settings().set('dired_index', self.index) self.restore_marks(marked) self.restore_sels((seled, [self.sel])) self.view.run_command('dired_call_vcs', {'path': self.path}) emit_event(u'finish_refresh', (self.view.id(), [filename]), view=self.view) def try_to_fold(self, marked): line = self.view.line(self.view.get_regions('marked')[0] if marked else list(self.view.sel())[0]) content = self.view.substr(line).lstrip()[0] if content == u'▾': self.view.run_command('dired_fold') return True else: return False class DiredFold(TextCommand, DiredBaseCommand): u''' This command used to fold/erase/shrink (whatever you like to call it) content of some [sub]directory (within current directory, see self.path). There are two cases when this command would be fired: 1. User mean to collapse (key ←) 2. User mean to expand (key →) In first case we just erase region, however, we need to figure out which region to erase: (a) if cursor placed on directory item and next line(s) indented (representing content of the directory) — erase indented line(s); (b) next line is not indented, but the line of directory item is indented — erase directory item itself and all neighbours with the same indent; (c) cursor placed on file item which is indented — same as prev. (erase item and neighbours) In second case we need to decide if erasing needed or not: (a) if directory was expanded — do erase (as in 1.a), so then it’ll be filled again, basically it is like update/refresh; (b) directory was collapsed — do nothing. Very important, in case of actual modification of view, set valid dired_index setting see DiredRefreshCommand docs for details ''' def run(self, edit, update=None, index=None): ''' update True when user mean to expand, i.e. no folding for collapsed directory even if indented index list returned by self.get_all(), kinda cache during DiredExpand.expand_single_directory Call self.fold method on each line (multiple selections/marks), restore marks and selections ''' v = self.view self.update = update self.index = index or self.get_all() self.marked = None self.seled = (self.get_selected(), list(self.view.sel())) marks = self.view.get_regions('marked') virt_sels = [] if marks: for m in marks: if 'directory' in self.view.scope_name(m.a): virt_sels.append(Region(m.a, m.a)) self.marked = self.get_marked() sels = virt_sels or list(v.sel()) lines = [v.line(s.a) for s in reversed(sels)] for line in lines: self.fold(edit, line) self.restore_marks(self.marked) self.restore_sels(self.seled) def fold(self, edit, line): '''line is a Region, on which folding is supposed to happen (or not)''' line, indented_region = self.get_indented_region(line) if not indented_region: return # folding is not supposed to happen, so we exit self.apply_change_into_view(edit, line, indented_region) def get_indented_region(self, line): '''Return tuple: line Region which shall NOT be erased, can be equal to argument line or less if folding was called on indented file item or indented collapsed directory indented_region Region which shall be erased ''' v = self.view eol = line.b - 1 if 'error' in v.scope_name(eol): # remove inline error, e.g. <empty> indented_region = v.extract_scope(eol) return (line, indented_region) current_region = v.indented_region(line.b) next_region = v.indented_region(line.b + 2) is_dir = 'directory' in v.scope_name(line.a) next_empty = next_region.empty() this_empty = current_region.empty() line_in_next = next_region.contains(line) this_in_next = next_region.contains(current_region) def __should_exit(): collapsed_dir = self.update and (line_in_next or next_empty or this_in_next) item_in_root = (not is_dir or next_empty) and this_empty return collapsed_dir or item_in_root if __should_exit(): return (None, None) elif self.update or (is_dir and not next_empty and not line_in_next): indented_region = next_region elif not this_empty: indented_region = current_region line = v.line(indented_region.a - 2) else: return (None, None) return (line, indented_region) def apply_change_into_view(self, edit, line, indented_region): '''set count and index, track marks/selections, replace icon, erase indented_region''' v = self.view # do not set count & index on empty directory if not line.contains(indented_region): removed_count = len(v.lines(indented_region)) dired_count = v.settings().get('dired_count', 0) v.settings().set('dired_count', int(dired_count) - removed_count) if indented_region.b == v.size(): # MUST avoid new line at eof indented_region = Region(indented_region.a - 1, indented_region.b) start_line = 1 + v.rowcol(line.a)[0] end_line = start_line + removed_count self.index = self.index[:start_line] + self.index[end_line:] v.settings().set('dired_index', self.index) if self.marked or self.seled: path = self.path folded_name = self.get_parent(line, path) if self.marked: self.marked.append(folded_name) elif self.seled: self.seled[0].append(folded_name) name_point = self._get_name_point(line) icon_region = Region(name_point - 2, name_point - 1) v.set_read_only(False) v.replace(edit, icon_region, u'▸') v.erase(edit, indented_region) v.set_read_only(True) emit_event(u'fold', (self.view.id(), self.index[start_line - 1]), view=self.view) class DiredUpCommand(TextCommand, DiredBaseCommand): def run(self, edit): path = self.path parent = dirname(path.rstrip(os.sep)) if parent != os.sep and parent[1:] != ':\\': # need to avoid c:\\\\ parent += os.sep if parent == path and NT: parent = 'ThisPC' elif parent == path: return elif path == 'ThisPC\\': self.view.run_command('dired_refresh') return view_id = (self.view.id() if reuse_view() else None) goto = basename(path.rstrip(os.sep)) or path show(self.view.window(), parent, view_id, goto=goto) class DiredGotoCommand(TextCommand, DiredBaseCommand): """ Prompt for a new directory. """ def run(self, edit): prompt.start('Goto:', self.view.window(), self.path, self.goto) def goto(self, path): show(self.view.window(), path, view_id=self.view.id()) # MARKING ########################################################### class DiredMarkExtensionCommand(TextCommand, DiredBaseCommand): def run(self, edit): filergn = self.fileregion() if filergn.empty(): return current_item = self.view.substr(self.view.line(self.view.sel()[0].a)) if current_item.endswith(os.sep) or current_item == PARENT_SYM: ext = '' else: ext = current_item.split('.')[-1] pv = self.view.window().show_input_panel('Extension:', ext, self.on_done, None, None) pv.run_command("select_all") def on_done(self, ext): ext = ext.strip() if not ext: return if not ext.startswith('.'): ext = '.' + ext self._mark(mark=lambda oldmark, filename: filename.endswith(ext) or oldmark, regions=[self.fileregion()]) class DiredMarkCommand(TextCommand, DiredBaseCommand): """ Marks or unmarks files. The mark can be set to '*' to mark a file, ' ' to unmark a file, or 't' to toggle the mark. By default only selected files are marked, but if markall is True all files are marked/unmarked and the selection is ignored. If there is no selection and mark is '*', the cursor is moved to the next line so successive files can be marked by repeating the mark key binding (e.g. 'm'). """ def run(self, edit, mark=True, markall=False, forward=True): assert mark in (True, False, 'toggle') filergn = self.fileregion() if filergn.empty(): return if not mark and markall: self.view.erase_regions('marked') return # If markall is set, mark/unmark all files. Otherwise only those that are selected. regions = [filergn] if markall else list(self.view.sel()) if mark == 'toggle': mark = lambda oldmark, filename: not oldmark self._mark(mark=mark, regions=regions) # If there is no selection, move the cursor forward so the user can keep pressing 'm' # to mark successive files. if not markall and len(self.view.sel()) == 1 and self.view.sel()[0].empty(): self.move(forward) # OTHER ############################################################# class DiredToggleHiddenFilesCommand(TextCommand): def run(self, edit): show = self.view.settings().get('dired_show_hidden_files', True) self.view.settings().set('dired_show_hidden_files', not show) self.view.run_command('dired_refresh') # MOUSE INTERATIONS ################################################# def dired_mouse(view, args): s = view.settings() if s.get("dired_path") and not s.get("dired_rename_mode"): if 'directory' in view.scope_name(view.sel()[0].a): command = ("dired_expand", {"toggle": True}) else: command = ("dired_select", {"other_group": True}) view.run_command(*command) else: system_command = args["command"] if "command" in args else None if system_command: system_args = dict({"event": args["event"]}.items()) system_args.update(dict(args["args"].items())) view.run_command(system_command, system_args) if ST3: class DiredDoubleclickCommand(TextCommand, DiredBaseCommand): def run_(self, view, args): dired_mouse(self.view, args) else: class DiredDoubleclickCommand(TextCommand, DiredBaseCommand): def run_(self, args): dired_mouse(self.view, args)