Skip to content

Commit 93daeb0

Browse files
committed
Add select mode for element inspection in Roopik
Introduces a select mode feature with a new SelectModeInjector, enabling element selection, box model visualization, and computed style inspection in sandboxed components. Updates the webview and extension to support select mode toggling, message passing, and opening source files with selection highlighting in the editor.
1 parent 9727a2f commit 93daeb0

7 files changed

Lines changed: 872 additions & 12 deletions

File tree

extensions/roopik/src/canvasPanel.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ export class CanvasPanel implements vscode.Disposable {
431431
filePath: string;
432432
line?: number;
433433
column?: number;
434+
endLine?: number;
435+
endColumn?: number;
434436
});
435437
break;
436438

@@ -734,13 +736,15 @@ export class CanvasPanel implements vscode.Disposable {
734736

735737
/**
736738
* Handle open file request from webview (View Code button)
739+
* Supports both cursor positioning and selection highlighting
737740
*/
738741
private async handleOpenFile(payload: {
739742
filePath: string;
740743
line?: number;
741744
column?: number;
745+
endLine?: number;
746+
endColumn?: number;
742747
}): Promise<void> {
743-
this.logger.info(`Opening file: ${payload.filePath}`);
744748

745749
try {
746750
// Resolve the file path relative to workspace
@@ -766,16 +770,32 @@ export class CanvasPanel implements vscode.Disposable {
766770
viewColumn: vscode.ViewColumn.One
767771
});
768772

769-
// Move cursor to specified line/column if provided
773+
// Move cursor or create selection if line is provided
770774
if (payload.line !== undefined) {
771-
const line = Math.max(0, payload.line - 1); // Convert to 0-based
772-
const column = Math.max(0, (payload.column || 1) - 1); // Convert to 0-based
773-
const position = new vscode.Position(line, column);
774-
editor.selection = new vscode.Selection(position, position);
775-
editor.revealRange(
776-
new vscode.Range(position, position),
777-
vscode.TextEditorRevealType.InCenter
778-
);
775+
const startLine = Math.max(0, payload.line - 1); // Convert to 0-based
776+
const startColumn = Math.max(0, (payload.column || 1) - 1); // Convert to 0-based
777+
const startPos = new vscode.Position(startLine, startColumn);
778+
779+
// Check if we have end position for selection highlighting
780+
if (payload.endLine !== undefined && payload.endColumn !== undefined) {
781+
const endLine = Math.max(0, payload.endLine - 1);
782+
const endColumn = Math.max(0, payload.endColumn - 1);
783+
const endPos = new vscode.Position(endLine, endColumn);
784+
785+
// Create selection range (highlighted)
786+
editor.selection = new vscode.Selection(startPos, endPos);
787+
editor.revealRange(
788+
new vscode.Range(startPos, endPos),
789+
vscode.TextEditorRevealType.InCenter
790+
);
791+
} else {
792+
// Just position cursor (no selection)
793+
editor.selection = new vscode.Selection(startPos, startPos);
794+
editor.revealRange(
795+
new vscode.Range(startPos, startPos),
796+
vscode.TextEditorRevealType.InCenter
797+
);
798+
}
779799
}
780800

781801
// this.logger.info(`File opened successfully: ${absolutePath}`);

extensions/roopik/webview/src/canvasView/components/InfiniteCanvas/InfiniteCanvas.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface InfiniteCanvasProps {
2727
snapMode?: SnapMode;
2828
/** Viewport dimensions for dynamic focused sandbox sizing */
2929
viewport?: { width: number; height: number };
30+
/** Whether select mode is enabled globally */
31+
isSelectMode?: boolean;
3032
/** Whether inspect mode is enabled globally */
3133
isInspectMode?: boolean;
3234
/** Whether inspect mode should auto-capture screenshots */
@@ -61,6 +63,8 @@ interface SandboxLayerProps {
6163
globalDeviceMode: DevicePreset;
6264
/** Viewport dimensions for dynamic focused sandbox sizing */
6365
viewport?: { width: number; height: number };
66+
/** Whether select mode is enabled globally */
67+
isSelectMode?: boolean;
6468
/** Whether inspect mode is enabled globally */
6569
isInspectMode?: boolean;
6670
/** Whether inspect mode should auto-capture screenshots */
@@ -87,6 +91,7 @@ function SandboxLayer({
8791
dragOffset,
8892
globalDeviceMode,
8993
viewport,
94+
isSelectMode,
9095
isInspectMode,
9196
captureOnInspectSelect,
9297
onSandboxDragStart,
@@ -125,6 +130,7 @@ function SandboxLayer({
125130
globalDeviceMode={globalDeviceMode}
126131
viewport={viewport}
127132
focusedSandboxPosition={focusedSandboxPosition}
133+
isSelectMode={isSelectMode}
128134
isInspectMode={isInspectMode}
129135
captureOnInspectSelect={captureOnInspectSelect}
130136
onMouseDown={(e) => onSandboxDragStart(e, sandbox.id)}
@@ -167,6 +173,7 @@ export function InfiniteCanvas({
167173
globalDeviceMode,
168174
snapMode = 'free',
169175
viewport,
176+
isSelectMode,
170177
isInspectMode,
171178
captureOnInspectSelect,
172179
onTransformChange,
@@ -240,6 +247,7 @@ export function InfiniteCanvas({
240247
dragOffset={dragOffset}
241248
globalDeviceMode={globalDeviceMode}
242249
viewport={viewport}
250+
isSelectMode={isSelectMode}
243251
isInspectMode={isInspectMode}
244252
captureOnInspectSelect={captureOnInspectSelect}
245253
onSandboxDragStart={handleSandboxDragStart}

extensions/roopik/webview/src/canvasView/components/SandboxCard/SandboxCard.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ interface SandboxCardProps {
2525
viewport?: { width: number; height: number };
2626
/** Position of the focused sandbox (for calculating push-away offset) */
2727
focusedSandboxPosition?: { x: number; y: number } | null;
28+
/** Whether select mode is enabled globally */
29+
isSelectMode?: boolean;
2830
/** Whether inspect mode is enabled globally */
2931
isInspectMode?: boolean;
3032
/** Whether inspect mode should auto-capture screenshots */
@@ -490,6 +492,7 @@ export function SandboxCard({
490492
globalDeviceMode,
491493
viewport,
492494
focusedSandboxPosition,
495+
isSelectMode = false,
493496
isInspectMode = false,
494497
captureOnInspectSelect = false,
495498
onMouseDown,
@@ -508,6 +511,16 @@ export function SandboxCard({
508511
// Track if component is "activated" for interaction (click-to-activate for better zoom UX)
509512
const [isActivated, setIsActivated] = useState(false);
510513

514+
// Send select mode toggle to iframe when isSelectMode changes
515+
useEffect(() => {
516+
if (iframeRef.current?.contentWindow) {
517+
iframeRef.current.contentWindow.postMessage({
518+
type: 'roopik-toggle-select',
519+
enabled: isSelectMode
520+
}, '*');
521+
}
522+
}, [isSelectMode, sandbox.id]);
523+
511524
// Send inspect mode toggle to iframe when isInspectMode changes
512525
useEffect(() => {
513526
if (iframeRef.current?.contentWindow) {

extensions/roopik/webview/src/canvasView/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ export type WebviewMessage =
331331
// Canvas state
332332
| { type: 'saveCanvas'; payload: { canvasId: string; state: CanvasState } }
333333
| { type: 'loadCanvas'; payload: { canvasId: string } }
334-
| { type: 'openFile'; payload: { filePath: string; line?: number; column?: number } }
334+
| { type: 'openFile'; payload: { filePath: string; line?: number; column?: number; endLine?: number; endColumn?: number } }
335335
| { type: 'log'; payload: { level: 'debug' | 'info' | 'warn' | 'error'; message: string; data?: unknown } }
336336
// Code editor - request to load component files from workspace
337337
| { type: 'loadComponentFiles'; payload: { componentId: string } }

extensions/roopik/webview/src/componentView/ComponentView.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,59 @@ function App() {
763763
}
764764
});
765765
}
766+
767+
// Handle select mode: open source file in editor with selection
768+
if (data.type === "roopik-select-open-source") {
769+
const { sourceLocation, componentId } = data;
770+
if (sourceLocation?.file) {
771+
// Source file is relative (e.g., "index.tsx"), need to resolve to absolute
772+
// using the component's folderPath
773+
let absoluteFilePath = sourceLocation.file;
774+
const sandbox = sandboxes.find(s => s.id === componentId);
775+
if (sandbox?.componentInput?.folderPath && !sourceLocation.file.includes('/') && !sourceLocation.file.includes('\\')) {
776+
// Relative path - combine with folderPath
777+
absoluteFilePath = `${sandbox.componentInput.folderPath}/${sourceLocation.file}`;
778+
}
779+
780+
vscode.postMessage({
781+
type: 'openFile',
782+
payload: {
783+
filePath: absoluteFilePath,
784+
line: sourceLocation.startLine || 1,
785+
column: sourceLocation.startCol || 1,
786+
endLine: sourceLocation.endLine,
787+
endColumn: sourceLocation.endCol
788+
}
789+
});
790+
}
791+
}
792+
793+
// Handle select mode: open component in editor
794+
if (data.type === "roopik-select-open-component") {
795+
const { componentId } = data;
796+
const sandbox = sandboxes.find(s => s.id === componentId);
797+
if (sandbox?.componentInput?.folderPath && sandbox.componentInput.entryFile) {
798+
const filePath = `${sandbox.componentInput.folderPath}/${sandbox.componentInput.entryFile}`;
799+
vscode.postMessage({
800+
type: 'openFile',
801+
payload: {
802+
filePath,
803+
line: 1,
804+
column: 1
805+
}
806+
});
807+
}
808+
}
809+
810+
// Handle select mode: ESC pressed, deselect
811+
if (data.type === "roopik-select-escape") {
812+
setIsSelectMode(false);
813+
}
814+
815+
// Handle select mode: selection cleared (click outside elements)
816+
if (data.type === "roopik-select-cleared") {
817+
// Selection was cleared in the sandbox
818+
}
766819
};
767820

768821
window.addEventListener("message", handleIframeMessage);
@@ -1513,6 +1566,7 @@ function App() {
15131566
globalDeviceMode={globalDeviceMode}
15141567
snapMode={snapMode}
15151568
viewport={viewport}
1569+
isSelectMode={isSelectMode}
15161570
isInspectMode={isInspectMode}
15171571
captureOnInspectSelect={AUTO_ATTACH_SCREENSHOT_ON_INSPECT}
15181572
onTransformChange={setTransform}

src/vs/workbench/contrib/roopik/electron-main/build/injectors/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export { InjectorPipeline } from './injectorPipeline.js';
1818
// Built-in Injectors
1919
export { ErrorBoundaryInjector } from './errorBoundaryInjector.js';
2020
export { InspectModeInjector } from './inspectModeInjector.js';
21+
export { SelectModeInjector } from './selectModeInjector.js';
2122
export { HmrBridgeInjector } from './hmrBridgeInjector.js';
2223
export { SourceTrackingInjector, createSourceTrackingTransform } from './sourceTrackingInjector.js';
2324

@@ -31,14 +32,16 @@ export { transformCode, parseElements, ParseOptions, TransformResult } from './s
3132
import { InjectorPipeline } from './injectorPipeline.js';
3233
import { ErrorBoundaryInjector } from './errorBoundaryInjector.js';
3334
import { InspectModeInjector } from './inspectModeInjector.js';
35+
import { SelectModeInjector } from './selectModeInjector.js';
3436
import { HmrBridgeInjector } from './hmrBridgeInjector.js';
3537

3638
/**
3739
* Create a default injector pipeline with all built-in injectors
3840
*
3941
* Includes:
4042
* - ErrorBoundaryInjector (priority 10): Error handling and display
41-
* - InspectModeInjector (priority 50): Element inspection
43+
* - SelectModeInjector (priority 45): Element selection with properties panel
44+
* - InspectModeInjector (priority 50): Element inspection for AI
4245
* - HmrBridgeInjector (priority 90): Hot module replacement
4346
*
4447
* @returns Configured InjectorPipeline
@@ -48,6 +51,7 @@ export function createDefaultPipeline(): InjectorPipeline {
4851

4952
// Add new injectors
5053
pipeline.register(new ErrorBoundaryInjector());
54+
pipeline.register(new SelectModeInjector());
5155
pipeline.register(new InspectModeInjector());
5256
pipeline.register(new HmrBridgeInjector());
5357
// pipeline.register(new MyCustomInjector()); // Future extensibility

0 commit comments

Comments
 (0)