1

Answers like https://stackoverflow.com/questions/20519040/search-in-all-files-in-a-project-in-sublime-text-3 and Search all within opened tabs with Sublime Text 2 tell how to search open files by their contents, using ++F and Where: <open files>.

But when I try this, it only searches files open in tabs in the current window. If I have two windows open with 10 tabs each, and I use ++F in one of the windows, the search results tell me that it searched 10 files, not 20.

Now if I have a dozen windows open, I don't want to have to cycle through each one searching for the file I want. (Yes I should clean up my clutter, but that's not the question I'm asking.)

Is there a way to find an open file in any open window?

I'd also like to find the file by name (or tab title, which is usually the same) rather than by contents. But I'd settle for searching by contents if I could search across all open windows.

Update: Things that seem like they ought to work

I've found packages like Emacs Pro Essentials and ExtendedTabSwitcher, both of which describe features that allow you to switch to other views/buffers/tabs by name. Both packages explicitly say that the default behavior is to do this across all groups (which I believe means across all windows). Yet when I try them, only tabs from the same window show up in the list to choose from.

The built-in Goto > Goto Anything menu item behaves the same way (unanswered ten-year-old technical support forum request).

It makes me wonder if there's something specific to the Mac version that is preventing all these tools from accessing tabs in other windows.

1
  • One option is to merge tabs from all windows into one window (stackoverflow.com/a/43836337/423105). That's not ideal ... it defeats the advantages of having separate windows in the first place. But at least you could then search across all open tabs.
    – LarsH
    May 31, 2018 at 20:31

2 Answers 2

2

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! :-)

3
  • I'm very late in writing this comment, but thank you -- for not just referring me to an existing solution but creating one. I ran across the same need 3+ years later, and am very glad to have this script.
    – LarsH
    Sep 20, 2021 at 14:14
  • When the list of search results is generated, is there any way to make a result clickable, so that clicking on it would open the existing tab where that file is open? Re-opening a file that's already open in a tab shows its contents on disk, so that if there are unsaved changes in the old tab, they aren't reflected in the new tab. You could end up with conflicts or lost data.
    – LarsH
    Sep 20, 2021 at 16:16
  • I updated the title of my question to clarify that I want to find the tab where the file is already open.
    – LarsH
    Sep 20, 2021 at 17:59
1

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
4
  • Do I also need Emacs Pro Essentials plugin installed for this to work? With the 2 files for the WinFind command as described in the other answer, I can find and run the win_find command via command palette. But with this one, I can't seem to find or run the command.
    – MikMak
    Apr 4 at 9:53
  • @MikMak Sorry, I don't know.
    – LarsH
    Apr 4 at 20:39
  • you wrote that you "tweaked the "Switch to View" code from the Emacs Pro Essentials plugin, so that it now works with views across all windows." That leads me to believe that the script you shared effectively works to search and switch to open tabs in any window (based on tab/file name). But does the script require the Emacs Pro Essentials plugin? or does it work independently, without the plugin installed? you wrote the script, so if anyone, i figured you should know what prerequisites it has.
    – MikMak
    Apr 14 at 8:33
  • @MikMak Indeed perhaps I should. (Or should have in 2021.) But my memory is not great. If I have time I'll look into it, but for now probably the safest assumption is that it does require that plugin.
    – LarsH
    Apr 15 at 12:59

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .