Sublime Forum

Adding folding support to a new syntax

#1

Hello,

Context:

  • I am new the Sublime.
  • I am writing a graphic novel using the Fountain syntax[1] - I use the section '## ’ string to specify pages and I use the scenes as panels.
  • I have added the necessary resources to my user folder in order to get the proper syntax highlighting.
  • I have a working plugin in place to add a few features I need, such as presenting a drop down list with my page headers or returning page headers based on various criteria.

Questions:

  • (main) The one thing I would really like in the short term would be to have the gutter show the folding triangle for my pages. While doing my research on syntax highlighting and plugins, I have not found yet a document that explains how to do that. Is there an obvious document I have missed?
  • (secondary) I have seen a few threads on the subject but have had to use globals to share data between various plugin commands. I also got errors trying to use a constructor. Are there a few recommended threads I should read on the subject?
    Regards,
    Philippe

PS: I want to thank the developers for the plugin and syntax highlighting mechanisms: very very nice and flexible: the reason I am now a customer:)

[1]https://fountain.io

0 Likes

#2

There’s not a direct API related way to control the fold arrows in the gutter (other than by adjusting the settings that turn them on or off).

At a user level, folding is controlled by this setting:

	// The style of code folding to use
	// - "auto": Use whatever the default is for the syntax in use
	// - "force_indentation": Always use indentation-based code folding
	// - "scope_only": Only allow syntax-based code folding
	"fold_style": "auto",

When the value of this is force_indentation, or when it is auto but the metadata that supplies scope based folding is missing, then the folding arrows that appear in the gutter appear automatically and are controlled by a change in indentation level.

Conversely, when fold_style is scope_only, or when it’s auto and there is metadata to supply scope based folding rules, fold indicators will appear in the gutter at positions where folding can start.

The metadata that you want is something that would be stored in a tmPreferences file. As an example, you can use View Package File from the command palette and use the filter markdown fold to find and open the Markdown/Fold.tmPreferences file.

In a nutshell, the file contains:

  • A scope that specifies to what syntax the metadata inside affects
  • A foldScopes inside of settings to indicate that the metadata to be added is fold scopes
  • An array of 1 or more sets of fold scopes that include:
    • The scope that will begin the fold area
    • The scope that will end the fold area
    • Whether or not the trailing newline on the line that contains the end scope will also be included in the fold

For your own syntax definition you would need to create such a file (the name of the file itself does not matter, only the tmPreferences extension does, but the convention is to name it Fold to remind you what it’s doing) that specifies your custom syntax scope and the locations where it should begin and end folding.

Generally speaking globals should work for this, though you have to be wary of the fact that whenever your plugin .py file reloads, the value of the global will reset, but anything holding a reference to it that was not also reloaded may be holding onto the old value.

The other way to share state between different commands would be settings (for example view settings or window settings), which have the benefit of being persisted in the session file so that their values will remain in place even if you quit and restart Sublime (assuming you’re using hot_exit). The caveat there is that you can only store basic types as settings (anything that can be converted to json, essentially).

I don’t know of any threads off the top of my head about the topic in general, but if you provide an example of what you were trying to do that didn’t work, I’m sure we can work out what went wrong and get you onto the right track.

1 Like

#3

@OdatNurd,
Many many thanks for your response. (an thanks for your videos)

  1. I will need to take some time and do my homework:)

The upload option does not allow me to upload my plugin so I am pasting my current code where the idea is to find a way to get rid of the g_variables.
They are needed to share data not only between two commands but also between two methods of the same command/class. When I tried to add a constructor in there to have an instance variable, I got something like “cannot initialize”. I also tried a class static but got another error if I remember correctly.

I got it to work but wish I could find a cleaner way.

These two commands work together to:

  1. allow me to choose from a list of search criteria from a text file
  2. run the search and give me another list of matching novel page titles to choose from.

That way I can easily have tags in my script and track plots, arcs, pace …

...
g_search_criteria = None
g_searches = None
....
# ############################################################

class FoundPcmCommand(sublime_plugin.TextCommand):

    def run(self, edit, item=None):
        global g_search_criteria
        o = Script()
        o.Extract()

        print('FoundPcmCommand', g_search_criteria)
        items = o.Find(g_search_criteria)   
        #print(items)
        if item:
            self.view.run_command("insert", {"Headers": item})
        else:
            self.view.window().show_quick_panel(
                items,
                lambda idx: self.pick(idx, items))

    def pick(self, index, items):

        if index >= 0:
            view  = self.view.window().open_file(SCRIPT_PATH)
            self.view.window().focus_view(view)

            #print('Searching for ', items[index])
            res = self.view.find(items[index], 0)
            #print('Location in file',res)
            self.view.show_at_center(res)
            self.view.sel().clear()
            self.view.sel().add(res)

# ############################################################
class FindPcmCommand(sublime_plugin.TextCommand):



    def run(self, edit, item=None):
        #o = Script()
        #o.Extract()
        global g_searches
        with open (SEARCHES, "rt") as f:
            lines = f.readlines()
        items = []
        g_searches = {}
        for line in lines:
            if len(line) > 1:
                name, search = line.split(':')
                name = name.strip()
                search = search.strip()
                print("Criteria", name, "==>", search)
                items.append(name)
                g_searches[name] = search
        if item:
            self.view.run_command("insert", {"Headers": item})
        else:
            self.view.window().show_quick_panel(
                items,
                lambda idx: self.pick(idx, items))

    def pick(self, index, items):

        global g_search_criteria
        if index >= 0:
            g_search_criteria = g_searches[items[index]]
            print('Run found_pcm', g_search_criteria)
            self.view.run_command("found_pcm")
0 Likes

#4

If you add an __init__ to one of the command classes, you need to make sure that your version has the correct argument list and chains to the internal constructor; if you were not doing that, that would be a problem. For example:

class MyCommand(sublime_plugin.TextCommand):
    def __init__(self, view):
        super().__init__(view)
        # custom member init here

That would allow you to have an instance variable that methods within the class could see. Note however that for a TextCommand, one instance of the class is created for every view (e.g. every open file gets its own distinct instance of the command class).

That’s usually what you want, so that what you do in one file does not directly influence what the same command does in another file, but if you want that to be the case, then you need to use a different mechanism.

The whole idea of a distinct instance of the class per view also means that if you were to use an instance variable, you can’t access it from another command because there is no way for you to find the distinct instance of the class that was created for the same view as you (if that makes sense).

If you wanted to, for example, share a string or an array of strings between multiple commands, you could use view settings.

If you do something like:

self.view.settings().set("_my_setting", "Text Here")

Then any command can use:

self.view.settings().get("_my_setting")

to get the value of that setting.

In the case of what it seems like your plugin is doing above, that would allow you to use the command in multiple files and have them be distinct.

Additionally if you have hot_exit turned on (which it is by default), then quitting Sublime or closing a project where you’ve used your command will make the view settings persist in the session/workspace and come back later. Closing the file removes the view settings.

You can do a similar thing with window settings, in which case your setting would be available to all tabs that share the same window.

0 Likes

#5

@OdatNurd
Thanks again,

I thought I had tried the param in the constructor but I obviously did something wrong.

Will definitely try the settings mechanism.

Regards,
Philippe

0 Likes