Created
May 11, 2025 20:37
-
-
Save KarloSiric/2ce085d879eefc8bd2e5730599699a42 to your computer and use it in GitHub Desktop.
FileBrowser Modified Files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # coding: utf-8 | |
| '''Common stuff, used in other modules''' | |
| # import traceback | |
| import re | |
| import os | |
| import fnmatch | |
| import itertools | |
| import sublime | |
| from sublime import Region | |
| from os.path import isdir, join, basename | |
| try: # unavailable dependencies shall not break basic functionality | |
| import package_events | |
| except ImportError: | |
| package_events = None | |
| PLATFORM = sublime.platform() | |
| NT = PLATFORM == 'windows' | |
| LIN = PLATFORM == 'linux' | |
| OSX = PLATFORM == 'osx' | |
| if NT: | |
| import ctypes | |
| MARK_OPTIONS = sublime.DRAW_NO_OUTLINE | |
| RE_FILE = re.compile(r'^(\s*)([^\\//].*)$') | |
| PARENT_SYM = "⠤" | |
| # This is a modification to handle consistent icon display | |
| # Define the ICONS dictionary at the top level of your module | |
| # (outside of the prepare_filelist method) so it can be accessed by other functions | |
| ICONS = { | |
| # Folders - keep the original arrow for functionality | |
| 'folder': '▸', | |
| 'folder_open': '▾', | |
| # Folder icons | |
| 'folder_collapsed': '', | |
| 'folder_expanded': '', # Different icon for expanded folders | |
| # Files - keeping ≡ for functionality but adding icons | |
| 'default': '', | |
| 'py': '', | |
| 'js': '', | |
| 'html': '', | |
| 'css': '', | |
| 'json': '', | |
| 'md': '', | |
| 'cpp': '', | |
| 'c': '', | |
| 'h': '', | |
| 'java': '', | |
| 'txt': '', | |
| 'pdf': '', | |
| 'zip': '', | |
| 'git': '', | |
| 'png': '', | |
| 'jpg': '', | |
| 'svg': '', | |
| 'webp': '', | |
| 'gif': '', | |
| 'bmp': '', | |
| 'sql': '', | |
| 'sqlite': '', | |
| 'db': '', | |
| 'xls': '', | |
| 'csv': '', | |
| 'xlsx': '', | |
| 'docx': '', | |
| 'doc': '', | |
| 'ppt': '', | |
| 'pptx': '', | |
| 'key': '', | |
| 'pages': '', | |
| 'numbers': '', | |
| 'php': '', | |
| 'sh': '', | |
| 'bash': '', | |
| } | |
| # THiS is a working code so do not modify it | |
| # def get_file_icon(filename): | |
| # ext = filename.split('.')[-1].lower() if '.' in filename else '' | |
| # icon = ICONS.get(ext, ICONS['default']) | |
| # print(f"Getting icon for {filename}: extension={ext}, icon={icon}") | |
| # return icon | |
| def get_file_icon(filename): | |
| ext = filename.split('.')[-1].lower() if '.' in filename else '' | |
| icon = ICONS.get(ext, ICONS['default']) | |
| return icon | |
| def first(seq, pred): | |
| '''similar to built-in any() but return the object instead of boolean''' | |
| return next((item for item in seq if pred(item)), None) | |
| def sort_nicely(names): | |
| """ Sort the given list in the way that humans expect. | |
| Source: http://www.codinghorror.com/blog/2007/12/sorting-for-humans-natural-sort-order.html | |
| """ | |
| convert = lambda text: int(text) if text.isdigit() else text.lower() | |
| alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] | |
| names.sort(key=alphanum_key) | |
| # WORKING CODE DO NOT MODIFY!!!!! 21.2.2025 | |
| # def set_proper_scheme(view): | |
| # ''' | |
| # this is callback, it is not meant to be called directly | |
| # view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(view)) | |
| # set once, right after view is created | |
| # _note_, color_scheme must not be set directly, but in a setting file | |
| # ''' | |
| # # Since we cannot create file with syntax, there is moment when view has no settings, | |
| # # but it is activated, so some plugins (e.g. Color Highlighter) set wrong color scheme | |
| # if view.settings().get('dired_rename_mode', False): | |
| # dired_settings = sublime.load_settings('dired-rename-mode.sublime-settings') | |
| # else: | |
| # dired_settings = sublime.load_settings('dired.sublime-settings') | |
| # color_scheme = dired_settings.get('color_scheme') | |
| # if view.settings().get('color_scheme') == color_scheme: | |
| # return | |
| # if color_scheme: | |
| # view.settings().set('color_scheme', color_scheme) | |
| # else: | |
| # view.settings().erase('color_scheme') | |
| def set_proper_scheme(view): | |
| ''' | |
| this is callback, it is not meant to be called directly | |
| view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(view)) | |
| set once, right after view is created | |
| _note_, color_scheme must not be set directly, but in a setting file | |
| ''' | |
| # Since we cannot create file with syntax, there is moment when view has no settings, | |
| # but it is activated, so some plugins (e.g. Color Highlighter) set wrong color scheme | |
| if view.settings().get('dired_rename_mode', False): | |
| dired_settings = sublime.load_settings('dired-rename-mode.sublime-settings') | |
| else: | |
| dired_settings = sublime.load_settings('dired.sublime-settings') | |
| color_scheme = dired_settings.get('color_scheme') | |
| if view.settings().get('color_scheme') == color_scheme: | |
| return | |
| if color_scheme: | |
| view.settings().set('color_scheme', color_scheme) | |
| else: | |
| view.settings().erase('color_scheme') | |
| def calc_width(view): | |
| ''' | |
| return float width, which must be | |
| 0.0 < width < 1.0 (other values acceptable, but cause unfriendly layout) | |
| used in show.show() and "dired_select" command with other_group=True | |
| ''' | |
| width = view.settings().get('dired_width', 0.3) | |
| if isinstance(width, float): | |
| width -= width // 1 # must be less than 1 | |
| elif isinstance(width, int): # assume it is pixels | |
| wport = view.viewport_extent()[0] | |
| width = 1 - round((wport - width) / wport, 2) | |
| if width >= 1: | |
| width = 0.9 | |
| else: | |
| sublime.error_message( | |
| 'FileBrowser:\n\ndired_width set to ' | |
| 'unacceptable type "{0}", please change it.\n\n' | |
| 'Fallback to default 0.3 for now.' | |
| .format(type(width)) | |
| ) | |
| width = 0.3 | |
| return width or 0.1 # avoid 0.0 | |
| def get_group(groups, nag): | |
| ''' | |
| groups amount of groups in window | |
| nag number of active group | |
| return number of neighbour group | |
| ''' | |
| if groups <= 4 and nag < 2: | |
| group = 1 if nag == 0 else 0 | |
| elif groups == 4 and nag >= 2: | |
| group = 3 if nag == 2 else 2 | |
| else: | |
| group = nag - 1 | |
| return group | |
| def relative_path(rpath): | |
| '''rpath is either list or empty string (if list, we need only first item); | |
| return either empty string or rpath[0] (or its parent), e.g. | |
| foo/bar/ → foo/bar/ | |
| foo/bar → foo/ | |
| ''' | |
| if rpath: | |
| rpath = rpath[0] | |
| if rpath[~0] != os.sep: | |
| rpath = os.path.split(rpath)[0] + os.sep | |
| if rpath == os.sep: | |
| rpath = '' | |
| return rpath | |
| def hijack_window(): | |
| '''Execute on loading plugin or on new window open; | |
| allow to open FileBrowser automatically | |
| ''' | |
| settings = sublime.load_settings('dired.sublime-settings') | |
| command = settings.get("dired_hijack_new_window") | |
| if command: | |
| if command == "jump_list": | |
| sublime.set_timeout(lambda: sublime.windows()[-1].run_command("dired_jump_list"), 1) | |
| else: | |
| sublime.set_timeout( | |
| lambda: sublime.windows()[-1].run_command("dired", {"immediate": True}), 1) | |
| # def emit_event(event_type, payload, view=None, plugin='FileBrowser'): | |
| # '''Notify our filesystem observer about changes in our views | |
| # event_type | |
| # Unicode object tells what happen, i.e set_paths, remove_path, view_closed, etc. | |
| # payload | |
| # some info for observer, e.g. id of view, list of paths, tuple of those things | |
| # must be immutable object | |
| # view | |
| # sublime.View object or None | |
| # plugin | |
| # Unicode object tells who is sender, in order to address event to certain listener | |
| # we have two different listeners for FileBrowser and FileBrowserWFS | |
| # FileBrowser | |
| # notifies observer about user actions to adjust scheduling paths | |
| # FileBrowserWFS | |
| # notifies FileSystemEventHandler about scheduled paths in order to schedule refresh when | |
| # sth is changed on file system | |
| # ''' | |
| # if package_events is None: | |
| # return | |
| # if view and not view.settings().get('dired_autorefresh', True): | |
| # package_events.notify(plugin, 'stop_watch', view.id()) | |
| # return | |
| # package_events.notify(plugin, event_type, payload) | |
| # -------------------- TESTING ONLY -------------------- | |
| def emit_event(event_type, payload, view=None, plugin='FileBrowser'): | |
| '''Notify our filesystem observer about changes in our views''' | |
| # Block refreshes caused by system files or temporary directories | |
| IGNORED_PATHS = ["/Library/Preferences", "/private/var/folders", "/tmp", "/var/log"] | |
| if any(ignored in str(payload) for ignored in IGNORED_PATHS): | |
| print(f"🚫 Ignoring event: {event_type}, payload: {payload}") # Debugging | |
| return # Don't trigger refresh for these paths | |
| if package_events is None: | |
| return | |
| if view and not view.settings().get('dired_autorefresh', True): | |
| package_events.notify(plugin, 'stop_watch', view.id()) | |
| return | |
| package_events.notify(plugin, event_type, payload) | |
| class DiredBaseCommand: | |
| """ | |
| Convenience functions for dired TextCommands | |
| """ | |
| @property | |
| def path(self): | |
| return self.view.settings().get('dired_path') | |
| # -------- ORIGINAL CODE -------- | |
| # def get_path(self): | |
| # path = self.path | |
| # if path == 'ThisPC\\': | |
| # path = '' | |
| # return path | |
| def get_path(self): | |
| path = self.path | |
| if path == 'ThisPC\\': | |
| path = '' | |
| return path | |
| def filecount(self): | |
| """ | |
| Returns the number of files and directories in the view. | |
| """ | |
| return self.view.settings().get('dired_count', 0) | |
| def move_to_extreme(self, extreme="bof"): | |
| """ | |
| Moves the cursor to the beginning or end of file list. Clears all sections. | |
| """ | |
| files = self.fileregion(with_parent_link=True) | |
| self.view.sel().clear() | |
| if extreme == "bof": | |
| ext_region = Region(files.a, files.a) | |
| else: | |
| name_point = self.view.extract_scope(self.view.line(files.b).a + 2).a | |
| ext_region = Region(name_point, name_point) | |
| self.view.sel().add(ext_region) | |
| self.view.show_at_center(ext_region) | |
| def move(self, forward=None): | |
| """ | |
| Moves the cursor one line forward or backwards. Clears all sections. | |
| """ | |
| assert forward in (True, False), 'forward must be set to True or False' | |
| files = self.fileregion(with_parent_link=True) | |
| if not files: | |
| return | |
| new_sels = [] | |
| for s in list(self.view.sel()): | |
| new_sels.append(self._get_name_point(self.next_line(forward, s.a, files))) | |
| self.view.sel().clear() | |
| for n in new_sels: | |
| self.view.sel().add(Region(n, n)) | |
| name_point = new_sels[~0] if forward else new_sels[0] | |
| surroundings = True if self.view.rowcol(name_point)[0] < 3 else False | |
| self.view.show(name_point, surroundings) | |
| def next_line(self, forward, pt, filergn): | |
| '''Return Region of line for pt within filergn''' | |
| if filergn.contains(pt): | |
| # Try moving by one line. | |
| line = self.view.line(pt) | |
| pt = forward and (line.b + 1) or (line.a - 1) | |
| if not filergn.contains(pt): | |
| # Not (or no longer) in the list of files, so move to the closest edge. | |
| pt = (pt > filergn.b) and filergn.b or filergn.a | |
| return self.view.line(pt) | |
| def _get_name_point(self, line): | |
| '''Return point at which filename starts (i.e. after icon & whitespace)''' | |
| scope = self.view.scope_name(line.a) | |
| if 'indent' in scope: | |
| name_point = self.view.extract_scope(line.a).b | |
| else: | |
| name_point = line.a | |
| return name_point + (2 if 'parent_dir' not in scope else 0) | |
| def show_parent(self): | |
| return self.view.settings().get('dired_show_parent', False) | |
| def fileregion(self, with_parent_link=False): | |
| """ | |
| Returns a region containing the lines containing filenames. | |
| If there are no filenames None is returned. | |
| """ | |
| if with_parent_link: | |
| all_items = sorted(self.view.find_by_selector('dired.item')) | |
| else: | |
| all_items = sorted(self.view.find_by_selector('dired.item.directory') + | |
| self.view.find_by_selector('dired.item.file')) | |
| if not all_items: | |
| return None | |
| return Region(all_items[0].a, all_items[~0].b) | |
| def get_parent(self, line, path): | |
| ''' | |
| Returns relative path for line | |
| • line is a region | |
| • path is self.path | |
| • self.index is list stored in view settings as 'dired_index' | |
| ''' | |
| return self.get_fullpath_for(line).replace(path, '', 1) | |
| def get_fullpath_for(self, line): | |
| return self.index[self.view.rowcol(line.a)[0]] | |
| def get_all(self): | |
| """ | |
| Returns a list of all filenames in the view. | |
| dired_index is always supposed to represent current state of view, | |
| each item matches corresponding line, thus list will never be empty unless sth went wrong; | |
| if header is enabled then first two elements are empty strings | |
| """ | |
| index = self.view.settings().get('dired_index', []) | |
| if not index: | |
| return sublime.error_message('FileBrowser:\n\n"dired_index" is empty,\n' | |
| 'that shouldn’t happen ever, there is some bug.') | |
| return index | |
| def get_all_relative(self, path): | |
| return [f.replace(path, '', 1) for f in self.get_all()] | |
| def get_selected(self, parent=True, full=False): | |
| """ | |
| parent | |
| if False, returned list does not contain PARENT_SYM even if it is in view | |
| full | |
| if True, items in returned list are full paths, else relative | |
| Returns a list of selected filenames. | |
| self.index should be assigned before call it | |
| """ | |
| fileregion = self.fileregion(with_parent_link=parent) | |
| if not fileregion: | |
| return None | |
| path = self.get_path() | |
| names = [] | |
| for line in self._get_lines([s for s in self.view.sel()], fileregion): | |
| text = self.get_fullpath_for(line) if full else self.get_parent(line, path) | |
| if text and text not in names: | |
| names.append(text) | |
| return names | |
| def get_marked(self, full=False): | |
| '''self.index should be assigned before call it''' | |
| if not self.filecount(): | |
| return [] | |
| path = self.get_path() | |
| names = [] | |
| for line in self.view.get_regions('marked'): | |
| text = self.get_fullpath_for(line) if full else self.get_parent(line, path) | |
| if text and text not in names: | |
| names.append(text) | |
| return names | |
| def _mark(self, mark, regions): | |
| """ | |
| Marks the requested files. | |
| mark | |
| True, False, or a function with signature `func(oldmark, filename)`. | |
| The function should return True or False. | |
| regions | |
| List of region(s). Only files within the region will be modified. | |
| """ | |
| filergn = self.fileregion() | |
| if not filergn: | |
| return | |
| self.index = self.get_all() | |
| # We can't update regions for a key, only replace, so we need to record the existing marks. | |
| marked = { | |
| self.get_fullpath_for(r): r | |
| for r in self.view.get_regions('marked') if not r.empty() | |
| } | |
| for line in self._get_lines(regions, filergn): | |
| filename = self.get_fullpath_for(line) | |
| if mark not in (True, False): | |
| newmark = mark(filename in marked, filename) | |
| assert newmark in (True, False), 'Invalid mark: {0}'.format(newmark) | |
| else: | |
| newmark = mark | |
| if newmark: | |
| name_point = self._get_name_point(line) | |
| marked[filename] = Region(name_point, line.b) | |
| else: | |
| marked.pop(filename, None) | |
| if marked: | |
| r = sorted(list(marked.values()), key=lambda region: region.a) | |
| self.view.add_regions('marked', r, 'dired.marked', '', MARK_OPTIONS) | |
| else: | |
| self.view.erase_regions('marked') | |
| def _get_lines(self, regions, within): | |
| ''' | |
| regions is a list of non-overlapping region(s), each may have many lines | |
| within is a region which is supposed to contain each line | |
| ''' | |
| return ( | |
| line | |
| for line in itertools.chain(*(self.view.lines(r) for r in regions)) | |
| if within.contains(line) | |
| ) | |
| # -------- ORGINAL CODE -------- | |
| # def set_ui_in_rename_mode(self, edit): | |
| # header = self.view.settings().get('dired_header', False) | |
| # if header: | |
| # regions = self.view.find_by_selector( | |
| # 'text.dired header.dired punctuation.definition.separator.dired') | |
| # else: | |
| # regions = self.view.find_by_selector('text.dired dired.item.parent_dir') | |
| # if not regions: | |
| # return | |
| # region = regions[0] | |
| # start = region.begin() | |
| # self.view.erase(edit, region) | |
| # if header: | |
| # new_text = "——[RENAME MODE]——" + ("—" * (region.size() - 17)) | |
| # else: | |
| # new_text = "⠤ [RENAME MODE]" | |
| # self.view.insert(edit, start, new_text) | |
| def set_ui_in_rename_mode(self, edit): | |
| header = self.view.settings().get('dired_header', False) | |
| if header: | |
| regions = self.view.find_by_selector( | |
| 'text.dired header.dired punctuation.definition.separator.dired') | |
| else: | |
| regions = self.view.find_by_selector('text.dired dired.item.parent_dir') | |
| if not regions: | |
| return | |
| region = regions[0] | |
| start = region.begin() | |
| self.view.erase(edit, region) | |
| if header: | |
| new_text = "——[RENAME MODE]——" + ("—" * (region.size() - 17)) | |
| else: | |
| new_text = "⠤ [RENAME MODE]" | |
| self.view.insert(edit, start, new_text) | |
| # Store the fact that we're in rename mode | |
| self.view.settings().set('dired_rename_mode', True) | |
| def set_status(self): | |
| '''Update status-bar; | |
| self.show_hidden must be assigned before call it''' | |
| # if view isnot focused, view.window() may be None | |
| window = self.view.window() or sublime.active_window() | |
| path_in_project = any(folder == self.path[:-1] for folder in window.folders()) | |
| settings = self.view.settings() | |
| copied_items = settings.get('dired_to_copy', []) | |
| cut_items = settings.get('dired_to_move', []) | |
| status = " 𝌆 [?: Help] {0}Hidden: {1}{2}{3}".format( | |
| 'Project root, ' if path_in_project else '', | |
| 'On' if self.show_hidden else 'Off', | |
| ', copied(%d)' % len(copied_items) if copied_items else '', | |
| ', cut(%d)' % len(cut_items) if cut_items else '' | |
| ) | |
| self.view.set_status("__FileBrowser__", status) | |
| # ---------- WORKING ICONS ------------------ | |
| # ---------- NEEDS TO BE FIXED STILL ---------- | |
| # def prepare_filelist(self, names, path, goto, indent): | |
| # Add the icons dictionary at the start of the method | |
| # adding this new line so that we can trace what called this method | |
| # and debug properly. | |
| # print("prepare_filelist called by:", traceback.format_stack()[-2]) | |
| # ICONS = { | |
| # # Folders - keep the original arrow for functionality | |
| # 'folder': '▸', # Keep this arrow for functionality | |
| # 'folder_open': '▾', # Keep for functionality | |
| # # Files - keeping ≡ for functionality but adding icons | |
| # 'default': '', | |
| # 'py': '', | |
| # 'js': '', | |
| # 'html': '', | |
| # 'css': '', | |
| # 'json': '', | |
| # 'md': '', | |
| # 'cpp': '', | |
| # 'c': '', | |
| # 'h': '', | |
| # 'java': '', | |
| # 'txt': '', | |
| # 'pdf': '', | |
| # 'zip': '', | |
| # 'git': '', | |
| # } | |
| # def get_file_icon(filename): | |
| # ext = filename.split('.')[-1].lower() if '.' in filename else '' | |
| # return ICONS.get(ext, ICONS['default']) | |
| # items = [] | |
| # tab = self.view.settings().get('tab_size') | |
| # line = self.view.line(self.sel.a if self.sel is not None else self.view.sel()[0].a) | |
| # content = self.view.substr(line).replace('\t', ' ' * tab) | |
| # ind = re.compile(r'^(\s*)').match(content).group(1) | |
| # level = indent * int((len(ind) / tab) + 1) if ind else indent | |
| # files = [] | |
| # index_dirs = [] | |
| # index_files = [] | |
| # for name in names: | |
| # full_name = join(path, goto, name) | |
| # if isdir(full_name): | |
| # index_dirs.append('%s%s' % (full_name, os.sep)) | |
| # # Keep the original ▸ for folders but add icon | |
| # items.append(''.join([level, "▸ ", name, os.sep])) | |
| # else: | |
| # index_files.append(full_name) | |
| # icon = get_file_icon(name) | |
| # # Keep the original ≡ for files but add icon | |
| # files.append(''.join([level, "≡ " + icon + " ", name])) | |
| # index = index_dirs + index_files | |
| # self.index = self.index[:self.number_line] + index + self.index[self.number_line:] | |
| # items += files | |
| # return items | |
| # ---------- WORKING ICONS ------------------ | |
| # ----------- LATEST WORKING 21.2.2025 ---------------- | |
| # def prepare_filelist(self, names, path, goto, indent): | |
| # # Add debug output | |
| # print(f"prepare_filelist called with {len(names)} items") | |
| # sample_names = names[:5] if names else [] | |
| # print(f"Sample names: {sample_names}") | |
| # items = [] | |
| # tab = self.view.settings().get('tab_size') | |
| # line = self.view.line(self.sel.a if self.sel is not None else self.view.sel()[0].a) | |
| # content = self.view.substr(line).replace('\t', ' ' * tab) | |
| # ind = re.compile(r'^(\s*)').match(content).group(1) | |
| # level = indent * int((len(ind) / tab) + 1) if ind else indent | |
| # files = [] | |
| # index_dirs = [] | |
| # index_files = [] | |
| # for name in names: | |
| # full_name = join(path, goto, name) | |
| # if isdir(full_name): | |
| # index_dirs.append('%s%s' % (full_name, os.sep)) | |
| # items.append(''.join([level, "▸ ", name, os.sep])) | |
| # else: | |
| # index_files.append(full_name) | |
| # icon = get_file_icon(name) | |
| # # Make sure file icons are displayed correctly | |
| # files.append(''.join([level, "≡ ", icon, " ", name])) | |
| # index = index_dirs + index_files | |
| # self.index = self.index[:self.number_line] + index + self.index[self.number_line:] | |
| # items += files | |
| # return items | |
| def prepare_filelist(self, names, path, goto, indent): | |
| items = [] | |
| tab = self.view.settings().get('tab_size') | |
| line = self.view.line(self.sel.a if self.sel is not None else self.view.sel()[0].a) | |
| content = self.view.substr(line).replace('\t', ' ' * tab) | |
| ind = re.compile(r'^(\s*)').match(content).group(1) | |
| level = indent * int((len(ind) / tab) + 1) if ind else indent | |
| files = [] | |
| index_dirs = [] | |
| index_files = [] | |
| for name in names: | |
| full_name = join(path, goto, name) | |
| if isdir(full_name): | |
| index_dirs.append('%s%s' % (full_name, os.sep)) | |
| # Use the collapsed folder icon | |
| items.append(''.join([level, "▸ ", ICONS['folder_collapsed'], " ", name, os.sep])) | |
| else: | |
| index_files.append(full_name) | |
| icon = get_file_icon(name) | |
| files.append(''.join([level, "≡ ", icon, " ", name])) | |
| index = index_dirs + index_files | |
| self.index = self.index[:self.number_line] + index + self.index[self.number_line:] | |
| items += files | |
| return items | |
| def is_hidden(self, filename, path, goto=''): | |
| if not (path or goto): # special case for ThisPC | |
| return False | |
| tests = self.view.settings().get('dired_hidden_files_patterns', ['.*']) | |
| if isinstance(tests, str): | |
| tests = [tests] | |
| if any(fnmatch.fnmatch(filename, pattern) for pattern in tests): | |
| return True | |
| if sublime.platform() != 'windows': | |
| return False | |
| # check for attribute on windows: | |
| try: | |
| attrs = ctypes.windll.kernel32.GetFileAttributesW(join(path, goto, filename)) | |
| assert attrs != -1 | |
| result = bool(attrs & 2) | |
| except (AttributeError, AssertionError): | |
| result = False | |
| return result | |
| def try_listing_directory(self, path): | |
| '''Return tuple of two element | |
| items sorted list of filenames in path, or empty list | |
| error exception message, or empty string | |
| ''' | |
| items, error = [], '' | |
| try: | |
| if not self.show_hidden: | |
| items = [name for name in os.listdir(path) if not self.is_hidden(name, path)] | |
| else: | |
| items = os.listdir(path) | |
| except OSError as e: | |
| error = str(e) | |
| if NT: | |
| error = ( | |
| error | |
| .split(':')[0] | |
| .replace('[Error 5] ', 'Access denied') | |
| .replace('[Error 3] ', 'Not exists, press r to refresh') | |
| ) | |
| else: | |
| sort_nicely(items) | |
| finally: | |
| return items, error | |
| def try_listing_only_dirs(self, path): | |
| '''Same as self.try_listing_directory, but items contains only directories. | |
| Used for prompt completion''' | |
| items, error = self.try_listing_directory(path) | |
| if items: | |
| items = [n for n in items if isdir(join(path, n))] | |
| return (items, error) | |
| def restore_marks(self, marked=None): | |
| if marked: | |
| # Even if we have the same filenames, they may have moved so we have to manually | |
| # find them again. | |
| path = self.get_path() | |
| regions = [] | |
| for mark in marked: | |
| matches = self._find_in_view(mark) | |
| for region in matches: | |
| filename = self.get_parent(region, path) | |
| if filename == mark: | |
| regions.append(region) | |
| # if it is found, no need to check other matches, so break | |
| break | |
| self._mark(mark=True, regions=regions) | |
| else: | |
| self.view.erase_regions('marked') | |
| def restore_sels(self, sels=None): | |
| ''' | |
| sels is tuple of two elements: | |
| 0 list of filenames | |
| relative paths to search in the view | |
| 1 list of Regions | |
| copy of view.sel(), used for fallback if filenames are not found | |
| in view (e.g. user deleted selected file) | |
| ''' | |
| if sels: | |
| seled_fnames, seled_regions = sels | |
| path = self.get_path() | |
| regions = [] | |
| for selection in seled_fnames: | |
| matches = self._find_in_view(selection) | |
| for region in matches: | |
| filename = self.get_parent(region, path) | |
| if filename == selection: | |
| name_point = self._get_name_point(region) | |
| regions.append(Region(name_point, name_point)) | |
| break | |
| if regions: | |
| return self._add_sels(regions) | |
| else: | |
| # e.g. when user remove file(s), we just restore sel RegionSet | |
| # despite positions may be wrong sometimes | |
| return self._add_sels(seled_regions) | |
| # fallback: | |
| return self._add_sels() | |
| # ------------- ORIGINAL CODE ----------------- | |
| # def _find_in_view(self, item): | |
| # '''item is Unicode''' | |
| # fname = re.escape(basename(os.path.abspath(item)) or item.rstrip(os.sep)) | |
| # if item[~0] == os.sep: | |
| # pattern = r'^\s*[▸▾] ' | |
| # sep = re.escape(os.sep) | |
| # else: | |
| # pattern = r'^\s*≡ ' | |
| # sep = '' | |
| # return self.view.find_all('%s%s%s' % (pattern, fname, sep)) | |
| # Fix the _find_in_view method to work with icons | |
| def _find_in_view(self, item): | |
| '''item is Unicode''' | |
| fname = re.escape(basename(os.path.abspath(item)) or item.rstrip(os.sep)) | |
| if item[~0] == os.sep: | |
| # For directories - use a more flexible pattern | |
| pattern = r'^\s*[▸▾].*?' | |
| sep = re.escape(os.sep) | |
| else: | |
| # For files - use a more flexible pattern | |
| pattern = r'^\s*≡.*?' | |
| sep = '' | |
| return self.view.find_all('%s%s%s' % (pattern, fname, sep)) | |
| def _add_sels(self, sels=None): | |
| self.view.sel().clear() | |
| if sels: | |
| eof = self.view.size() | |
| for s in sels: | |
| if s.begin() <= eof: | |
| self.view.sel().add(s) | |
| if not sels or not list(self.view.sel()): # all sels more than eof | |
| fbs = self.view.find_by_selector | |
| item = ( | |
| fbs('text.dired dired.item.parent_dir ') | |
| or fbs('text.dired dired.item.directory string.name.directory.dired ') | |
| or fbs('text.dired dired.item.file string.name.file.dired ') | |
| ) | |
| s = Region(item[0].a, item[0].a) if item else Region(0, 0) | |
| self.view.sel().add(s) | |
| self.view.show_at_center(s) | |
| def display_path(self, folder): | |
| display = folder | |
| home = os.path.expanduser("~") | |
| if folder.startswith(home): | |
| display = folder.replace(home, "~", 1) | |
| return display |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # coding: utf-8 | |
| '''Main module; launch and navigation related stuff''' | |
| from collections import defaultdict | |
| import os | |
| from os.path import basename, dirname, isdir, exists, join | |
| import sys | |
| from textwrap import dedent | |
| import sublime | |
| from sublime import Region | |
| from sublime_plugin import EventListener, WindowCommand, TextCommand | |
| from .common import ( | |
| DiredBaseCommand, set_proper_scheme, calc_width, get_group, hijack_window, emit_event, | |
| NT, PARENT_SYM, get_file_icon, ICONS) | |
| from . 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() | |
| 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 can't restore sels | |
| v.run_command("dired_refresh", {"reset_sels": True}) | |
| dfsobserver = 'FileBrowser.0_dired_fs_observer' | |
| if dfsobserver not in sys.modules or sys.modules[dfsobserver].Observer is None: | |
| print(dedent(''' | |
| FileBrowser: watchdog module is not importable, hence auto-refresh will not work. | |
| You can still manually refresh a dired view with `[r]`. | |
| • If you installed via Package Control, make sure to restart Sublime Text often enough | |
| to give it a chance to install the required dependencies. | |
| • If you installed manually, then refer the Readme on how to install dependencies as well. | |
| ''')) # noqa: E501 | |
| settings = sublime.load_settings('dired.sublime-settings') | |
| settings.add_on_change( | |
| 'dired_autorefresh', | |
| lambda: emit_event('toggle_watch_all', settings.get('dired_autorefresh', None)) | |
| ) | |
| def plugin_unloaded(): | |
| sublime.load_settings('dired.sublime-settings').clear_on_change('dired_autorefresh') | |
| class DiredCommand(WindowCommand, DiredBaseCommand): | |
| """ | |
| Open a dired view. This is the main entrypoint/constructor. | |
| If `immediate` is set: open a dired view immediately. Usually highlight the | |
| file of the current view, fallback to an open folder or the users home | |
| directory as a last resort. If `project` is also set: set the root | |
| directory to the best matching open folder. Otherwise the root will be | |
| the directory of the view's file. (In other words, you get either a flat, | |
| if `project: false`, or nested listing, if it is `true`.) | |
| If `immediate` is *not* set: open an input box to let the user type in the | |
| directory they want. The input box implements a completion helper using | |
| `<tab>` to make this easier. The input box is typically filled with the | |
| name of the directory of the active view's file, or a fallback. If you | |
| set `project`, it is instead pre-filled with the folder attached to the | |
| window. In case there are multiple folders open, the user gets prompted | |
| by a quick panel to choose a folder from. In case there are *no* folders, | |
| show some fallbacks. | |
| """ | |
| def run(self, immediate=False, project=False, single_pane=False, other_group=False): | |
| if immediate: | |
| fpath = self._get_current_fpath() | |
| if not fpath: | |
| path, goto = self._fallback_path(), '' | |
| elif project: | |
| path = self._best_root_for_fpath(fpath) | |
| if path: | |
| goto = os.path.relpath(fpath, path) | |
| else: | |
| path, goto = os.path.split(fpath) | |
| else: | |
| path, goto = os.path.split(fpath) | |
| show(self.window, path, goto=goto, single_pane=single_pane, other_group=other_group) | |
| return | |
| if project: | |
| folders = self.window.folders() | |
| if len(folders) == 1: | |
| path = folders[0] | |
| else: | |
| self._show_folders_panel(folders, single_pane, other_group) | |
| return | |
| else: | |
| fpath = self._get_current_fpath() | |
| path = os.path.dirname(fpath) if fpath else self._fallback_path() | |
| prompt.start('Directory:', self.window, path, self._show, single_pane, other_group) | |
| def _get_current_fpath(self): | |
| view = self.window.active_view() | |
| return view.file_name() if view else None | |
| def _show_folders_panel(self, folders, single_pane, other_group): | |
| if not folders: | |
| fpath = self._get_current_fpath() | |
| if fpath: | |
| folders += [os.path.dirname(fpath)] | |
| folders += [os.path.expanduser('~')] | |
| 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] = '%s%s%s' % (name, offset, self.display_path(f)) | |
| self.window.show_quick_panel( | |
| names, | |
| lambda i: self._show_folder(i, folders, single_pane, other_group), | |
| sublime.MONOSPACE_FONT | |
| ) | |
| def _fallback_path(self): | |
| folders = self.window.folders() | |
| return folders[0] if folders else os.path.expanduser('~') | |
| def _best_root_for_fpath(self, fpath): | |
| folders = self.window.folders() | |
| for f in folders: | |
| # e.g. ['/a', '/aa'], to open '/aa/f' we need '/aa/' | |
| if fpath.startswith(''.join([f, os.sep])): | |
| return f | |
| def _show_folder(self, index, folders, single_pane, other_group): | |
| if index != -1: | |
| path = folders[index] | |
| show(self.window, path, single_pane=single_pane, other_group=other_group) | |
| def _show(self, path, single_pane, other_group): | |
| show(self.window, path, single_pane=single_pane, other_group=other_group) | |
| 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 directories 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( | |
| ( | |
| 'FileBrowser:\n\n' | |
| 'Directory does not exist:\n\n' | |
| '\t{0}\n\nTry to go up?'.format(path) | |
| ), | |
| 'Go' | |
| ): | |
| self.view.run_command('dired_up') | |
| return | |
| self.expanded = expanded = self.view.find_all(r'^\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( | |
| 'set_paths', | |
| (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, | |
| '\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}) | |
| # --------------------------- ORIGINAL CODE DO NOT MODIFY, BACKUP 22.2.2025!!! ------------------------------ | |
| # 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 = ['%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) | |
| # # Use the expanded folder icon | |
| # tree.append('%s▾ %s %s%s' % (indent[:-1], ICONS['folder_expanded'], bname.rstrip(os.sep), os.sep)) | |
| # self.index.append('%s' % path) | |
| # items, error = self.try_listing_directory(path) | |
| # if error: | |
| # tree[~0] += '\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 = '%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) | |
| # # Use the collapsed folder icon | |
| # tree.append('%s▸ %s %s%s' % (indent, ICONS['folder_collapsed'], f.rstrip(os.sep), os.sep)) | |
| # else: | |
| # index_files.append(new_path) | |
| # icon = get_file_icon(f) | |
| # files.append('%s≡ %s %s' % (indent, icon, f)) | |
| # self.index += index_files | |
| # tree += files | |
| # return tree | |
| 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 = ['%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) | |
| # Use the expanded folder icon | |
| tree.append('%s▾ %s %s%s' % (indent[:-1], ICONS['folder_expanded'], bname.rstrip(os.sep), os.sep)) | |
| self.index.append('%s' % path) | |
| items, error = self.try_listing_directory(path) | |
| if error: | |
| tree[~0] += '\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 = '%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) | |
| # Use the collapsed folder icon | |
| tree.append('%s▸ %s %s%s' % (indent, ICONS['folder_collapsed'], f.rstrip(os.sep), os.sep)) | |
| else: | |
| index_files.append(new_path) | |
| icon = get_file_icon(f) | |
| files.append('%s≡ %s %s' % (indent, icon, 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 = "{0} → {1}".format(name, path) if name else path or self.path | |
| text = [caption, len(caption) * '—'] if header else [] | |
| icon = self.view.name()[:2] | |
| if not path: | |
| title = '%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 = '%s%s (%s)' % (icon, name or basename(norm_path), norm_path) | |
| else: | |
| title = '%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.replace(edit, Region(0, self.view.size()), '\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) | |
| else: | |
| groups = window.num_groups() | |
| if groups > 1: | |
| dired_group = window.active_group() | |
| group = get_group(groups, dired_group) | |
| for other_view in window.views_in_group(group): | |
| if other_view.settings().get("dired_preview_view"): | |
| other_view.close() | |
| window.focus_view(dired_view) | |
| 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, sublime.FORCE_GROUP, group=-1) | |
| else: | |
| sublime.status_message( | |
| 'File does not exist ({0})'.format((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('Nothing to preview') | |
| fqn = filenames[0] | |
| if isdir(fqn) or fqn == PARENT_SYM: | |
| self.view.run_command('dired_preview_directory', {'fqn': fqn}) | |
| return | |
| if exists(fqn): | |
| window = self.view.window() | |
| dired_view = self.view | |
| will_create_preview_group = window.num_groups() == 1 | |
| group = self._other_group(window, window.active_group()) | |
| other_view = window.active_view_in_group(group) | |
| if ( | |
| other_view | |
| and other_view.file_name() == fqn | |
| and other_view.settings().get("dired_preview_view") | |
| ): | |
| # We need to focus even when we close, otherwise Sublime | |
| # magically opens an empty/new view. `set_timeout` for the | |
| # same reason. TODO: Investigate in safe-mode. | |
| window.focus_view(other_view) | |
| other_view.close() | |
| sublime.set_timeout(lambda: window.focus_view(dired_view)) | |
| else: | |
| open_views = window.views_in_group(group) | |
| close_empty_preview_group = ( | |
| will_create_preview_group | |
| or ( | |
| open_views and all( | |
| other_view.settings().get("dired_preview_view") | |
| for other_view in open_views | |
| ) | |
| ) | |
| ) | |
| other_view = window.open_file(fqn, sublime.FORCE_GROUP, group=group) | |
| other_view.settings().set("dired_preview_view", True) | |
| other_view.settings().set( | |
| "dired_close_empty_preview_pane", close_empty_preview_group) | |
| for v in open_views: | |
| if v != other_view and v.settings().get("dired_preview_view"): | |
| v.close() | |
| when_loaded(other_view, lambda: window.focus_view(dired_view)) | |
| else: | |
| sublime.status_message( | |
| 'File does not exist ({0})'.format(basename(fqn.rstrip(os.sep)) or fqn)) | |
| views_yet_to_get_loaded = defaultdict(list) | |
| preview_to_get_closed = {} | |
| def when_loaded(view, handler): | |
| if view.is_loading(): | |
| views_yet_to_get_loaded[view.id()].append(handler) | |
| else: | |
| handler() | |
| class PreviewViewHandler(EventListener): | |
| def on_load(self, view): | |
| callbacks = views_yet_to_get_loaded.pop(view.id(), []) | |
| for fn in callbacks: | |
| sublime.set_timeout(fn) | |
| def on_modified_async(self, view): | |
| if view.settings().get("dired_preview_view"): | |
| view.settings().erase("dired_preview_view") | |
| def on_pre_close(self, view): | |
| window = view.window() | |
| if ( | |
| window | |
| and view.settings().get("dired_close_empty_preview_pane") | |
| ): | |
| group, _ = window.get_view_index(view) | |
| preview_to_get_closed[view.id()] = (window, group) | |
| def on_close(self, view): | |
| window, group = preview_to_get_closed.pop(view.id(), (None, None)) | |
| if (window, group) == (None, None): | |
| return | |
| if window.num_groups() == 2 and len(window.views_in_group(group)) == 0: | |
| window.set_layout({"cols": [0.0, 1.0], "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1]]}) | |
| 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() | |
| items = self.get_marked(full=True) or self.get_selected(parent=False, full=True) | |
| paths = [path for path in items if path.endswith(os.sep)] | |
| if len(paths) == 1: | |
| return self.expand_single_directory(edit, paths[0], toggle) | |
| elif paths: | |
| # 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': paths, 'toggle': toggle}) | |
| return | |
| else: | |
| return sublime.status_message('Item cannot be expanded') | |
| # def expand_single_directory(self, edit, path, 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() | |
| # self.sel = self.view.get_regions('marked')[0] if marked else list(self.view.sel())[0] | |
| # line = self.view.line(self.sel) | |
| # line_content = self.view.substr(line) | |
| # if line_content.lstrip().startswith('▾'): | |
| # if toggle: | |
| # self.view.run_command('dired_fold') | |
| # return | |
| # # 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 | |
| # # Replace both the arrow and the folder icon | |
| # collapsed_icon = ICONS['folder_collapsed'] | |
| # expanded_icon = ICONS['folder_expanded'] | |
| # # Replace arrow and icon | |
| # if collapsed_icon in line_content: | |
| # parts = line_content.split(collapsed_icon) | |
| # if len(parts) > 1: | |
| # prefix = parts[0].replace('▸', '▾') | |
| # rest = parts[1] | |
| # root = prefix + expanded_icon + rest | |
| # else: | |
| # root = line_content.split(os.sep)[0].replace('▸', '▾', 1) + os.sep | |
| # else: | |
| # root = line_content.split(os.sep)[0].replace('▸', '▾', 1) + os.sep | |
| # self.show_hidden = self.view.settings().get('dired_show_hidden_files', True) | |
| # items, error = self.try_listing_directory(path) | |
| # if error: | |
| # replacement = ['%s\t<%s>' % (root, error)] | |
| # elif items: | |
| # replacement = [root] + self.prepare_filelist(items, '', path, '\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 = ['%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_draw_vcs_marker") | |
| # emit_event('add_paths', (self.view.id(), [path]), view=self.view) | |
| def expand_single_directory(self, edit, path, 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() | |
| self.sel = self.view.get_regions('marked')[0] if marked else list(self.view.sel())[0] | |
| line = self.view.line(self.sel) | |
| line_content = self.view.substr(line) | |
| if line_content.lstrip().startswith('▾'): | |
| if toggle: | |
| self.view.run_command('dired_fold') | |
| return | |
| # 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 | |
| # Replace both the arrow and the folder icon | |
| collapsed_icon = ICONS['folder_collapsed'] | |
| expanded_icon = ICONS['folder_expanded'] | |
| # Replace arrow and icon | |
| if collapsed_icon in line_content: | |
| parts = line_content.split(collapsed_icon) | |
| if len(parts) > 1: | |
| prefix = parts[0].replace('▸', '▾') | |
| rest = parts[1] | |
| root = prefix + expanded_icon + rest | |
| else: | |
| root = line_content.split(os.sep)[0].replace('▸', '▾', 1) + os.sep | |
| else: | |
| root = line_content.split(os.sep)[0].replace('▸', '▾', 1) + os.sep | |
| self.show_hidden = self.view.settings().get('dired_show_hidden_files', True) | |
| items, error = self.try_listing_directory(path) | |
| if error: | |
| replacement = ['%s\t<%s>' % (root, error)] | |
| elif items: | |
| replacement = [root] + self.prepare_filelist(items, '', path, '\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 = ['%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_draw_vcs_marker") | |
| emit_event('add_paths', (self.view.id(), [path]), view=self.view) | |
| class DiredFold(TextCommand, DiredBaseCommand): | |
| ''' | |
| 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) | |
| self.view.run_command("dired_draw_vcs_marker") | |
| 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) | |
| # ------------- ORIGINAL CODE ----------------------------- | |
| # 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 | |
| # start_line = 1 + v.rowcol(line.a)[0] | |
| # # 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) | |
| # 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, '▸') | |
| # v.erase(edit, indented_region) | |
| # v.set_read_only(True) | |
| # emit_event('remove_path', (self.view.id(), self.index[start_line - 1]), view=self.view) | |
| # 4. Update the apply_change_into_view method in DiredFold class | |
| # to preserve icons when changing the arrow | |
| # ------------- WORKING CODE(21.2.2025) -----------------------------!!!!!!!!!!!!!!!!!! | |
| 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 | |
| start_line = 1 + v.rowcol(line.a)[0] | |
| # 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) | |
| 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: | |
| if isinstance(self.seled[0], list): | |
| self.seled[0].append(folded_name) | |
| else: | |
| try: | |
| self.seled = (list(self.seled[0]) + [folded_name], self.seled[1]) | |
| except (TypeError, ValueError): | |
| self.seled = ([folded_name], self.seled[1]) | |
| # Find and replace the arrow and folder icon | |
| line_content = v.substr(line) | |
| expanded_icon = ICONS['folder_expanded'] | |
| collapsed_icon = ICONS['folder_collapsed'] | |
| if '▾' in line_content and expanded_icon in line_content: | |
| # Replace both the arrow and the folder icon | |
| parts = line_content.split(expanded_icon) | |
| if len(parts) > 1: | |
| prefix = parts[0].replace('▾', '▸') | |
| rest = parts[1] | |
| v.set_read_only(False) | |
| v.replace(edit, line, prefix + collapsed_icon + rest) | |
| v.erase(edit, indented_region) | |
| v.set_read_only(True) | |
| else: | |
| # Fallback to simple replacement | |
| arrow_pos = line_content.find('▾') | |
| if arrow_pos != -1: | |
| icon_region = Region(line.a + arrow_pos, line.a + arrow_pos + 1) | |
| v.set_read_only(False) | |
| v.replace(edit, icon_region, '▸') | |
| v.erase(edit, indented_region) | |
| v.set_read_only(True) | |
| else: | |
| # Fallback to original method | |
| 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, '▸') | |
| v.erase(edit, indented_region) | |
| v.set_read_only(True) | |
| emit_event('remove_path', (self.view.id(), self.index[start_line - 1]), view=self.view) | |
| class DiredUpCommand(TextCommand, DiredBaseCommand): | |
| def run(self, edit): | |
| path = self.path | |
| if path == 'ThisPC\\': | |
| self.view.run_command('dired_refresh') | |
| return | |
| parent = dirname(path.rstrip(os.sep)) | |
| if not parent.endswith(os.sep): | |
| parent += os.sep | |
| if parent == path: | |
| if NT: | |
| parent = 'ThisPC\\' | |
| else: | |
| 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 INTERACTIONS ################################################# | |
| class DiredDoubleclickCommand(TextCommand, DiredBaseCommand): | |
| def run_(self, _, args): | |
| view = self.view | |
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # coding: utf8 | |
| '''This module contains all commands for operations with files: | |
| create, delete, rename, copy, and move | |
| ''' | |
| import re | |
| import os | |
| import shutil | |
| import tempfile | |
| import itertools | |
| import threading | |
| from os.path import basename, dirname, isdir, isfile, exists, join | |
| import sublime | |
| from sublime import Region | |
| from sublime_plugin import TextCommand | |
| from .common import ( | |
| DiredBaseCommand, relative_path, emit_event, | |
| NT, PARENT_SYM, MARK_OPTIONS) | |
| from . import prompt | |
| if NT: | |
| import ctypes | |
| from Default.send2trash.plat_win import SHFILEOPSTRUCTW | |
| try: | |
| import Default.send2trash as send2trash | |
| except ImportError: | |
| send2trash = None | |
| class DiredCreateCommand(TextCommand, DiredBaseCommand): | |
| def run(self, edit, which=None): | |
| assert which in ('file', 'directory'), "which: " + which | |
| emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| self.index = self.get_all() | |
| rel_path = relative_path(self.get_selected(parent=False) or '') | |
| self.which = which | |
| self.refresh = True | |
| pv = self.view.window().show_input_panel( | |
| which.capitalize() + ':', rel_path, self.on_done, None, None) | |
| pv.run_command('move_to', {'to': 'eol', 'extend': False}) | |
| pv.settings().set('dired_create', True) | |
| pv.settings().set('which', which) | |
| pv.settings().set('dired_path', self.path) | |
| def on_done(self, value): | |
| value = value.strip() | |
| if not value: | |
| return False | |
| fqn = join(self.path, value) | |
| if exists(fqn): | |
| sublime.error_message('{0} already exists'.format(fqn)) | |
| return False | |
| if self.which == 'directory': | |
| os.makedirs(fqn) | |
| else: | |
| with open(fqn, 'wb'): | |
| pass | |
| if self.refresh: # user press enter | |
| emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| self.view.run_command('dired_refresh', {'goto': fqn}) | |
| # user press ctrl+enter, no refresh | |
| return fqn | |
| class DiredCreateAndOpenCommand(DiredCreateCommand): | |
| '''Being called with ctrl+enter while user is in Create prompt | |
| So self.view is prompt view | |
| ''' | |
| def run(self, edit): | |
| self.which = self.view.settings().get('which', '') | |
| if not self.which: | |
| return sublime.error_message('oops, does not work!') | |
| self.refresh = False | |
| value = self.view.substr(Region(0, self.view.size())) | |
| fqn = self.on_done(value) | |
| if not fqn: | |
| return sublime.status_message('oops, does not work!') | |
| sublime.active_window().run_command('hide_panel', {'cancel': True}) | |
| dired_view = sublime.active_window().active_view() | |
| if dired_view.settings().has('dired_path'): | |
| self.refresh = True | |
| if self.which == 'directory': | |
| dired_view.settings().set('dired_path', fqn + os.sep) | |
| else: | |
| sublime.active_window().open_file(fqn) | |
| if self.refresh: | |
| emit_event('watch_view', dired_view.id(), plugin='FileBrowserWFS') | |
| dired_view.run_command('dired_refresh', {'goto': fqn}) | |
| class DiredDeleteCommand(TextCommand, DiredBaseCommand): | |
| def run(self, edit, trash=False): | |
| self.index = self.get_all() | |
| files = self.get_marked() or self.get_selected(parent=False) | |
| if not files: | |
| return sublime.status_message('Nothing chosen') | |
| msg, trash = self.setup_msg(files, trash) | |
| emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| if trash: | |
| need_confirm = self.view.settings().get('dired_confirm_send2trash', True) | |
| msg = msg.replace('Delete', 'Delete to trash', 1) | |
| if not need_confirm or (need_confirm and sublime.ok_cancel_dialog(msg)): | |
| self._to_trash(files) | |
| elif not trash and sublime.ok_cancel_dialog(msg): | |
| self._delete(files) | |
| else: | |
| print("Cancel delete or something wrong in DiredDeleteCommand") | |
| emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| def setup_msg(self, files, trash): | |
| '''If user send to trash, but send2trash is unavailable, we suggest deleting permanently''' | |
| # Yes, I know this is English. Not sure how Sublime is translating. | |
| if len(files) == 1: | |
| msg = "Delete {0}?".format(files[0]) | |
| else: | |
| msg = "Delete {0} items?".format(len(files)) | |
| if trash and not send2trash: | |
| msg = "Cannot delete to trash.\nPermanently " + msg.replace('D', 'd', 1) | |
| trash = False | |
| return (msg, trash) | |
| def _to_trash(self, files): | |
| '''Sending to trash might be slow | |
| So we start two threads which run _sender function in parallel and each of them waiting and | |
| setting certain event: | |
| 1. remove one which signals that _sender need try to call send2trash for current file | |
| and if it fails collect error message (always ascii, no bother with encoding) | |
| 2. report one which call _status function to display message on status-bar so user | |
| can see what is going on | |
| When loop in _sender is finished we refresh view and show errors if any. | |
| On Windows we call API directly in call_SHFileOperationW class. | |
| ''' | |
| path = self.path | |
| if NT: # use winapi directly, because better errors handling, plus GUI | |
| sources_move = [join(path, f) for f in files] | |
| return call_SHFileOperationW(self.view, sources_move, [], '$TRASH$') | |
| errors = [] | |
| def _status(filename='', done=False): | |
| if done: | |
| sublime.set_timeout(lambda: self.view.run_command('dired_refresh'), 1) | |
| if errors: | |
| sublime.error_message( | |
| 'Some files couldn’t be sent to trash ' | |
| '(perhaps, they are being used by another process): \n\n' | |
| + '\n'.join(errors).replace('Couldn\'t perform operation.', '') | |
| ) | |
| else: | |
| status = 'Please, wait… Removing ' + filename | |
| sublime.set_timeout(lambda: self.view.set_status("__FileBrowser__", status), 1) | |
| def _sender(files, event_for_wait, event_for_set): | |
| for filename in files: | |
| event_for_wait.wait() | |
| event_for_wait.clear() | |
| if event_for_wait is remove_event: | |
| try: | |
| send2trash.send2trash(join(path, filename)) | |
| except OSError as e: | |
| errors.append('{0}:\t{1}'.format(e, filename)) | |
| else: | |
| _status(filename) | |
| event_for_set.set() | |
| if event_for_wait is remove_event: | |
| _status(done=True) | |
| remove_event = threading.Event() | |
| report_event = threading.Event() | |
| t1 = threading.Thread(target=_sender, args=(files, remove_event, report_event)) | |
| t2 = threading.Thread(target=_sender, args=(files, report_event, remove_event)) | |
| t1.start() | |
| t2.start() | |
| report_event.set() | |
| def _delete(self, files): | |
| '''Delete is fast, no need to bother with threads or even status message | |
| But need to bother with encoding of error messages since they are vary, | |
| depending on OS and/or version of Python | |
| ''' | |
| errors = [] | |
| for filename in files: | |
| fqn = join(self.path, filename) | |
| try: | |
| if isdir(fqn): | |
| shutil.rmtree(fqn) | |
| else: | |
| os.remove(fqn) | |
| except (PermissionError, FileNotFoundError) as e: | |
| e = str(e).split(':')[0].replace('[Error 5] ', 'Access denied') | |
| errors.append('{0}:\t{1}'.format(e, filename)) | |
| self.view.run_command('dired_refresh') | |
| if errors: | |
| sublime.error_message('Some files couldn’t be deleted: \n\n' + '\n'.join(errors)) | |
| # -------------- ORIGINAL CODE ------------------- | |
| # class DiredRenameCommand(TextCommand, DiredBaseCommand): | |
| # def run(self, edit): | |
| # if not self.filecount(): | |
| # return sublime.status_message('Directory seems empty, nothing could be renamed') | |
| # emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| # # Store the original filenames so we can compare later. | |
| # path = self.path | |
| # self.view.settings().set( | |
| # 'rename', | |
| # [ | |
| # f | |
| # for f in self.get_all_relative('' if path == 'ThisPC\\' else path) | |
| # if f and f != PARENT_SYM | |
| # ] | |
| # ) | |
| # self.view.settings().set('dired_rename_mode', True) | |
| # self.view.set_read_only(False) | |
| # self.set_ui_in_rename_mode(edit) | |
| # self.view.set_status( | |
| # "__FileBrowser__", | |
| # " 𝌆 [enter: Apply changes] [escape: Discard changes]" | |
| # + ( | |
| # ' ¡¡¡DO NOT RENAME DISKS!!! you can rename their children though' | |
| # if path == 'ThisPC\\' else '' | |
| # ) | |
| # ) | |
| # # Mark the original filename lines so we can make sure they are in the same place. | |
| # r = self.fileregion() | |
| # self.view.add_regions('rename', [r], '', '', MARK_OPTIONS) | |
| class DiredRenameCommand(TextCommand, DiredBaseCommand): | |
| def run(self, edit): | |
| if not self.filecount(): | |
| return sublime.status_message('Directory seems empty, nothing could be renamed') | |
| emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| # Store the original filenames so we can compare later. | |
| path = self.path | |
| self.view.settings().set( | |
| 'rename', | |
| [ | |
| f | |
| for f in self.get_all_relative('' if path == 'ThisPC\\' else path) | |
| if f and f != PARENT_SYM | |
| ] | |
| ) | |
| self.view.settings().set('dired_rename_mode', True) | |
| self.view.set_read_only(False) | |
| self.set_ui_in_rename_mode(edit) | |
| self.view.set_status( | |
| "__FileBrowser__", | |
| " 𝌆 [enter: Apply changes] [escape: Discard changes]" | |
| + ( | |
| ' ¡¡¡DO NOT RENAME DISKS!!! you can rename their children though' | |
| if path == 'ThisPC\\' else '' | |
| ) | |
| ) | |
| # Mark the original filename lines so we can make sure they are in the same place. | |
| r = self.fileregion() | |
| self.view.add_regions('rename', [r], '', '', MARK_OPTIONS) | |
| class DiredRenameCancelCommand(TextCommand, DiredBaseCommand): | |
| """Cancel rename mode""" | |
| def run(self, edit): | |
| # Remember which directories were expanded | |
| expanded_dirs = [r.a for r in self.view.find_all(r'^\s*▾')] | |
| expanded_paths = [] | |
| for region in expanded_dirs: | |
| line = self.view.line(region) | |
| try: | |
| path = self.get_fullpath_for(line) | |
| expanded_paths.append(path) | |
| except: | |
| pass | |
| # Make sure we're watching the view again | |
| emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| # Clear rename-related settings | |
| self.view.settings().erase('rename') | |
| self.view.settings().set('dired_rename_mode', False) | |
| self.view.erase_regions('rename') | |
| # Refresh the view to restore the original state with icons | |
| self.view.run_command('dired_refresh', {'to_expand': expanded_paths}) | |
| class DiredRenameCommitCommand(TextCommand, DiredBaseCommand): | |
| # --------------- ORIGINAL CODE ---------------- | |
| # def run(self, edit): | |
| # if not self.view.settings().has('rename'): | |
| # # Shouldn't happen, but we want to cleanup when things go wrong. | |
| # self.view.run_command('dired_refresh') | |
| # return | |
| # before = self.view.settings().get('rename') | |
| # # We marked the set of files with a region. Make sure the region still has the same | |
| # # number of files. | |
| # after = self.get_after() | |
| # if len(after) != len(before): | |
| # return sublime.error_message('You cannot add or remove lines') | |
| # if len(set(after)) != len(after): | |
| # return self.report_conflicts(before, after) | |
| # self.apply_renames(before, after) | |
| # self.view.erase_regions('rename') | |
| # self.view.settings().erase('rename') | |
| # self.view.settings().set('dired_rename_mode', False) | |
| # emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| # self.view.run_command('dired_refresh', {'to_expand': self.re_expand_new_names()}) | |
| def run(self, edit): | |
| print("DiredRenameCommitCommand run method called") | |
| if not self.view.settings().has('rename'): | |
| # Shouldn't happen, but we want to cleanup when things go wrong. | |
| self.view.run_command('dired_refresh') | |
| return | |
| before = self.view.settings().get('rename') | |
| after = self.get_after() | |
| print(f"Before: {before}") | |
| print(f"After: {after}") | |
| if len(after) != len(before): | |
| return sublime.error_message('You cannot add or remove lines') | |
| if len(set(after)) != len(after): | |
| return self.report_conflicts(before, after) | |
| # Remember which directories were expanded | |
| expanded_dirs = [r.a for r in self.view.find_all(r'^\s*▾')] | |
| expanded_paths = [] | |
| for region in expanded_dirs: | |
| line = self.view.line(region) | |
| path = self.get_fullpath_for(line) | |
| expanded_paths.append(path) | |
| # Apply the renames | |
| self.apply_renames(before, after) | |
| # Clean up rename mode settings | |
| self.view.erase_regions('rename') | |
| self.view.settings().erase('rename') | |
| self.view.settings().set('dired_rename_mode', False) | |
| # Make sure to set read-only back to True | |
| self.view.set_read_only(True) | |
| # Now emit the watch event for the view | |
| emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| # Complete refresh of the view with the expanded dirs | |
| to_expand = self.re_expand_new_names() or expanded_paths | |
| self.view.run_command('dired_refresh', {'to_expand': to_expand}) | |
| # --------------- ORIGINAL CODE ---------------- | |
| # def get_after(self): | |
| # '''Return list of all filenames in the view''' | |
| # self.index = self.get_all() | |
| # path = self.path | |
| # lines = self._get_lines(self.view.get_regions('rename'), self.fileregion()) | |
| # return [self._new_name(line, path=path) for line in lines] | |
| def get_after(self): | |
| '''Return list of all filenames in the view''' | |
| self.index = self.get_all() | |
| path = self.path | |
| lines = self._get_lines(self.view.get_regions('rename'), self.fileregion()) | |
| return [self._new_name(line, path=path) for line in lines] | |
| # MODIFINY THIS ORIGINAL CODE | |
| # def _new_name(self, line, path=None, full=False): | |
| # '''Return new name for line | |
| # full | |
| # if True return full path, otherwise relative to path variable | |
| # path | |
| # root, returning value is relative to it | |
| # ''' | |
| # if full: | |
| # parent = dirname(self.get_fullpath_for(line).rstrip(os.sep)) | |
| # else: | |
| # parent = dirname(self.get_parent(line, path).rstrip(os.sep)) | |
| # new_name = self.view.substr(Region(self._get_name_point(line), line.b)) | |
| # if os.sep in new_name: # ignore trailing errors, e.g. <empty> | |
| # new_name = new_name.split(os.sep)[0] + os.sep | |
| # return join(parent, new_name) | |
| def _new_name(self, line, path=None, full=False): | |
| '''Return new name for line''' | |
| # Get the parent directory path | |
| if full: | |
| parent = dirname(self.get_fullpath_for(line).rstrip(os.sep)) | |
| else: | |
| parent = dirname(self.get_parent(line, path).rstrip(os.sep)) | |
| # Get the filename from the line | |
| name_point = self._get_name_point(line) | |
| text = self.view.substr(Region(name_point, line.b)) | |
| # Remove any Nerd Font icons (non-ASCII characters) | |
| cleaned_text = re.sub(r'[^\x20-\x7E]', '', text).strip() | |
| # Handle directory separator if present | |
| is_dir = self.get_fullpath_for(line).endswith(os.sep) | |
| # Create the cleaned name | |
| new_name = cleaned_text | |
| if is_dir and not new_name.endswith(os.sep): | |
| new_name += os.sep | |
| # Join with parent path | |
| return join(parent, new_name) | |
| def report_conflicts(self, before, after): | |
| ''' | |
| before list of all filenames before enter rename mode | |
| after list of all filenames upon commit rename | |
| Warn about conflicts and print original (before) and conflicting (after) names for | |
| each item which cause conflict. | |
| ''' | |
| sublime.error_message('There are duplicate filenames (see details in console)') | |
| self.view.window().run_command("show_panel", {"panel": "console"}) | |
| print( | |
| *( | |
| '\n Original name: {0}\nConflicting name: {1}'.format(b, a) | |
| for (b, a) in zip(before, after) if b != a and a in before | |
| ), | |
| sep='\n', | |
| end='\n\n' | |
| ) | |
| print('You can either resolve conflicts and apply changes or cancel renaming.\n') | |
| def apply_renames(self, before, after): | |
| '''args are the same as in self.report_conflicts | |
| If before and after differ, try to do actual rename for each pair | |
| Take into account directory symlinks on Unix-like OSes (they cannot be just renamed) | |
| In case of error, show message and exit skipping remain pairs | |
| ''' | |
| # reverse order to allow renaming child and parent at the same time (tree view) | |
| diffs = list(reversed([(b, a) for (b, a) in zip(before, after) if b != a])) | |
| if not diffs: | |
| return sublime.status_message('Exit rename mode, no changes') | |
| path = self.path | |
| existing = set(before) | |
| window = self.view.window() | |
| def retarget(a, b): | |
| if not window: | |
| return | |
| view = window.find_open_file(a) | |
| if view: | |
| view.retarget(b) | |
| while diffs: | |
| b, a = diffs.pop(0) | |
| if a in existing: | |
| # There is already a file with this name. Give it a temporary name (in | |
| # case of cycles like "x->z and z->x") and put it back on the list. | |
| tmp = tempfile.NamedTemporaryFile(delete=False, dir=path).name | |
| os.unlink(tmp) | |
| diffs.append((tmp, a)) | |
| a = tmp | |
| print('FileBrowser rename: {0} → {1}'.format(b, a)) | |
| orig = join(path, b) | |
| if orig[~0] == '/' and os.path.islink(orig[:~0]): | |
| # last slash shall be omitted; file has no last slash, | |
| # thus it False and symlink to file shall be os.rename'd | |
| dest = os.readlink(orig[:~0]) | |
| os.unlink(orig[:~0]) | |
| os.symlink(dest, join(path, a)[:~0]) | |
| else: | |
| try: | |
| os.rename(orig, join(path, a)) | |
| except OSError: | |
| msg = ( | |
| 'FileBrowser:\n\nError is occurred during renaming.\n' | |
| 'Please, fix it and apply changes or cancel renaming.\n\n' | |
| '\t {0} → {1}\n\n' | |
| 'Don’t rename\n' | |
| ' • non-existed file (cancel renaming to refresh)\n' | |
| ' • file if you’re not owner' | |
| ' • disk letter on Windows\n'.format(b, a) | |
| ) | |
| sublime.error_message(msg) | |
| return | |
| retarget(orig, join(path, a)) | |
| existing.remove(b) | |
| existing.add(a) | |
| def re_expand_new_names(self): | |
| '''Make sure that expanded directories will keep state if were renamed''' | |
| expanded = [self.view.line(r) for r in self.view.find_all(r'^\s*▾')] | |
| return [self._new_name(line, full=True) for line in expanded] | |
| class DiredCopyFilesCommand(TextCommand, DiredBaseCommand): | |
| '''Store filename(s) in settings, when user copy or cut''' | |
| def run(self, edit, cut=False): | |
| self.index = self.get_all() | |
| filenames = self.get_marked(full=True) or self.get_selected(parent=False, full=True) | |
| if not filenames: | |
| return sublime.status_message('Nothing chosen') | |
| settings = sublime.load_settings('dired.sublime-settings') | |
| copy_list = settings.get('dired_to_copy', []) | |
| cut_list = settings.get('dired_to_move', []) | |
| # copied item shall not be added into cut list, and vice versa | |
| for f in filenames: | |
| if cut: | |
| if f not in copy_list: | |
| cut_list.append(f) | |
| else: | |
| if f not in cut_list: | |
| copy_list.append(f) | |
| settings.set('dired_to_move', list(set(cut_list))) | |
| settings.set('dired_to_copy', list(set(copy_list))) | |
| sublime.save_settings('dired.sublime-settings') | |
| self.show_hidden = self.view.settings().get('dired_show_hidden_files', True) | |
| self.set_status() | |
| class DiredPasteFilesCommand(TextCommand, DiredBaseCommand): | |
| '''Init file operation(s) for stored names in settings, when user paste''' | |
| def run(self, edit): | |
| s = self.view.settings() | |
| sources_move = s.get('dired_to_move', []) | |
| sources_copy = s.get('dired_to_copy', []) | |
| if not (sources_move or sources_copy): | |
| return sublime.status_message('Nothing to paste') | |
| self.index = self.get_all() | |
| path = self.get_path() | |
| rel_path = relative_path(self.get_selected(parent=False) or '') | |
| destination = join(path, rel_path) or path | |
| emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| if NT: | |
| return call_SHFileOperationW(self.view, sources_move, sources_copy, destination) | |
| else: | |
| return call_SystemAgnosticFileOperation( | |
| self.view, sources_move, sources_copy, destination) | |
| class DiredPasteFilesToCommand(TextCommand, DiredBaseCommand): | |
| '''Init prompt for path where to paste, then init file ops.''' | |
| def run(self, edit): | |
| s = self.view.settings() | |
| self.index = self.get_all() | |
| sources_move = s.get('dired_to_move', []) | |
| sources_copy = ( | |
| s.get('dired_to_copy') | |
| or self.get_marked(full=True) | |
| or self.get_selected(parent=False, full=True) | |
| ) | |
| mitems = len(sources_move) | |
| citems = len(sources_copy) | |
| if not (mitems or citems): | |
| return sublime.status_message('Nothing to paste') | |
| both = mitems and citems | |
| msg = '%s%s to:' % (('Move %d' % mitems) if mitems else '', | |
| ('%sopy %d' % (' and c' if both else 'C', citems)) if citems else '') | |
| path = self.get_path() | |
| window = self.view.window() or sublime.active_window() | |
| emit_event('ignore_view', self.view.id(), plugin='FileBrowserWFS') | |
| prompt.start(msg, window, path, self.initfo, sources_move, sources_copy) | |
| def initfo(self, destination, move, copy): | |
| if NT: | |
| return call_SHFileOperationW(self.view, move, copy, destination) | |
| else: | |
| return call_SystemAgnosticFileOperation(self.view, move, copy, destination) | |
| class DiredClearCopyCutList(TextCommand): | |
| def run(self, edit): | |
| sublime.load_settings('dired.sublime-settings').set('dired_to_move', []) | |
| sublime.load_settings('dired.sublime-settings').set('dired_to_copy', []) | |
| sublime.save_settings('dired.sublime-settings') | |
| self.view.run_command('dired_refresh') | |
| def _dups(sources_copy, destination): | |
| '''return list of files that should be duplicated silently''' | |
| return [ | |
| p | |
| for p in sources_copy | |
| if os.path.split(p.rstrip(os.sep))[0] == destination.rstrip(os.sep) | |
| ] | |
| class call_SHFileOperationW(object): | |
| '''call Windows API for file operations''' | |
| def __init__(self, view, sources_move, sources_copy, destination): | |
| self.view = view | |
| if destination == '$TRASH$': | |
| self.shfow_d_thread = threading.Thread(target=self.caller, args=(3, sources_move, '')) | |
| self.shfow_d_thread.start() | |
| return | |
| if sources_move: | |
| self.shfow_m_thread = threading.Thread( | |
| target=self.caller, args=(1, sources_move, destination)) | |
| self.shfow_m_thread.start() | |
| if sources_copy: | |
| # if user paste files in the same folder where they are then | |
| # it shall duplicate these files w/o asking anything | |
| dups = _dups(sources_copy, destination) | |
| if dups: | |
| self.shfow_d_thread = threading.Thread( | |
| target=self.caller, args=(2, dups, destination, True)) | |
| self.shfow_d_thread.start() | |
| sources_copy = [p for p in sources_copy if p not in dups] | |
| if sources_copy: | |
| self.shfow_c_thread = threading.Thread( | |
| target=self.caller, args=(2, sources_copy, destination)) | |
| self.shfow_c_thread.start() | |
| else: | |
| self.shfow_c_thread = threading.Thread( | |
| target=self.caller, args=(2, sources_copy, destination)) | |
| self.shfow_c_thread.start() | |
| def caller(self, mode, sources, destination, duplicate=False): | |
| '''mode is int: 1 (move), 2 (copy), 3 (delete)''' | |
| if duplicate: | |
| fFlags = 8 | |
| elif mode == 3: | |
| fFlags = 64 # send to recycle bin | |
| else: | |
| fFlags = 0 | |
| SHFileOperationW = ctypes.windll.shell32.SHFileOperationW | |
| SHFileOperationW.argtypes = [ctypes.POINTER(SHFILEOPSTRUCTW)] | |
| pFrom = '\x00'.join(sources) + '\x00' | |
| pTo = ('%s\x00' % destination) if destination else None | |
| wf = ctypes.WINFUNCTYPE(ctypes.wintypes.HWND) | |
| get_hwnd = wf(ctypes.windll.user32.GetForegroundWindow) | |
| args = SHFILEOPSTRUCTW( | |
| hwnd = get_hwnd(), | |
| wFunc = ctypes.wintypes.UINT(mode), | |
| pFrom = ctypes.wintypes.LPCWSTR(pFrom), | |
| pTo = ctypes.wintypes.LPCWSTR(pTo), | |
| fFlags = fFlags, | |
| fAnyOperationsAborted = ctypes.wintypes.BOOL() | |
| ) | |
| out = SHFileOperationW(ctypes.byref(args)) | |
| sublime.set_timeout( | |
| lambda: emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS'), 1) | |
| if not out and destination: # 0 == success | |
| sublime.set_timeout(lambda: self.view.run_command('dired_clear_copy_cut_list'), 1) | |
| else: # probably user cancel op., or sth went wrong; keep settings | |
| sublime.set_timeout(lambda: self.view.run_command('dired_refresh'), 1) | |
| class call_SystemAgnosticFileOperation(object): | |
| '''file operations using Python standard library''' | |
| def __init__(self, view, sources_move, sources_copy, destination): | |
| self.view = view | |
| self.window = view.window() | |
| self.threads = [] | |
| self.errors = {} | |
| if sources_move: | |
| self.caller('move', sources_move, destination) | |
| if sources_copy: | |
| # if user paste files in the same folder where they are then | |
| # it shall duplicate these files w/o asking anything | |
| dups = _dups(sources_copy, destination) | |
| if dups: | |
| self.caller('dup', dups, destination) | |
| sources_copy = [p for p in sources_copy if p not in dups] | |
| if sources_copy: | |
| self.caller('copy', sources_copy, destination) | |
| else: | |
| self.caller('copy', sources_copy, destination) | |
| self.check_errors() | |
| self.start_threads() | |
| def check_errors(self): | |
| msg = ( | |
| 'FileBrowser:\n\n' | |
| 'Some files exist already, Cancel to skip all, OK to overwrite or rename.\n\n' | |
| '\t{0}'.format('\n\t'.join(self.errors.keys())) | |
| ) | |
| self.actions = [['Overwrite', 'Folder cannot be overwritten'], | |
| ['Duplicate', 'Item will be renamed automatically']] | |
| if self.errors and sublime.ok_cancel_dialog(msg): | |
| self.show_quick_panel() | |
| def start_threads(self): | |
| if self.threads: | |
| for t in self.threads: | |
| t.start() | |
| self.progress_bar(self.threads) | |
| def show_quick_panel(self): | |
| '''dialog asks if user would like to duplicate, overwrite, or skip item''' | |
| t, f = self.errors.popitem() | |
| options = self.actions + [['from %s' % f, 'Skip'], ['to %s' % t, 'Skip']] | |
| done = lambda i: self.user_input(i, f, t) | |
| sublime.set_timeout( | |
| lambda: self.window.show_quick_panel(options, done, sublime.MONOSPACE_FONT), | |
| 10 | |
| ) | |
| return | |
| def user_input(self, i, name, new_name): | |
| if i == 0: | |
| self._setup('overwrite', name, new_name) | |
| if i == 1: | |
| self.duplicate(name, new_name) | |
| if self.errors: | |
| self.show_quick_panel() | |
| else: | |
| self.start_threads() | |
| def caller(self, mode, sources, destination): | |
| for fqn in sources: | |
| new_name = join(destination, basename(fqn.rstrip(os.sep))) | |
| if mode == 'dup': | |
| self.duplicate(fqn, new_name) | |
| else: | |
| self._setup(mode, fqn, new_name) | |
| def duplicate(self, name, new_name): | |
| new_name = self.generic_nn(new_name) | |
| self._setup_copy(name, new_name, False) | |
| def _setup(self, mode, original, new_name): | |
| '''mode can be "move", "copy", "overwrite"''' | |
| if mode == 'move' and original != dirname(new_name): | |
| return self._setup_move(original, new_name) | |
| overwrite = mode == 'overwrite' | |
| if mode == 'copy' or overwrite: | |
| return self._setup_copy(original, new_name, overwrite) | |
| def _setup_move(self, original, new_name): | |
| if not exists(new_name): | |
| self._init_thread('move', original, new_name) | |
| else: | |
| self.errors.update({new_name: original}) | |
| def _setup_copy(self, original, new_name, overwrite): | |
| '''overwrite is either True or False''' | |
| def __dir_or_file(mode, new_name, overwrite): | |
| exist = isdir(new_name) if mode == 'dir' else isfile(new_name) | |
| if not exist or overwrite: | |
| self._init_thread(mode, original, new_name) | |
| else: | |
| self.errors.update({new_name: original}) | |
| if isdir(original): | |
| __dir_or_file('dir', new_name, overwrite) | |
| else: | |
| __dir_or_file('file', new_name, overwrite) | |
| def _init_thread(self, mode, source_name, new_name): | |
| '''mode can be "move", "dir", "file"''' | |
| t = threading.Thread(target=self._do, args=(mode, source_name, new_name)) | |
| t.setName(new_name) | |
| self.threads.append(t) | |
| def _do(self, mode, source_name, new_name): | |
| try: | |
| if mode == 'move': shutil.move(source_name, new_name) | |
| if mode == 'dir': shutil.copytree(source_name, new_name) | |
| if mode == 'file': shutil.copy2(source_name, new_name) | |
| except shutil.Error as e: | |
| m = e.args[0] | |
| if isinstance(m, list): | |
| sublime.error_message( | |
| 'FileBrowser:\n\n{0}'.format('\n'.join([i[-1] for i in m])) | |
| ) | |
| else: | |
| sublime.error_message('FileBrowser:\n\n' + e) | |
| except Exception as e: # just in case | |
| sublime.error_message('FileBrowser:\n\n' + str([e])) | |
| def progress_bar(self, threads, i=0, dir=1): | |
| threads = [t for t in threads if t.is_alive()] | |
| if threads: | |
| # This animates a little activity indicator in the status area | |
| before = i % 8 | |
| after = (7) - before | |
| if not after: dir = -1 | |
| if not before: dir = 1 | |
| i += dir | |
| self.view.set_status( | |
| '__FileBrowser__', | |
| 'Please wait{0}…{1}Writing {2}'.format( | |
| ' ' * before, | |
| ' ' * after, | |
| ', '.join([t.name for t in threads]) | |
| ) | |
| ) | |
| sublime.set_timeout(lambda: self.progress_bar(threads, i, dir), 100) | |
| return | |
| else: | |
| emit_event('watch_view', self.view.id(), plugin='FileBrowserWFS') | |
| self.view.run_command('dired_clear_copy_cut_list') | |
| def generic_nn(self, old_name): | |
| path, name = os.path.split(old_name) | |
| split_name = name.split('.') | |
| no_extension = len(split_name) == 1 or isdir(old_name) | |
| separator = self.view.settings().get('dired_dup_separator', ' — ') | |
| for i in itertools.count(2): | |
| if no_extension: | |
| cfp = "{1}{2}{0}".format(i, old_name, separator) | |
| else: | |
| # leading space may cause problems, e.g. | |
| # good: 'name — 2.ext' | |
| # good: '— 2.ext' | |
| # bad: ' — 2.ext' | |
| fn, ext = '.'.join(split_name[:~0]), split_name[~0] | |
| nn = '{0}{1}{2}.{3}'.format(fn, separator, i, ext) | |
| cfp = join(path, nn.lstrip()) | |
| if not os.path.exists(cfp): | |
| break | |
| return cfp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/python | |
| # -*- coding: utf-8 -*- | |
| import os | |
| from os.path import basename | |
| from .common import first, set_proper_scheme, calc_width, get_group | |
| SYNTAX_EXTENSION = '.sublime-syntax' | |
| def set_active_group(window, view, other_group): | |
| nag = window.active_group() | |
| if other_group: | |
| group = 0 if other_group == 'left' else 1 | |
| groups = window.num_groups() | |
| if groups == 1: | |
| width = calc_width(view) | |
| cols = [0.0, width, 1.0] if other_group == 'left' else [0.0, 1-width, 1.0] | |
| window.set_layout({ | |
| "cols": cols, | |
| "rows": [0.0, 1.0], | |
| "cells": [[0, 0, 1, 1], [1, 0, 2, 1]] | |
| }) | |
| elif view: | |
| group = get_group(groups, nag) | |
| window.set_view_index(view, group, 0) | |
| else: | |
| group = nag | |
| # when other_group is left, we need move all views to right except FB view | |
| if nag == 0 and other_group == 'left' and group == 0: | |
| for v in reversed(window.views_in_group(nag)[1:]): | |
| window.set_view_index(v, 1, 0) | |
| return (nag, group) | |
| def set_view(view_id, window, ignore_existing, path, single_pane): | |
| view = None | |
| if view_id: | |
| # The Goto command was used so the view is already known and its contents should be | |
| # replaced with the new path. | |
| view = first(window.views(), lambda v: v.id() == view_id) | |
| if not view and not ignore_existing: | |
| # See if a view for this path already exists. | |
| same_path = lambda v: v.settings().get('dired_path') == path | |
| # See if any reusable view exists in case of single_pane argument | |
| any_path = lambda v: v.score_selector(0, "text.dired") > 0 | |
| view = first(window.views(), any_path if single_pane else same_path) | |
| if not view: | |
| view = window.new_file() | |
| view.settings().add_on_change('color_scheme', lambda: set_proper_scheme(view)) | |
| view.set_syntax_file('Packages/FileBrowser/dired' + SYNTAX_EXTENSION) | |
| view.set_scratch(True) | |
| reset_sels = True | |
| else: | |
| reset_sels = path != view.settings().get('dired_path', '') | |
| return (view, reset_sels) | |
| def show( | |
| window, | |
| path, | |
| view_id=None, | |
| ignore_existing=False, | |
| single_pane=False, | |
| goto='', | |
| other_group='' | |
| ): | |
| """ | |
| Determines the correct view to use, creating one if necessary, and prepares it. | |
| """ | |
| if other_group: | |
| prev_focus = window.active_view() | |
| # simulate 'toggle sidebar': | |
| if prev_focus and 'dired' in prev_focus.scope_name(0): | |
| window.run_command('close_file') | |
| return | |
| if not path.endswith(os.sep): | |
| path += os.sep | |
| view, reset_sels = set_view(view_id, window, ignore_existing, path, single_pane) | |
| set_active_group(window, view, other_group) | |
| if other_group and prev_focus: | |
| window.focus_view(prev_focus) | |
| if path == os.sep: | |
| view_name = os.sep | |
| else: | |
| view_name = basename(path.rstrip(os.sep)) | |
| name = "𝌆 {0}".format(view_name) | |
| view.set_name(name) | |
| view.settings().set('dired_path', path) | |
| view.settings().set('dired_rename_mode', False) | |
| # forcibly shoot on_activated, because when view was created it didnot have any settings | |
| window.show_quick_panel(['a', 'b'], None) | |
| view.run_command('dired_refresh', {'goto': goto, 'reset_sels': reset_sels}) | |
| window.run_command('hide_overlay') | |
| window.focus_view(view) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment