Skip to content
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
11 changes: 11 additions & 0 deletions editor/src/dashboard/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ export function DashboardProjectItem(props: IDashboardProjectItemProps) {
execNodePty(`code "${dirname(props.project.absolutePath)}"`);
}

function handleOpenInDefaultIde() {
const projectDir = dirname(props.project.absolutePath);
ipcRenderer.send("editor:open-with", projectDir);
}

return (
<ContextMenu onOpenChange={(o) => setContextMenuOpen(o)}>
<ContextMenuTrigger>
Expand Down Expand Up @@ -174,6 +179,9 @@ export function DashboardProjectItem(props: IDashboardProjectItemProps) {
{`Show in ${isDarwin() ? "Finder" : "Explorer"}`}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="flex items-center gap-2" onClick={() => handleOpenInDefaultIde()}>
Open in Default IDE
</DropdownMenuItem>
<DropdownMenuItem className="flex items-center gap-2" onClick={() => handleOpenInVisualStudioCode()}>
Open in Visual Studio Code
</DropdownMenuItem>
Expand Down Expand Up @@ -219,6 +227,9 @@ export function DashboardProjectItem(props: IDashboardProjectItemProps) {
{`Show in ${isDarwin() ? "Finder" : "Explorer"}`}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem className="flex items-center gap-2" onClick={() => handleOpenInDefaultIde()}>
Open in Default IDE
</ContextMenuItem>
<ContextMenuItem className="flex items-center gap-2" onClick={() => handleOpenInVisualStudioCode()}>
Open in Visual Studio Code
</ContextMenuItem>
Expand Down
17 changes: 12 additions & 5 deletions editor/src/editor/layout/assets-browser/items/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,10 @@ export class AssetsBrowserItem extends Component<IAssetsBrowserItemProps, IAsset
* Called on the item is double-clicked. To be overriden by the specialized items implementations.
*/
protected onDoubleClick(): void | Promise<void> {
// Nothing to do by default.
// If it's a file (not a directory), open it in the default editor
if (!this.state.isDirectory) {
ipcRenderer.send("editor:open-with", this.props.absolutePath);
}
}

private _handleDragStart(ev: DragEvent<HTMLDivElement>): void {
Expand Down Expand Up @@ -392,12 +395,16 @@ export class AssetsBrowserItem extends Component<IAssetsBrowserItemProps, IAsset

return (
<ContextMenuContent>
{!this.state.isDirectory && (
<ContextMenuItem className="flex items-center gap-2" onClick={() => ipcRenderer.send("editor:open-with", this.props.absolutePath)}>
Open
</ContextMenuItem>
)}

<ContextMenuItem className="flex items-center gap-2" onClick={() => ipcRenderer.send("editor:show-item", this.props.absolutePath)}>
<ImFinder className="w-4 h-4" /> {`Show in ${isDarwin ? "Finder" : "Explorer"}`}
{`Show in ${isDarwin ? "Finder" : "Explorer"}`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the import of "ImFinder " then to fix the lint issue

</ContextMenuItem>

<ContextMenuSeparator />

{items.map((item, index) => (
<Fragment key={`context-menu-item-${index}`}>{item}</Fragment>
))}
Expand All @@ -418,7 +425,7 @@ export class AssetsBrowserItem extends Component<IAssetsBrowserItemProps, IAsset
<ContextMenuSeparator />

<ContextMenuItem className="flex items-center gap-2 !text-red-400" onClick={() => this._handleTrashItem()}>
<AiOutlineClose className="w-5 h-5" fill="rgb(248, 113, 113)" /> Remove
<AiOutlineClose className="w-5 h-5" fill="rgb(248, 113, 113)" /> Delete
</ContextMenuItem>
</ContextMenuContent>
);
Expand Down
14 changes: 14 additions & 0 deletions editor/src/editor/layout/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class EditorToolbar extends Component<IEditorToolbarProps> {
super(props);

ipcRenderer.on("editor:open-project", () => this._handleOpenProject());
ipcRenderer.on("editor:open-default-ide", () => this._handleOpenInDefaultIde());
ipcRenderer.on("editor:open-vscode", () => this._handleOpenVisualStudioCode());

this._nodeCommands = getNodeCommands(this.props.editor);
Expand Down Expand Up @@ -90,6 +91,10 @@ export class EditorToolbar extends Component<IEditorToolbarProps> {

<MenubarSeparator />

<MenubarItem disabled={!this.props.editor.state.projectPath} onClick={() => this._handleOpenInDefaultIde()}>
Open in Default IDE
</MenubarItem>

<MenubarItem disabled={!this.props.editor.state.visualStudioCodeAvailable} onClick={() => this._handleOpenVisualStudioCode()}>
Open in Visual Studio Code
</MenubarItem>
Expand Down Expand Up @@ -258,4 +263,13 @@ export class EditorToolbar extends Component<IEditorToolbarProps> {
const p = await execNodePty(`code "${join(dirname(this.props.editor.state.projectPath), "/")}"`);
await p.wait();
}

private _handleOpenInDefaultIde(): void {
if (!this.props.editor.state.projectPath) {
return;
}

const projectDir = dirname(this.props.editor.state.projectPath);
ipcRenderer.send("editor:open-with", projectDir);
}
}
4 changes: 4 additions & 0 deletions editor/src/editor/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export function setupEditorMenu(): void {
{
type: "separator",
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could replace "Open in Visual Studio"

label: "Open in Default IDE",
click: () => BrowserWindow.getFocusedWindow()?.webContents.send("editor:open-default-ide"),
},
{
label: "Open in Visual Studio Code",
click: () => BrowserWindow.getFocusedWindow()?.webContents.send("editor:open-vscode"),
Expand Down
117 changes: 117 additions & 0 deletions editor/src/electron/events/shell.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { platform } from "os";
import { ipcMain, shell } from "electron";
import { exec } from "child_process";
import { statSync } from "fs";

ipcMain.on("editor:trash-items", async (ev, items) => {
const isWindows = platform() === "win32";
Expand All @@ -20,3 +22,118 @@ ipcMain.on("editor:show-item", (_, item) => {

shell.showItemInFolder(item);
});

ipcMain.on("editor:open-in-external-editor", (_, item) => {
const isWindows = platform() === "win32";
item = isWindows ? item.replace(/\//g, "\\") : item.replace(/\\/g, "/");

shell.openPath(item);
});

async function checkCommandAvailable(command: string): Promise<boolean> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to put it in src/tools/process.ts

return new Promise((resolve) => {
exec(`${command} --version`, (error) => {
resolve(!error);
});
});
}

async function openInIde(path: string, isDirectory: boolean): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to put it in src/tools/ide.ts which doesn't exist yet.
This one I'll have to test on each platform. Is it an output from Claude?

const isWindows = platform() === "win32";
const normalizedPath = isWindows ? path.replace(/\//g, "\\") : path.replace(/\\/g, "/");

if (isDirectory) {
// Try to open directory in IDEs
const ideCommands = [
{ command: "code", args: [normalizedPath] },
{ command: "cursor", args: [normalizedPath] },
{ command: "subl", args: [normalizedPath] }, // Sublime Text
];

// Try each IDE in order
for (const ide of ideCommands) {
if (await checkCommandAvailable(ide.command)) {
const fullCommand = `${ide.command} "${normalizedPath}"`;
exec(fullCommand, (error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exec means the IDE process will be attached to the electron one (or am I wrong?)
If it is attached, I suggest to use node-pty in order to run the command outside of the current process like I did for VSCode so the IDE keeps opened even if the user closes the editor

if (error) {
console.error(`Failed to open with ${ide.command}:`, error);
}
});
return;
}
}

// On macOS, try JetBrains IDEs (PhpStorm, WebStorm, IntelliJ IDEA)
if (platform() === "darwin") {
exec(`open -a "PhpStorm" "${normalizedPath}"`, (error) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use the same method than the part // Try each IDE in order in line 53

if (!error) {
return;
}
exec(`open -a "WebStorm" "${normalizedPath}"`, (error) => {
if (!error) {
return;
}
exec(`open -a "IntelliJ IDEA" "${normalizedPath}"`, (error) => {
if (!error) {
return;
}
exec(`open -a "IntelliJ IDEA CE" "${normalizedPath}"`, (error) => {
if (!error) {
return;
}
// Fallback to shell.openPath
shell.openPath(normalizedPath);
});
});
});
});
return;
}

// On Windows, try JetBrains IDEs via CLI
if (isWindows) {
if (await checkCommandAvailable("phpstorm")) {
exec(`phpstorm "${normalizedPath}"`, (error) => {
if (error) {
console.error("Failed to open with PhpStorm:", error);
}
});
return;
}
if (await checkCommandAvailable("webstorm")) {
exec(`webstorm "${normalizedPath}"`, (error) => {
if (error) {
console.error("Failed to open with WebStorm:", error);
}
});
return;
}
if (await checkCommandAvailable("idea")) {
exec(`idea "${normalizedPath}"`, (error) => {
if (error) {
console.error("Failed to open with IntelliJ IDEA:", error);
shell.openPath(normalizedPath);
}
});
return;
}
}

// Fallback: open with default application
shell.openPath(normalizedPath);
} else {
// For files, use shell.openPath which uses OS default application
shell.openPath(normalizedPath);
}
}

ipcMain.on("editor:open-with", async (_, item) => {
try {
const stats = statSync(item);
const isDirectory = stats.isDirectory();
await openInIde(item, isDirectory);
} catch (e) {
// If stat fails, try as directory first, then as file
await openInIde(item, true);
}
});