Last active
December 9, 2025 15:44
-
-
Save nakwa/b00ae372174981bf5fce673ef64ca75d to your computer and use it in GitHub Desktop.
Ask Claude Code - Simple Sublime Text Plugin for Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Ask Claude - Sublime Text Plugin | |
| This plugin integrates Claude Code CLI with Sublime Text, allowing you to send code | |
| selections directly to Claude for analysis, refactoring, debugging, or any other task. | |
| INSTALLATION: | |
| ------------- | |
| 1. Save this file as 'ask_claude.py' in your Sublime Text User package directory: | |
| macOS: ~/Library/Application Support/Sublime Text 3/Packages/User/ask_claude.py | |
| Quick access: Sublime Text > Preferences > Browse Packages > User folder | |
| 2. Restart Sublime Text or run "Plugin: Reload Plugin" from Command Palette | |
| REQUIREMENTS: | |
| ------------- | |
| 1. Terminus plugin for Sublime Text (https://packagecontrol.io/packages/Terminus) | |
| Install via Package Control: Command Palette > Package Control: Install Package > Terminus | |
| 2. Claude Code CLI (https://claude.com/claude-code) | |
| Install via: npm install -g claude-code | |
| KEYBOARD SHORTCUTS: | |
| ------------------- | |
| Add these to your Sublime keymap file (Preferences > Key Bindings): | |
| {"keys": ["command+shift+c"], "command": "ask_claude", "args": {"resume": false}} | |
| {"keys": ["command+shift+r"], "command": "ask_claude", "args": {"resume": true}} | |
| {"keys": ["shift+enter"], "command": "terminus_keypress", "args": {"key": "j", "ctrl": true}, "context": [{"key": "terminus_view"}]} | |
| USAGE: | |
| ------ | |
| 1. Cmd+Shift+C: Start NEW Claude session | |
| - Opens Claude in a new Terminus panel (creates split view if needed) | |
| - Sends selected code with file path and line numbers | |
| - If no text is selected, sends just the file path | |
| - Creates a dedicated Claude context directory per project (~/.sublime-claude/{project-id}/) | |
| 2. Cmd+Shift+R: RESUME existing Claude session | |
| - Continues your existing conversation with Claude | |
| - Automatically finds and reuses the Claude Terminus view if already open | |
| - Maintains conversation history and context | |
| - Ideal for iterative development workflows | |
| HOW IT WORKS: | |
| ------------- | |
| - Automatically creates a project-specific Claude context directory for each Sublime project | |
| - Grants Claude access to your project folders via additionalDirectories in settings.json | |
| - Auto-paste file path, lines and code selections into Claude | |
| - Opens Claude in an opposite pane for side-by-side workflow | |
| - Preserves conversation history when resuming sessions | |
| import sublime | |
| import sublime_plugin | |
| import uuid | |
| class AskClaudeCommand(sublime_plugin.TextCommand): | |
| """Send selected text or current line to Claude in Terminus""" | |
| def run(self, edit, resume=True): | |
| # Get the current file path | |
| file_path = self.view.file_name() or "untitled" | |
| # Get file extension for syntax highlighting | |
| import os | |
| file_ext = "" | |
| if file_path and file_path != "untitled": | |
| _, ext = os.path.splitext(file_path) | |
| file_ext = ext[1:] if ext else "" # Remove the dot | |
| # Get selected text | |
| selection = self.view.sel() | |
| selected_text = "" | |
| line_info = "" | |
| # Check if there's any actual text selected | |
| has_selection = False | |
| selected_regions = [] | |
| for region in selection: | |
| if not region.empty(): | |
| has_selection = True | |
| selected_regions.append(region) | |
| if has_selection and len(selected_regions) > 0: | |
| # Build message with selected text | |
| regions_text = [] | |
| for region in selected_regions: | |
| regions_text.append(self.view.substr(region)) | |
| selected_text = "\n".join(regions_text) | |
| # Get line numbers of first selection | |
| first_line = self.view.rowcol(selected_regions[0].begin())[0] + 1 | |
| last_line = self.view.rowcol(selected_regions[-1].end())[0] + 1 | |
| if first_line == last_line: | |
| line_info = "Line {}".format(first_line) | |
| else: | |
| line_info = "Lines {}-{}".format(first_line, last_line) | |
| # Remove trailing empty lines and add indentation to each line of code | |
| selected_text = selected_text.rstrip('\n') | |
| indented_text = "\n".join(" " + line for line in selected_text.split("\n")) | |
| # Format the message with code fence for syntax highlighting | |
| message = "# {}\n# {}\n\n```{}\n{}\n```\n\n".format( | |
| file_path, | |
| line_info, | |
| file_ext, | |
| indented_text | |
| ) | |
| else: | |
| # No selection - only send file path | |
| message = "# {}\n\n".format(file_path) | |
| # Check if there's already a Terminus view with Claude running | |
| terminus_view = self._find_claude_terminus_view() | |
| if terminus_view: | |
| print("AskClaude: Found existing terminus view: {}".format(terminus_view.name())) | |
| # Send text to existing Terminus | |
| self._send_to_terminus(terminus_view, message) | |
| else: | |
| print("AskClaude: No existing terminus view found, opening new one") | |
| # Open new Terminus with Claude and send message after delay | |
| self._open_new_terminus(message, resume=resume) | |
| def _find_claude_terminus_view(self): | |
| """Find an existing Terminus view running Claude""" | |
| window = self.view.window() | |
| if not window: | |
| return None | |
| for view in window.views(): | |
| # Check if it's a Terminus view | |
| if view.settings().get('terminus_view'): | |
| # Check if it has 'claude' in the title or command | |
| view_name = view.name() or "" | |
| # Also check the tag which might contain the command | |
| tag = view.settings().get('terminus_view.tag') or "" | |
| cmd = view.settings().get('terminus_view.cmd') or "" | |
| if 'claude' in view_name.lower() or 'claude' in tag.lower() or 'claude' in str(cmd).lower(): | |
| return view | |
| return None | |
| def _send_to_terminus(self, terminus_view, text): | |
| """Send text to an existing Terminus view""" | |
| window = self.view.window() | |
| if window: | |
| print("AskClaude: Focusing terminus view") | |
| # Focus the Terminus view | |
| window.focus_view(terminus_view) | |
| # Use sublime.set_timeout to ensure the view is focused before sending | |
| def send_text(): | |
| print("AskClaude: Clearing input buffer with Ctrl+C keypress") | |
| # Send Ctrl+C as an actual keypress to clear the input buffer | |
| terminus_view.run_command('terminus_keypress', { | |
| 'key': 'c', | |
| 'ctrl': True | |
| }) | |
| # Small delay before sending the actual text | |
| def send_actual_text(): | |
| print("AskClaude: Sending text to terminus: {}".format(repr(text[:50]))) | |
| window.run_command('terminus_send_string', { | |
| 'string': text, | |
| 'visible_only': True | |
| }) | |
| sublime.set_timeout(send_actual_text, 200) | |
| sublime.set_timeout(send_text, 100) | |
| def _get_project_session_id(self): | |
| """Generate a deterministic session ID based on the project path""" | |
| window = self.view.window() | |
| if not window: | |
| return None | |
| # Get the project folder | |
| folders = window.folders() | |
| if folders: | |
| project_path = folders[0] | |
| else: | |
| # Use current file directory if no project | |
| if self.view.file_name(): | |
| import os | |
| project_path = os.path.dirname(self.view.file_name()) | |
| else: | |
| return None | |
| # Generate a deterministic UUID from the project path | |
| # Use UUID5 with a namespace (DNS namespace is fine) | |
| namespace = uuid.UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8') # DNS namespace | |
| session_uuid = uuid.uuid5(namespace, project_path) | |
| return str(session_uuid) | |
| def _setup_project_claude_context(self): | |
| """Setup a dedicated Claude context directory for this project""" | |
| import os | |
| import json | |
| window = self.view.window() | |
| if not window: | |
| return None, False | |
| # Get project session ID | |
| session_id = self._get_project_session_id() | |
| if not session_id: | |
| return None, False | |
| # Create dedicated directory for this Sublime project's Claude context | |
| claude_sublime_dir = os.path.expanduser('~/.sublime-claude') | |
| project_claude_dir = os.path.join(claude_sublime_dir, session_id) | |
| claude_settings_dir = os.path.join(project_claude_dir, '.claude') | |
| # Create directories if they don't exist | |
| os.makedirs(claude_settings_dir, exist_ok=True) | |
| # Get all project folders for additionalDirectories | |
| project_folders = window.folders() | |
| if not project_folders and self.view.file_name(): | |
| # If no project folders, use the current file's directory | |
| project_folders = [os.path.dirname(self.view.file_name())] | |
| # Generate Claude settings.json with project access | |
| settings = { | |
| "additionalDirectories": project_folders | |
| } | |
| settings_path = os.path.join(claude_settings_dir, 'settings.json') | |
| with open(settings_path, 'w') as f: | |
| json.dump(settings, f, indent=2) | |
| print("AskClaude: Created Claude context at {}".format(project_claude_dir)) | |
| print("AskClaude: Granted access to: {}".format(project_folders)) | |
| return project_claude_dir | |
| def _open_new_terminus(self, initial_text, resume=True): | |
| """Open a new Terminus window with Claude in opposite pane""" | |
| window = self.view.window() | |
| if not window: | |
| return | |
| # Setup project-specific Claude context directory | |
| claude_context_dir = self._setup_project_claude_context() | |
| if not claude_context_dir: | |
| print("AskClaude: Failed to setup Claude context, using default") | |
| claude_context_dir = window.folders()[0] if window.folders() else None | |
| # Get current group and determine target group for terminus | |
| current_group = window.active_group() | |
| num_groups = window.num_groups() | |
| # If only one group, create a split layout (2 columns) | |
| if num_groups == 1: | |
| window.run_command('set_layout', { | |
| 'cols': [0.0, 0.5, 1.0], | |
| 'rows': [0.0, 1.0], | |
| 'cells': [[0, 0, 1, 1], [1, 0, 2, 1]] | |
| }) | |
| target_group = 1 # Open in the new right pane | |
| else: | |
| # Use the opposite group (if in 0, use 1; if in 1, use 0) | |
| target_group = 1 if current_group == 0 else 0 | |
| # Focus the target group before opening terminus | |
| window.focus_group(target_group) | |
| # Build Claude command | |
| claude_cmd = 'claude --resume' if resume else 'claude' | |
| print("AskClaude: Starting Claude with: {}".format(claude_cmd)) | |
| # Open Terminus with Claude from the project-specific context directory | |
| window.run_command('terminus_open', { | |
| 'cmd': ['/bin/zsh', '-i', '-c', claude_cmd], | |
| 'cwd': claude_context_dir, | |
| 'title': 'Claude' | |
| }) | |
| # Only paste prompt for new sessions, not when resuming | |
| if not resume: | |
| def send_delayed_text(): | |
| print("AskClaude: Pasting prompt to new session") | |
| window.run_command('terminus_send_string', { | |
| 'string': initial_text, | |
| 'visible_only': True | |
| }) | |
| # Wait 2 seconds for Claude to fully start up | |
| sublime.set_timeout(send_delayed_text, 2000) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment