Skip to content

Instantly share code, notes, and snippets.

@OdatNurd
Last active May 14, 2022 00:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save OdatNurd/b3ff731554962021e233b337c7905fa8 to your computer and use it in GitHub Desktop.
Save OdatNurd/b3ff731554962021e233b337c7905fa8 to your computer and use it in GitHub Desktop.
Simple Fold Plugin
[
// You can also include a "caption" key to set what the menu
// text displays.
{ "command": "mark_fold_region" }
]
[
// A sample key binding; choose a key of your choice. You should
// also put this in your normal keybinding file, not one with the
// name shown here (it would have a platform in the name, such as
// Default (Windows).sublime-keymap)
{ "keys": ["super+f"], "command": "mark_fold_region", },
]
import sublime
import sublime_plugin
# Inspired by a comment from C. Y. Hollander on the video regarding making auto
# folding code snippets (https://youtu.be/oZTQAuC47hQ).
# We are storing a list of custom fold regions; these store the attributes for
# those regions; the name they're stored under, the color they are, the icon to
# use for them in the gutter, and the flags to apply when drawing them.
_setting_name = "_has_custom_folds"
_fold_name = "_folded_regions"
_unfold_name = "_unfolded_regions"
_scope = "region.greenish"
_icon = "dot"
_fold_icon = "Packages/User/arrow_right.png"
_unfold_icon = "Packages/User/arrow_down.png"
_flags = sublime.HIDDEN | sublime.PERSISTENT
def _get_custom_regions(view):
"""
Return back a list of all of the currently defined custom fold regions in
the provided view. The return is a tuple that tells you both the folded and
unfolded regions distinctly.
"""
folded = view.get_regions(_fold_name)
unfolded = view.get_regions(_unfold_name)
return folded, unfolded
def _set_custom_regions(view, folded, unfolded):
"""
Set a list of regions for both fold and unfolded regions into the buffer
"""
view.add_regions(_fold_name, folded, scope=_scope, icon=_fold_icon,
flags=_flags)
view.add_regions(_unfold_name, unfolded, scope=_scope, icon=_unfold_icon,
flags=_flags)
# Get the total count of regions; if there are any, add a setting that will
# turn on the hover listener; if not, remove the setting.
if len(folded) + len(unfolded) > 0:
view.settings().set(_setting_name, True)
else:
view.settings().erase(_setting_name)
def _intersect_region(new_region, regions, add_if_not_intersected):
"""
Update the list of regions given so that if any region overlaps the one
that is passed in, the region in the list is expanded to cover both the old
and new region.
In cases where no intersection happens, the function can also optionally
just append the region to the end of the list of regions; this only happens
The adjusted region list is returned back.
"""
intersected = False
result = []
for region in regions:
if region.intersects(new_region):
region = region.cover(new_region)
intersected = True
result.append(region)
# If the new region didn't intersect any existing ones, just add it in.
if not intersected and add_if_not_intersected:
result.append(new_region)
return result
class MarkFoldRegionCommand(sublime_plugin.TextCommand):
"""
As long as there is exactly one non-empty selection, add it as a custom
folding region by keeping a record of the selected text and marking it with
a gutter icon. If this is the first region added, turn on a hover listener
that will allow the user to manipulate the regions by hovering on them in
the gutter.
If the selected text overlaps an existing fold region, it will be combined
so that there is at one one fold region that exists on any one line.
"""
def run(self, edit):
# The fold region that we're going to add; the whole first selection
new_region = self.view.sel()[0]
# Get the existing folded and unfolded regions
folded, unfolded = _get_custom_regions(self.view)
# Add the new region into the list of regions; this will intersect any
# existing region with the new one (combining them into one if they
# overlap) so that any given line only ever covers a single region.
#
# In case of no overlap, the region will be added to the list of
# regions that are unfolded.
folded = _intersect_region(new_region, folded, False)
unfolded = _intersect_region(new_region, unfolded, True)
# Update the regions that are displayed based on our changes.
_set_custom_regions(self.view, folded, unfolded)
def is_enabled(self):
"""
The command is only enabled and executable when there is exactly 1
selection, and that selection is not empty.
"""
return len(self.view.sel()) == 1 and len(self.view.sel()[0]) > 0
class UnmarkFoldRegionCommand(sublime_plugin.TextCommand):
"""
Given a region start and end, remove that region from the list of stored
fold regions. If this is the last existing fold region, this also turns off
the hover listener. Once the removal is complete, this hides any visible
hover popup.
"""
def run(self, edit, begin, end):
folded, unfolded = _get_custom_regions(self.view)
removed_region = sublime.Region(begin, end)
folded.remove(removed_region)
unfolded.remove(removed_region)
_set_custom_regions(self.view, folded, unfolded)
self.view.hide_popup()
class HoverFolderCommand(sublime_plugin.TextCommand):
"""
Given a region start and end, either fold or unfold the region depending on
wether or not fold is true. Once the operation is complete, hide any
visible hover popup.
"""
def run(self, edit, begin, end, fold=True):
region = sublime.Region(begin, end)
folded, unfolded = _get_custom_regions(self.view)
# Flip the region between the two lists, depending on the action.
if fold:
unfolded.remove(region)
folded.append(region)
else:
folded.remove(region)
unfolded.append(region)
_set_custom_regions(self.view, folded, unfolded)
self.view.fold([region]) if fold else self.view.unfold([region])
self.view.hide_popup()
class CustomFoldControlEventListener(sublime_plugin.ViewEventListener):
"""
For any file that has at least one custom fold region in it as defined by
the mark_fold_region command, listen for a hover event on the gutter area
and provide an option to fold/unfold/remove the fold region at that
location.
"""
@classmethod
def is_applicable(cls, settings):
return settings.get(_setting_name, False)
def on_hover(self, point, hover_zone):
if hover_zone != sublime.HOVER_GUTTER:
return
# Get all regions and combine them into a single list for hover check
# purposes.
regions, unfolded = _get_custom_regions(self.view)
regions.extend(unfolded)
# The hover point is the start of a line; since we can only ever have
# at most one folded region per line, make a region that covers the
# whole line so that we can easily see which region intersects us, then
# check against all regions.
hover_line = self.view.full_line(point)
for region in regions:
# Does the current line intersect with this region?
if region.intersects(hover_line):
# We're either going to fold or unfold based on the current
# state of the region.
fold = not self.view.is_folded(region)
# A command to either fold or unfold the region, based on the
# current state.
fold_text = "Fold" if fold else "Unfold"
fold_cmd = sublime.command_url('hover_folder', {
'begin': region.a,
'end': region.b,
'fold': fold
})
# A command to remove this fold region
remove_text = "Remove"
remove_cmd = sublime.command_url('unmark_fold_region', {
'begin': region.a,
'end': region.b
})
# Set up the popup content, then show the popup in the window;
# when any of the commands is executed, the popup will close in
# response (it also closes if you move away from it with the
# mouse).
popup_contents = f"""<a href="{fold_cmd}">{fold_text}</a> |
<a href="{remove_cmd}">{remove_text}</a>"""
self.view.show_popup(popup_contents,
flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY,
location=point)
@OdatNurd
Copy link
Author

