How to find the tab of an open file by name in all Sublime Text windows

Solution 1:

You could solve this with a small package. Here's my try: create a folder named "winfinder" unter the Sublime 3 package folder (on the Mac, this would be ~/Library/Application Support/Sublime Text 3/Packages/winfinder).

Next, create a file main.py in that folder with this content:

import sublime
import sublime_plugin

class WinFindCommand(sublime_plugin.TextCommand):

    def search(self, search_string):
        l = []
        for w in sublime.windows():
            for sh in w.sheets():
                fn = sh.view().file_name()
                if fn is not None:
                    if search_string.lower() in fn:
                        l.append(fn + "\n")
        if len(l) > 0:
            v = sublime.active_window().new_file()
            v.set_name("SearchResults")
            v.run_command("insert",{"characters": str(len(l)) + " matches:\n\n"})
            v.run_command("insert",{"characters": "\n".join(l)})
        else:
            sublime.message_dialog("No match found.")


    def run(self, edit):
        w = sublime.active_window()
        w.show_input_panel("Search text", "", self.search, None, None)

Now we need a way to invoke the functionality. This is done by creating a file named main.sublime-commands in the same folder. Content is as follows:

[
    { "caption": "WindowFind: find in window title", "command": "win_find" },
]

Usage

If you open the command palette and type "WindowFind", you should see the command. Press [ENTER] and the package will prompt you for a search string to be searched for in all tabs of all windows. If there is no match, a message is displayed.

If there is a match, a new tab names "SearchResults" will be opened with the search results:

3 matches:

/Users/your_user/notes/daylog.txt

/Users/your_user/Documents/2018/paychecks.csv

/Users/your_user/source/python/daily_tweets/daily.py

(search string was "ay") -- just testet it on Sublime 3, works. Thanks for the idea, this is helpful! :-)

Solution 2:

Using an idea from @Arminius' answer, I tweaked the "Switch to View" code from the Emacs Pro Essentials plugin, so that it now works with views across all windows. Here is the resulting code in main.py:

# coding=utf-8
# Sublime plugin to search open files by filename, across all windows.
# See problem statement at https://superuser.com/questions/1327172/how-to-find-the-tab-of-an-open-file-by-name-in-all-sublime-text-windows

# Code adapted from https://github.com/sublime-emacs/sublemacspro/blob/master/switch_to_view.py
# and https://superuser.com/a/1328545/75777

import sublime, sublime_plugin, os


#
# Switch buffer command. "C-x b" equiv in emacs. This limits the set of files in a chooser to the
# ones currently loaded. Do we sort the files by last access? like emacs

class SwitchToViewCommand(sublime_plugin.TextCommand):
    def run(self, util, current_group_only=False, preview=False, completion_components=2, display_components=1):
        self.preview = preview
        self.completion_components = completion_components
        self.display_components = display_components
        window = self.window = sublime.active_window()
        self.group = window.active_group()
        # was: self.views = ViewState.sorted_views(window, window.active_group() if current_group_only else None)
        self.views = [sh.view() for w in sublime.windows() for sh in w.sheets()]
        ## TODO: sort the above views?
        # if window.num_groups() > 1 and not current_group_only:
        #     self.group_views = set(view.id() for view in sorted_views(window, window.active_group()))
        # else:
        #     self.group_views = None
        self.roots = get_project_roots()
        self.original_view = window.active_view()
        self.highlight_count = 0

        # swap the top two views to enable switching back and forth like emacs
        if len(self.views) >= 2:
            index = 1
        else:
            index = 0
        window.show_quick_panel(self.get_items(), self.on_select, 0, index, self.on_highlight)

    def on_select(self, index):
        if index >= 0:
            # Does this work even on views that aren't in self.window?
            v = self.views[index]
            if v.window() is not self.window:
                try:
                    v.window().bring_to_front()
                    v.window().focus_window()
                    # Strangely, I get the error 'Window' object has no attribute 'focus_window'
                    # even though the documentation says it should be there in this version of ST.
                except AttributeError as e:
                    print(f"{type(e).__name__} at line {e.__traceback__.tb_lineno} of {__file__}: {e}")
            v.window().focus_view(self.views[index])
        else:
            self.window.focus_view(self.original_view)

    def on_highlight(self, index):
        if not self.preview:
            return
        self.highlight_count += 1
        if self.highlight_count > 1:
            # if self.group_views is None or self.views[index].id() in self.group_views:
            self.window.focus_view(self.views[index])

    def get_items(self):
        if self.display_components > 0:
            return [[self.get_path(view), self.get_display_name(view)] for view in self.views]
        return [[self.get_path(view)] for view in self.views]

    def get_display_name(self, view):
        mod_star = '*' if view.is_dirty() else ''

        if view.is_scratch() or not view.file_name():
            disp_name = view.name() if len(view.name()) > 0 else 'untitled'
        else:
            disp_name = get_relative_path(self.roots, view.file_name(), self.display_components)

        return '%s%s' % (disp_name, mod_star)

    def get_path(self, view):
        if view.is_scratch():
            return view.name() or ""

        if not view.file_name():
            return '<unsaved>'

        return get_relative_path(self.roots, view.file_name(), self.completion_components)

# From https://github.com/sublime-emacs/sublemacspro/blob/master/lib/misc.py
# Returns the relative path for the specified file name. The roots are supplied by the
# get_project_roots function, which sorts them appropriately for this function.
#
def get_relative_path(roots, file_name, n_components=2):
    if file_name is not None:
        if roots is not None:
            for root in roots:
                if file_name.startswith(root):
                    file_name = file_name[len(root) + 1:]
                    break
        # show (no more than the) last 2 components of the matching path name
        return os.path.sep.join(file_name.split(os.path.sep)[-n_components:])
    else:
        return "<no file>"

# Get the current set of project roots, sorted from longest to shortest. They are suitable for
# passing to the get_relative_path function to produce the best relative path for a view file name.
#
def get_project_roots():
    window = sublime.active_window()
    if window.project_file_name() is None:
        roots = None
    else:
        project_dir = os.path.dirname(window.project_file_name())
        roots = sorted([os.path.normpath(os.path.join(project_dir, folder))
                       for folder in window.folders()],
                       key=lambda name: len(name), reverse=True)
    return roots

# # From ViewState. But I'm not using these right now. Don't really want to build the data structure of ViewStates.
#
# # Returns a list of views from a given window sorted by most recently accessed/touched. If group
# # is specified, uses only views in that group.
# #
# # @classmethod
# def sorted_views(window, group=None):
#     views = window.views_in_group(group) if group is not None else window.views()
#     states = [find_or_create(view) for view in views]
#     sorted_states = sorted(states, key=lambda state: state.touched, reverse=True)
#     return [state.view for state in sorted_states]
#
#
# # Finds or creates the state for the given view. This doesn't imply a touch().
# #
# # @classmethod
# def find_or_create(cls, view):
#     state = cls.view_state_dict.get(view.id(), None)
#     if state is None:
#         state = ViewState(view)
#     return state