LightTable vs. Traditional IDEs: Which Is Right for You?

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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *