Sublime Forum

Snippet with dynamic character length

#1

Hello. LOVE sublime, I swear by it. I’m trying to create a snippet that triggers a comment with a dynamic character length. It would like something like this:

/** [[START]] Section title ------------------------------------------------------------------------
 *  This is my section.
 */

I want the first line to be exactly “x” characters, where Section title is a triggered field with a dynamic length. How can I dynamically adjust the number of - characters so the total length of the first line is, for example, 100 characters?

I appreciate any help… I have looked far and wide for an answer to this.

0 Likes

#2

How is the snipped you tried so far?

Also I do not think it is possible, but you could create a plugin for Sublime Text which performs as a snippet.

0 Likes

#3

I tried making this purely with a snippet, but couldn’t get it to work. You can, however, do this with a simple plugin. Go to Tools :arrow_right: Developer :arrow_right: New Plugin…, and paste the following:

import sublime_plugin


class InsertHeaderCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        rulers = self.view.settings().get("rulers", [])
        maxlength = min(rulers) if rulers else 80
        if len(self.view.sel()) > 1:
            print("This doesn't work with more than one selection :(")
            return
        region = self.view.sel()[0]
        # If we have an empty region, assume we want to convert the whole line.
        if region.empty():
            region = self.view.line(region.begin())
        print('handling', self.view.substr(region), region)
        # -4 for '/** '
        # -1 for the space between the region and the start of the dashes.
        # Totals -5.
        dashlength = maxlength - region.size() - 5
        if dashlength < 0:
            print("Region is too large:", self.view.substr(region))
            return
        string = "/** {} {}\n * $0\n */".format(self.view.substr(region),
                                                "-" * dashlength)
        self.view.erase(edit, region)
        self.view.run_command("insert_snippet", {"contents": string})

Save this file in your Packages/User directory: just hit “Save” and you’ll be prompted to the correct folder.
Now open your keybindings file by going to Preferences :arrow_right: Key Bindings, and in the right view, paste the following:

{ "keys": ["ctrl+alt+i"], "command": "insert_header" },  // or any other key-combination

You might want to use a context mask to mask it for the appropriate scope.

3 Likes

#4

This is great, thank you for posting this!!! I haven’t tried it yet, but will get around to it. I don’t know python, so thank you for the comments as well; should be enough to help me navigate through it. Again, many thanks!

0 Likes

#5

So I added this plugin… copied verbatum, as well as the keyboard shortcut. When hit the shortcut, the console verifies it’s running the insert_header command, but nothing is happening. I don’t see anywhere in the code it’s mapping insert_header to an actual command; does it map it from the class name? What might I be doing wrong?

