Building Plugins for LightTable: A Step-by-Step TutorialLightTable is a lightweight, extensible code editor designed with real-time feedback, tight integration with languages and runtimes, and a plugin-friendly architecture. This tutorial walks you through creating a functional LightTable plugin from idea to publication, covering project setup, core APIs, UI integration, state management, and best practices.
What you’ll build
We’ll create a sample plugin called “lt-todos” that lets you attach persistent TODO notes to specific lines in files. Features:
- Add a TODO to the current line
- List all TODOs in the current project
- Jump to a TODO from the list
- Persist TODOs across sessions
This example demonstrates buffer interaction, commands, UI panes, and workspace persistence.
Prerequisites
- Basic knowledge of JavaScript (or ClojureScript; LightTable plugins often use JavaScript)
- LightTable installed (version 0.8.x or similar)
- Familiarity with Node (optional) and Git for packaging/publishing
Project structure
Create a folder named lt-todos with this structure:
lt-todos/ ├─ main.js ├─ package.json ├─ lt-plugin.json ├─ README.md ├─ styles.css └─ icons/ └─ todo-icon.png
- lt-plugin.json describes the plugin to LightTable.
- main.js contains plugin code.
- package.json for npm publishing (optional).
- styles.css for any UI styling.
lt-plugin.json
Create lt-plugin.json to register your plugin and expose commands:
{ "name": "lt-todos", "version": "0.1.0", "description": "Attach persistent TODOs to lines in LightTable.", "keywords": ["lighttable", "todo", "plugin"], "main": "main.js", "lt": { "type": "plugin", "dependencies": [] } }
Bootstrapping the plugin (main.js)
LightTable plugins register behaviors, commands, and watchers via the lt.plugins API. Below is a scaffolded main.js with comments explaining each section.
// main.js ;(function() { var lt = (function() { try { return window.lt; } catch(e) { return null; } })(); if(!lt) { console.error("LightTable global 'lt' not found."); return; } var docs = lt.objs ? lt.objs : lt.doc; var cmds = lt.commands; var workspace = lt.workspace; var stor = lt.storage; // Namespace for our plugin var ns = "lt-todos"; // Utility: generate key for storage function storeKey() { return ns + ":todos"; } // Load existing todos from storage function loadTodos() { try { return stor.get(storeKey()) || {}; } catch(e) { return {}; } } // Save todos function saveTodos(todos) { stor.set(storeKey(), todos); } // Add a TODO for the current line in the active editor function addTodo() { var ed = workspace.getActiveEditor(); if (!ed) { return; } var path = ed.getSession().$filePath || ed.getDoc().fileName || "untitled"; var row = ed.getCursorPosition().row; var todos = loadTodos(); if (!todos[path]) todos[path] = []; todos[path].push({row: row, text: "TODO"}); saveTodos(todos); showStatus("TODO added at line " + (row + 1)); refreshTodoMarkers(ed); } // List all TODOs in project function listTodos() { var todos = loadTodos(); var items = []; Object.keys(todos).forEach(function(path) { todos[path].forEach(function(t, i) { items.push({path: path, row: t.row, text: t.text}); }); }); // Create a simple popup list var html = document.createElement("div"); html.className = "lt-todos-list"; items.forEach(function(it, idx) { var el = document.createElement("div"); el.className = "lt-todo-item"; el.textContent = it.path + " : " + (it.row + 1) + " — " + it.text; el.onclick = function() { jumpTo(it.path, it.row); }; html.appendChild(el); }); workspace.addModal(html, {title: "Project TODOs"}); } function jumpTo(path, row) { workspace.openFile(path, function(ed) { ed.setCursorPosition({row: row, column: 0}); ed.focus(); }); } function showStatus(msg) { var s = document.createElement("div"); s.className = "lt-todos-status"; s.textContent = msg; document.body.appendChild(s); setTimeout(function() { s.parentNode && s.parentNode.removeChild(s); }, 3000); } // Add visual markers for todos in an editor function refreshTodoMarkers(ed) { if(!ed) return; var path = ed.getSession().$filePath || ed.getDoc().fileName || "untitled"; var todos = loadTodos(); var marks = (ed.__lt_todo_marks || []); // Clear old markers marks.forEach(function(m) { try { ed.getSession().removeMarker(m); } catch(e) {} }); ed.__lt_todo_marks = []; if (!todos[path]) return; todos[path].forEach(function(t) { var Range = ace.require('ace/range').Range; var range = new Range(t.row, 0, t.row, 1); var markerId = ed.getSession().addMarker(range, "lt-todo-marker", "fullLine"); ed.__lt_todo_marks.push(markerId); }); } // Hook into editor open/change events to refresh markers lt.objs.watch(function(obj, old, key) { try { if (obj && obj.getSession && obj.getSession().on) { var ed = obj; ed.getSession().on('change', function() { refreshTodoMarkers(ed); }); refreshTodoMarkers(ed); } } catch(e) {} }); // Register commands cmds.register("lt-todos:add-todo", addTodo, "Add TODO at current line"); cmds.register("lt-todos:list-todos", listTodos, "List all TODOs"); // Default keybindings cmds.addKeybinding("lt-todos:add-todo", "Ctrl-Alt-T"); cmds.addKeybinding("lt-todos:list-todos", "Ctrl-Alt-L"); // Styles var style = document.createElement("style"); style.textContent = ".lt-todo-marker { background: rgba(255,200,0,0.2); } .lt-todos-list { max-height: 400px; overflow: auto; padding:10px; } .lt-todo-item { padding:6px; border-bottom:1px solid #eee; cursor:pointer; } .lt-todos-status { position:fixed; bottom:10px; right:10px; background:#333; color:#fff; padding:6px 10px; border-radius:4px; opacity:0.9; }"; document.head.appendChild(style); // Expose for debugging window.lt_todos = { addTodo: addTodo, listTodos: listTodos, loadTodos: loadTodos, saveTodos: saveTodos }; })();
Key APIs explained
- workspace.getActiveEditor() — returns the active editor object.
- editor.getSession() / addMarker / removeMarker — manage visual markers (uses Ace editor API).
- lt.storage — persistent key/value storage for plugins.
- lt.commands.register / addKeybinding — register commands and keyboard shortcuts.
- workspace.openFile — open a file and focus editor.
- lt.objs.watch — observe object creation (editors) to attach behavior.
Persistence format
We store TODOs as a map keyed by file path:
{ "src/main.js": [{ "row": 12, "text": "TODO" }, ...], "README.md": [{ "row": 3, "text": "Refactor" }] }
Enhancements and improvements
- Allow editing/removing TODO text.
- Use file URIs instead of paths for better uniqueness.
- Show inline widgets (editable) instead of markers.
- Fast search integration (index todos for quick lookup).
- Sync across machines via external storage.
Packaging and publishing
- Ensure lt-plugin.json fields are correct.
- Publish to npm if desired (npm init, npm publish).
- Share on LightTable community channels or GitHub.
Troubleshooting
- Markers not showing: ensure ACE Range is available (ace.require(‘ace/range’)) and editor sessions exist.
- Storage not persisting: confirm lt.storage API is present and keys are correct.
- Keybinding conflicts: choose unique combos or make configurable.
Conclusion
This tutorial covered building a practical LightTable plugin from setup to features and deployment. The lt-todos example demonstrates editor integration, persistent storage, UI listing, and navigation—core patterns useful across many plugin types.
Leave a Reply