Blog posts for tags/sublime

  1. Sublime Text: remember cursor position plugin

    My editor of choice for Python development is Sublime Text. It has been for a very long time (10 years). It's fast, minimalist and straight to the point, which is why I always resisted the temptation to use more advanced and modern IDEs such as PyCharm or VS code, which admittedly have superior auto-completion and refactoring tools.

    There is a very simple feature I've always missed in ST: the possibility to "remember" / save the cursor position when a file is closed. The only plugin promising to do such a thing is called BufferScroll, but for some reason it ceased working for me at some point. I spent a considerable amount of time Googling for an alternative but, to my surprise, I couldn't find any plugin which implements such a simple feature. Therefore today I decided to bite the bullet and try to implement this myself, by writing my first ST plugin, which I paste below.

    What it does is this:

    • every time a file is closed, save the cursor position (x and y axis) to a JSON file
    • if that same file is re-opened, restore the cursor at that position

    What's neat about ST plugins is that they are just Python files which you can install by copying them in ST's config directory. On Linux you can copy the script below in:


    ...and will work out of the box. This is exactly the kind of minimalism which I love about ST, and which I've always missed in other IDEs.

    A plugin for SublimeText which saves (remembers) cursor position when
    a file is closed.
    Install it by copying this file in ~/.config/sublime-text-3/Packages/User/
    directory (Linux).
    Author: Giampaolo Rodola'
    License: MIT
    import datetime
    import json
    import os
    import tempfile
    import threading
    import sublime
    import sublime_plugin
    SUBLIME_ROOT = os.path.realpath(os.path.join(sublime.packages_path(), '..'))
    SESSION_FILE = os.path.join(
        SUBLIME_ROOT, "Local", "cursor_positions.session.json")
    # when reading the session file on startup, we'll remove entries
    # older than X days
    def log(*args):
        print("    %s: " % os.path.basename(__file__), end="")
    class Session:
        def __init__(self):
            self._lock = threading.Lock()
            os.makedirs(os.path.dirname(SESSION_FILE), exist_ok=True)
        # --- file
        def read_session_file(self):
                with self._lock:
                    with open(SESSION_FILE, "r") as f:
                        return json.load(f)
            except (FileNotFoundError, json.decoder.JSONDecodeError):
                return {}
        def write_session_file(self, d):
            # Use the same FS so that the move operation is atomic:
            with tempfile.NamedTemporaryFile(
                    "wt", delete=False, dir=os.path.dirname(SESSION_FILE)) as f:
                f.write(json.dumps(d, indent=4, sort_keys=True))
            with self._lock:
                os.rename(, SESSION_FILE)
        def prune_old_entries(self):
            old = self.read_session_file()
            new = old.copy()
            now =
            for file, entry in old.items():
                tstamp = entry["last_update"]
                last_update = datetime.datetime.strptime(
                    tstamp, '%Y-%m-%d %H:%M:%S.%f')
                delta_days = (now - last_update).days
                if delta_days > RM_FILE_OLDER_THAN_DAYS:
                    log("removing old saved file %r" % file)
                    del new[file]
            if new != old:
        # --- operations
        def add_entry(self, file, x, y):
            d = self.read_session_file()
            d[file] = dict(
        def load_entry(self, file):
            d = self.read_session_file()
                return d[file]
            except KeyError:
                return None
    session = Session()
    class Events(sublime_plugin.EventListener):
        # --- utils
        def get_cursor_pos(view):
            x, y = view.rowcol(view.sel()[0].begin())
            return x, y
        def set_cursor_pos(view, x, y):
            pt = view.text_point(x, y)
        def save_cursor_position(self, view):
            file_name = view.file_name()
            if file_name is None:
                return  # non-existent file
            log("saving cursor position for %s" % file_name)
            x, y = self.get_cursor_pos(view)
            session.add_entry(file_name, x, y)
        def load_cursor_position(self, view):
            entry = session.load_entry(view.file_name())
            if entry:
                self.set_cursor_pos(view, entry["x"], entry["y"])
        # --- callbacks
        def on_close(self, view):
            # called when a file is closed
        def on_load(self, view):
            # called when a file is opened