A separate question: How would I be able to get this to act like a snippet by being automatically inserted when I type certain keys such as /**-<tab>?

0 Likes

#6

When hit the shortcut, the console verifies it’s running the insert_header command, but nothing is happening.

The way you use this is you select some text, or you put your cursor on a line with some text, and you hit the keybinding.

What is the output of the console?

A separate question: How would I be able to get this to act like a snippet by being automatically inserted when I type certain keys such as /**-?

That’s going to be harder, because we don’t know beforehand how many dashes we should insert.

0 Likes

#7

When you want to reference a command in a keybinding, you use the snake_case name of that command, without the “Command” suffix. So, for instance, FoorBarCommand would be foo_bar, and BarBazQuxCommand would be bar_baz_qux.

0 Likes

#8

I figured that must be the case.

The console output simply shows:

command: move_to {"to": "eol", "extend":true}
command: insert_header

So the command is running with text selected, but it appears the plugin is not. Do I need to save the plugin anywhere specific? I have mine saved in Packages/User/plugins/InsertHeaderCommand.py


I do appreciate your help. Ideally, I’d like this to work like a snippet; something like:

  1. Type some snippet text (i.e., /***<tab>)
  2. Have the placeholder text to allow me to type the comment title.
  3. After tabbing out of the title, dynamically adding the number of dashes needed to fill the line.

OR

  1. Type the snippet text.
  2. Have the placeholder snippet show all dashes to fill the line (e.g., 80 dashes minus 14 to account for the /** [[START]] text; THEN…
  3. …each time a character is typed in the title field a single dash is removed.

If you or anybody else has any more ideas or thoughts on how to accomplish such, I would appreciate that! :slight_smile:

0 Likes

#9

You cannot put the plugin on one than one level deeper on the Packages folder. You plugin must be at most on:

  1. Packages/User/InsertHeaderCommand.py
  2. or Packages/InsertHeaderCommand.py

Otherwise Sublime text will not load it.

1 Like

#10

Theoretically you could bind the command to a fake snippet by having this kind of listener:

    class SnippetListener(sublime_plugin.EventListener):

        def on_text_command(self, view, command_name, args):

            if command_name == 'insert_snippet' and args == {"contents": "your_fake_snippet"}:
                return "your_command"

Make a fake snippet with the trigger you prefer and the content that must be matched by the listener, the command should be run. Just theory btw, I didn’t try it myself.

Edit: just tried it and it doesn’t work, commit_completion doesn’t run the insert_snippet command. You could in theory check for commit_completion, trace back the inserted characters and if they match a string, run a command, but it’s a bit more complicated than that.

0 Likes

#11

I don’t think it’s true. Plugins deeper than that are loaded at startup, just not refreshed if you save them again.

I don’t think you should put plugins there either.

0 Likes

#12

Thank you. The plugin now runs correctly after I moved it to Packages/User/InsertHeaderCommand.py.


Thank you for the thoughts. This seems a lot more difficult than I’d like it to be. I swear a couple years ago I found a nifty snippet that did just what I’m looking for, but for the life of me I can’t seem to find it anymore. When I have some more time I’ll give it another go. If anybody has more ideas, please pitch in. I will update this thread with my complete solution if I’m able to work it out.

0 Likes

#13

I opened a issue on the Sublime Text GitHub issue tracker asking for this feature, you can up vote it:

  1. https://github.com/SublimeTextIssues/Core/issues/2077 Allow to create completions which call back Sublime Text Command

Then you can just write this plugin, and it will do the autocompletion by calling @rwols command:

class InsertHeaderCommand(sublime_plugin.TextCommand):

    def on_query_completions(self, view, prefix, locations):
        return ['/**', self]

    def run(self, edit):
    """
        Snippet with dynamic character length
        https://forum.sublimetext.com/t/snippet-with-dynamic-character-length/33601
    """
        rulers = self.view.settings().get("rulers", [])
        maxlength = min(rulers) if rulers else 80
        if len(self.view.sel()) > 1:
            print("This doesn't work with more than one selection :(")
            return
        region = self.view.sel()[0]
        # If we have an empty region, assume we want to convert the whole line.
        if region.empty():
            region = self.view.line(region.begin())
        print('handling', self.view.substr(region), region)
        # -4 for '/** '
        # -1 for the space between the region and the start of the dashes.
        # Totals -5.
        dashlength = maxlength - region.size() - 5
        if dashlength < 0:
            print("Region is too large:", self.view.substr(region))
            return
        string = "/** {} {}\n * $0\n */".format(self.view.substr(region),
                                                "-" * dashlength)
        self.view.erase(edit, region)
        self.view.run_command("insert_snippet", {"contents": string})
0 Likes

#14

You are correct, however if you finish reading the phrase, you will see

Otherwise Sublime text will not load it.

deeper than that you must load they from some of the plugins Sublime Text has already loaded. You can use this for the job:

def reload_package(full_module_name):
    import imp
    import sys
    import importlib

    if full_module_name in sys.modules:
        module_object = sys.modules[full_module_name]
        module_object = imp.reload( module_object )

    else:
        importlib.import_module( full_module_name )

reload_package( "User.plugins.InsertHeaderCommand" )
0 Likes

#18

Here’s another method that you can use to get this to work in a more seamless manner and without anything else third party installed. This method still leaves a bit to be desired as a general purpose solution though (see last paragraph), although it could be tweaked further.

First, here is a different plugin, which is a modification of the one that @rwols wrote above (all props to him for the core logic):

import sublime
import sublime_plugin


class InsertHeaderCommand(sublime_plugin.TextCommand):
    def run(self, edit, trigger):
        rulers = self.view.settings().get("rulers", [])
        maxlength = min(rulers) if rulers else 80

        # Get the caret location and the whole line the caret is on
        caret = self.view.sel()[0]
        line = self.view.line(caret.begin())

        # Find the region that the trigger text is contained in
        t_pos = self.view.find(trigger, line.begin(), sublime.LITERAL)

        # Everything from the end of the trigger position to the caret
        # position is the text to put into the header
        v_text = self.view.substr(sublime.Region(t_pos.end(), caret.begin()))

        # Calculate the dash length; we subtract one extra because in the
        # replacement text below we force a space between the text and the
        # dash line
        dashlength = maxlength - len(v_text) - len(trigger) - 1

        if dashlength < 0:
            return sublime.status_message("Line too long to insert header")

        string = "{}{} {}\n* $0\n*/".format(trigger, v_text,
                                            "-" * dashlength)

        # Erase from the start of the trigger all the way to the cursor
        # to make way, the insert the snippet
        self.view.erase(edit, t_pos.cover(caret))
        self.view.run_command("insert_snippet", {"contents": string})