This is an example of a simple plugin that can be used to mark arbitrary parts of a file as fold regions, and then use gutter controls to fold and unfold that text.

This works by using Sublime's internal region api, which allows you mark one or more regions of text. Normally these would be marked with a background color, an outline, an underline, etc (linters use this, for example) but here it's used to store areas that are interesting for folding.

In use, you would use the command either from a key binding or from the context menu by first marking a set of text that you want to fold by selecting it, and then invoking the command. The command will disable itself if there is more than one selection, or if the first selection is empty (i.e. just a cursor).

The first line of every area that has a saved fold region in it will be marked with an icon in the gutter that looks like a fold arrow; hovering over the arrow will show a popup with options that allow you to either fold/unfold that text, or remove that region if you no longer need it.

The marked regions are persisted in the session storage information, so if you use this in a project you can close and reopen that project, or close and reopen Sublime in general (if hot_exit is turned on) and have the fold regions persist. They will NOT be saved if you close the file; for that the plugin would need to be extended to save the regions to disk and load them again.

To use this, place most of the files in your User package (Preferences > Browse Packages) and you should be good to go. The one file you don't want to place is the sublime-keymap file. For that, use Prefernces > Settings - Key BIndings from the menu and copy the binding into the right hand pane, adjusting as you would like. Copying the file directly will also work, though it will make the key binding active across all platforms that Sublime supports, which may or may not be desirable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment