Sublime Text Plugin Explorations: Markdown Link Manager

August 31, 2012 by Gabe | [mmd] |

I've been tinkering with some more Sublime Text plugin development. My first introduction can be found here.1 This work was inspired by Brett's MarkdownEditing plugin.

For a demo of what the plugin does, here's a little video:

I created these by reading the API documentation, the command documentation and browsing the forum. In addition, I examined the code of many, many other plugins for hints as to what the hell the first three meant.

list_links

The "list_links" command shows a list of the current end references and footnotes. Selecting one inserts the marker to the right of the current selection. Selecting "Add New" opens the input panel where I type a reference or footnote. The new reference or footnote is added to the end of my document and the marker is dropped to the right of my current selection. I bound this to ctrl+opt+⌘+l (for "list").

Here's the code for the plugin:

import sublime, sublime_plugin

class ListLinksCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.markers = ['Add New']
        self.startloc = self.view.sel()[-1].end()
        self.view.find_all("(^\\[.*?\\]:) (.*)", 0, "$1 $2", self.markers)
        self.view.window().show_quick_panel(self.markers, self.insert_link, sublime.MONOSPACE_FONT)

    def insert_link(self, choice):
        if choice == -1:
            return
        if choice == 0:
            self.view.window().show_input_panel("Create New Link", "", self.create_link, None, None)
        else:
            edit = self.view.begin_edit()
            link = self.markers[choice].split(':')[0]
            self.view.insert(edit, self.startloc, link)
            self.view.end_edit(edit)

    def create_link(self, new_link):
        l = re.compile('(^\\[.*?\\]):')
        m = l.match(new_link)
        if m:
            new_marker = new_link.split(':', 1)[0].strip()
            new_url = new_link.split(':', 1)[1].strip()
            edit = self.view.begin_edit()
            self.view.insert(edit, self.startloc, new_marker)
            self.view.insert(edit, self.view.size(), '\n\n'+new_marker+': '+new_url)
            self.view.end_edit(edit)

A couple things of note:

I changed the regex for matching on links. I no longer look for a properly formatted URL. I often create a placeholder reference with just a note about what I want to add. I don't want to stop writing to look anything up. I just know I want a reference at that point.

I've added a new command to the list of links, "Add New". When this is selected, the "insert_link" method is called. Since it is always the first item in the list, it passes "if choice == 0" and falls into the "create_link" method.

The method "create_link" exist just to add a reference or footnote to the end of the document.

There's some basic Python regex matching at the beginning, using the re module. I try to properly format the end reference by stripping unnecessary spaces from the beginning and ends. The nice trick for inserting at the end of the document without scrolling is this line:

self.view.insert(edit, self.view.size(), '\n\n'+new_marker+': '+new_url)

The "insert" method takes an "edit" object, a location and the string to insert. In this case, I'm using the total character size of the document ("self.view.size()") as the insert location. I also add two newlines before the new reference. I like to space things out for clarity.

goto_reference

I created an additional script in the plugin folder that adds a "goto_reference" command. This command presents the quick panel with a list of end references and footnotes. Selecting one creates a bookmark in the current location and then selects the reference at the end of the document and then centers on the new selection, ready for editing.

I bound this command to ctrl+opt+⌘+f (for "find").

import sublime, sublime_plugin, re

class GotoReferenceCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.linkRef = []
        self.view.find_all("(^\\[.*?\\]:) (.*)", 0, "$1 $2", self.linkRef)
        self.view.window().show_quick_panel(self.linkRef, self.jump_to_link, sublime.MONOSPACE_FONT)

    def jump_to_link(self, choice):
        if choice == -1:
            return
        # Set a bookmark so we can easily jump back
        self.view.run_command('toggle_bookmark')
        findmarker = self.linkRef[choice].split(':', 1)[1].strip()
        if len(findmarker) == 0:
            findmarker = self.linkRef[choice].split(':', 1)[0].strip()
            pt = self.view.find(re.escape(findmarker+':'), 0)
        else:
            pt = self.view.find(re.escape(findmarker), 0)
        self.view.sel().clear()
        # Get the selection    
        self.view.sel().add(pt)
        # Scroll the window to the new selection
        self.view.show_at_center(pt)

The goal of this script is to help me maneuver between my end references and footnotes. I wanted to experiment with using the find command for making selections as well as scrolling the window.

There's not much new in the run method. It uses the "find_all" method to locate anything that looks like an end reference or footnote and then show a list to select from.

In the "jump_to_link" method I first set a bookmark because I'm going to be moving the selection. If I want to get back to where I was, I figured that a bookmark was the best way to do it.

This bit of code is to handle end references that may not have a definition:

findmarker = self.linkRef[choice].split(':', 1)[1].strip()
if len(findmarker) == 0:
    findmarker = self.linkRef[choice].split(':', 1)[0].strip()
    pt = self.view.find(re.escape(findmarker+':'), 0)
else:
    pt = self.view.find(re.escape(findmarker), 0)

The first link splits the choice from the dropdown at the colon and then checks to see if there is any text after it. If the length is zero then there was no definition and we need to get the marker instead.

Next, I use the "view.find" method to locate either the first marker that matches or the first definition that matches and save that as a selection point.

Finally, I clear any selections that already exist and then add our point from the "find" method as a selection. The "self.view.show_at_center(pt)" call tells the current view to center on the same point which causes the window to scroll if necessary.

Conclusion

I'm pretty happy with the end result. I learned how to create a selection, use the input panel and scroll the window. I also have some really nice tools for working in Markdown.

When I think the collection is close to complete, I'll release it as a plugin through GitHub. For now, I'm not ready to support it in the current state. Feel free to use the code if you like.


  1. These are partly to explore Sublime Text and partly to make tools I really like 

blog comments powered by Disqus