With that in place, we then add this key binding:

{
    "keys": ["tab"], "command": "insert_header",
    "args": {
        "trigger": "/**"
    },

    "context": [
        { "key": "selector", "operator": "equal", "operand": "(source.c, source.c++) & comment"},
        { "key": "preceding_text", "operator": "regex_contains", "operand": "/\\*\\*"},
        { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true },
        { "key": "num_selections", "operator": "equal", "operand": 1}
    ]
},

Now you can enter /** followed by some text, press Tab and the “snippet” expands out in a more natural manner:

What this does is first implement a command that presupposes that there is only a single caret and no selection, and that somewhere on the line the cursor is currently on there is some text whose value is given by the argument trigger that appears before the cursor.

The command then finds where on the current line the first instance of the trigger text is. Everything from that point forward is the text of the header, and erases both parts and inserts a snippet with a dynamically calculated dashed line.

The key binding contains contexts that ensure that all of the preconditions on the command are met, partly so that the command doesn’t have to be smarter and do more checks but also because stealing the Tab key is something you should only do in very specific instances.

To this end, something important to keep in mind is that without that first context that is constraining the locations the expansion can happen in (here in a C or C++ file while inside of a comment) this may trigger in cases where you don’t expect it to.

The downside of this that makes it not great for a general purpose solution is that the key binding needs a regex_contains that includes the trigger text, and the command also needs to be told what the trigger is, so you have to modify both of them.

3 Likes

#19

Reposting my old answer after deleting it, based on @rwols code…

Due a bug on Sublime Text, you cannot create completion with special characters as /**:

  1. https://github.com/SublimeTextIssues/Core/issues/140 .sublime-completions problems with triggers starting with backslash and containing curly brace
  2. https://github.com/SublimeTextIssues/Core/issues/156 Can’t start a snippet with special characters :
  3. https://github.com/SublimeTextIssues/Core/issues/456 Completions thwarted by ‘-’ character
  4. https://github.com/SublimeTextIssues/Core/issues/819 on_query_completions fails to return custom completions when some characters are used
  5. https://github.com/SublimeTextIssues/Core/issues/1061 Completion triggers with characters not in [a-zA-Z0-9_-] prevent buffer completions
  6. https://github.com/SublimeTextIssues/Core/issues/1166 Clicking on an autocomplete entry does not fire the commit_completion command

Then the command you need to trigger you command must be some word. Then I choose the word:

starcommentsnippet

You need to install another package to this work:

  1. https://github.com/evandrocoan/SublimeOverrideCommitCompletion Overwrite Commit Completion

To install it you can run the following command on your Packages folder:

git clone https://github.com/evandrocoan/SublimeOverrideCommitCompletion OverrideCommitCompletion

Then you can create a plugin which loads the completion for you inside the folder:

Packages/OverrideCommitCompletion/dynamic_character_lenght.py

import sublime
import sublime_plugin
from .overwrite_commit_completion import add_function_word_callback

starcomment_trigger = 'starcommentsnippet'

def dynamic_character_lenght(view, edit):
    """
        Snippet with dynamic character length
        https://forum.sublimetext.com/t/snippet-with-dynamic-character-length/33601
    """
    rulers = view.settings().get("rulers", [])
    maxlength = min(rulers) if rulers else 80
    if len(view.sel()) > 1:
        print("This doesn't work with more than one selection :(")
        return
    region = view.sel()[0]
    # If we have an empty region, assume we want to convert the whole line.
    if region.empty():
        region = view.line(region.begin())
    print('handling', view.substr(region), region)
    # -4 for '/** '
    # -1 for the space between the region and the start of the dashes.
    # Totals -5.
    dashlength = maxlength - region.size() - 5
    if dashlength < 0:
        print("Region is too large:", view.substr(region))
        return
    string = "/** {} {}\n * $0\n */".format(view.substr(region),
                                            "-" * dashlength)
    view.erase(edit, region)
    view.run_command("insert_snippet", {"contents": string})

    return len( string )

class InsertHeaderCompletionCommand(sublime_plugin.TextCommand):

    def on_query_completions(self, view, prefix, locations):
        return [(starcomment_trigger + ' \tsnippet', starcomment_trigger)]

def plugin_loaded():
    add_function_word_callback( starcomment_trigger, dynamic_character_lenght )
0 Likes

#20

You could associate a custom context key (eg, key “insert_header”), then have on_query_context look for the trigger at caret position, without needing to specify the regex in the context itself.

0 Likes