Skip to content

Introduce VSCode native Ruby REPL #3562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions project-words
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,7 @@ yarp
YARP
yjit
YJIT
Repls
COLORTERM
truecolor
IRBRC
99 changes: 98 additions & 1 deletion vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
"when": "resourceLangId == ruby",
"command": "workbench.action.terminal.runSelectedText",
"group": "9_cutcopypaste"
},
{
"when": "resourceLangId == ruby && editorHasSelection",
"command": "rubyLsp.execInREPL",
"group": "9_cutcopypaste"
}
],
"editor/title": [
Expand Down Expand Up @@ -191,6 +196,44 @@
"command": "rubyLsp.showOutput",
"title": "Show output channel",
"category": "Ruby LSP"
},
{
"command": "rubyLsp.startREPL",
"title": "Start Ruby REPL",
"category": "Ruby LSP"
},
{
"command": "rubyLsp.execInREPL",
"title": "Execute in Ruby REPL",
"category": "Ruby LSP",
"when": "editorFocus && editorLangId == ruby"
},
{
"command": "rubyLsp.interruptREPL",
"title": "Interrupt Ruby REPL",
"category": "Ruby LSP"
},
{
"command": "rubyLsp.executeScratchPadLine",
"title": "Execute Current Line in Scratch Pad",
"category": "Ruby LSP",
"when": "editorTextFocus && editorLangId == ruby && resourceScheme == untitled"
},
{
"command": "rubyLsp.executeScratchPadSelection",
"title": "Execute Selection in Scratch Pad",
"category": "Ruby LSP",
"when": "editorTextFocus && editorLangId == ruby && resourceScheme == untitled"
},
{
"command": "rubyLsp.createIrbTerminal",
"title": "Start Ruby REPL (IRB)",
"category": "Ruby LSP"
},
{
"command": "rubyLsp.createRailsConsoleTerminal",
"title": "Start Rails Console",
"category": "Ruby LSP"
}
],
"configuration": {
Expand Down Expand Up @@ -540,6 +583,34 @@
"description": "Controls the level of opacity for inline RBS comment signatures",
"type": "string",
"default": "1"
},
"rubyLsp.replSettings": {
"description": "Settings for the Ruby REPL integration",
"type": "object",
"properties": {
"showWelcomeMessage": {
"description": "Show welcome message with tips when starting REPL",
"type": "boolean",
"default": true
},
"executionFeedbackDuration": {
"description": "How long to show execution feedback decorations (in milliseconds)",
"type": "number",
"default": 3000,
"minimum": 1000,
"maximum": 10000
},
"autoOpenScratchPad": {
"description": "Automatically open the scratch pad when starting REPL",
"type": "boolean",
"default": true
}
},
"default": {
"showWelcomeMessage": true,
"executionFeedbackDuration": 3000,
"autoOpenScratchPad": true
}
}
}
},
Expand All @@ -555,6 +626,18 @@
}
]
},
"terminal": {
"profiles": [
{
"id": "rubyLsp.irbTerminal",
"title": "Ruby REPL (IRB)"
},
{
"id": "rubyLsp.railsConsoleTerminal",
"title": "Rails Console"
}
]
},
"breakpoints": [
{
"language": "ruby"
Expand Down Expand Up @@ -768,7 +851,21 @@
"editor.formatOnType": true,
"editor.wordSeparators": "`~@#$%^&*()-=+[{]}\\|;:'\",.<>/"
}
}
},
"keybindings": [
{
"command": "rubyLsp.executeScratchPadLine",
"key": "ctrl+enter",
"mac": "cmd+enter",
"when": "editorTextFocus && editorLangId == ruby && resourceScheme == untitled"
},
{
"command": "rubyLsp.executeScratchPadSelection",
"key": "ctrl+shift+enter",
"mac": "cmd+shift+enter",
"when": "editorTextFocus && editorLangId == ruby && resourceScheme == untitled"
}
]
},
"scripts": {
"vscode:prepublish": "yarn run esbuild-base --minify",
Expand Down
70 changes: 70 additions & 0 deletions vscode/src/commands/baseScratchPadCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import * as vscode from "vscode";

import { Workspace } from "../workspace";
import { ReplScratchPad } from "../replScratchPad";
import { TerminalRepl } from "../terminalRepl";

import { BaseCommandHandler } from "./commandHandler";

/**
* Base class for scratch pad execution commands
*/
export abstract class BaseScratchPadCommandHandler extends BaseCommandHandler {
constructor(
private currentActiveWorkspace: (
activeEditor?: vscode.TextEditor,
) => Workspace | undefined,
private getScratchPad: (workspaceKey: string) => ReplScratchPad | undefined,
private getTerminalRepl: (workspaceKey: string) => TerminalRepl | undefined,
) {
super();
}

async execute(): Promise<void> {
const editor = this.getActiveEditor();
if (!editor) {
return;
}

if (!this.isScratchPadDocument(editor.document)) {
return;
}

const workspace = this.currentActiveWorkspace(editor);
if (!workspace) {
return;
}

const workspaceKey = workspace.workspaceFolder.uri.toString();
const scratchPad = this.getScratchPad(workspaceKey);

if (!scratchPad) {
vscode.window.showWarningMessage(
"No scratch pad found for this workspace",
);
return;
}

const terminalRepl = this.getTerminalRepl(workspaceKey);
if (!terminalRepl) {
vscode.window.showWarningMessage("No REPL found for this workspace");
return;
}

await this.executeScratchPadAction(scratchPad, terminalRepl, editor);
}

protected abstract executeScratchPadAction(
scratchPad: ReplScratchPad,
terminalRepl: TerminalRepl,
editor: vscode.TextEditor,
): Promise<void>;

protected isExecutableCode(code: string): boolean {
return Boolean(code && !code.startsWith("#"));
}

private isScratchPadDocument(document: vscode.TextDocument): boolean {
return document.isUntitled && document.languageId === "ruby";
}
}
59 changes: 59 additions & 0 deletions vscode/src/commands/commandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as vscode from "vscode";

/**
* Interface for command handlers that can be registered with VS Code
*/
export interface CommandHandler {
/**
* The command identifier that this handler responds to
*/
readonly commandId: string;

/**
* Execute the command
*/
execute(...args: any[]): Promise<void> | void;

/**
* Register this command handler with VS Code
*/
register(): vscode.Disposable;
}

/**
* Abstract base class for command handlers with common functionality
*/
export abstract class BaseCommandHandler implements CommandHandler {
abstract readonly commandId: string;

abstract execute(...args: any[]): Promise<void> | void;

/**
* Register this command handler with VS Code
*/
register(): vscode.Disposable {
return vscode.commands.registerCommand(this.commandId, (...args) =>
this.execute(...args),
);
}

/**
* Helper method to show error messages consistently
*/
protected showError(message: string, error?: Error): void {
const fullMessage = error ? `${message}: ${error.message}` : message;
vscode.window.showErrorMessage(fullMessage);
}

/**
* Helper method to get the active editor with validation
*/
protected getActiveEditor(): vscode.TextEditor | undefined {
const editor = vscode.window.activeTextEditor;
if (!editor) {
vscode.window.showWarningMessage("No active editor found");
return undefined;
}
return editor;
}
}
86 changes: 86 additions & 0 deletions vscode/src/commands/execInReplCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import * as vscode from "vscode";

import { Command } from "../common";
import { Workspace } from "../workspace";
import { TerminalRepl } from "../terminalRepl";

import { BaseCommandHandler } from "./commandHandler";

/**
* Command handler for executing code in an existing REPL
*/
export class ExecInReplCommandHandler extends BaseCommandHandler {
readonly commandId = Command.ExecInRepl;

constructor(
private currentActiveWorkspace: (
activeEditor?: vscode.TextEditor,
) => Workspace | undefined,
private getTerminalRepl: (workspaceKey: string) => TerminalRepl | undefined,
) {
super();
}

async execute(): Promise<void> {
const editor = this.getActiveEditor();
if (!editor) {
return;
}

const workspace = this.currentActiveWorkspace(editor);
if (!workspace) {
vscode.window.showWarningMessage("No workspace found for current file");
return;
}

const workspaceKey = workspace.workspaceFolder.uri.toString();
const terminalRepl = this.getTerminalRepl(workspaceKey);

if (!terminalRepl || !terminalRepl.isRunning) {
await this.promptToStartRepl();
return;
}

const code = this.getCodeToExecute(editor);
if (!code.trim()) {
vscode.window.showWarningMessage("No code selected to execute");
return;
}

try {
await terminalRepl.execute(code);
} catch (error) {
this.showError("Failed to execute in REPL", error as Error);
}
}

private getCodeToExecute(editor: vscode.TextEditor): string {
const selection = editor.selection;

if (selection.isEmpty) {
return this.getCurrentLineText(editor);
} else {
return this.getSelectedText(editor);
}
}

private getCurrentLineText(editor: vscode.TextEditor): string {
const line = editor.document.lineAt(editor.selection.active.line);
return line.text;
}

private getSelectedText(editor: vscode.TextEditor): string {
return editor.document.getText(editor.selection);
}

private async promptToStartRepl(): Promise<void> {
const answer = await vscode.window.showInformationMessage(
"No REPL is running for this workspace. Would you like to start one?",
"Start REPL",
);

if (answer === "Start REPL") {
await vscode.commands.executeCommand(Command.StartRepl);
}
}
}
Loading
Loading