From 97af4d3f303b3ef7000cc3962346015e50961f95 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 30 Jun 2025 16:21:05 +0100 Subject: [PATCH] Open project in ide (#25) * Task attempt a92c0626-bdf1-4325-9d85-fdec9dbb46c1 - Final changes * Task attempt a92c0626-bdf1-4325-9d85-fdec9dbb46c1 - Final changes * Prettier fix --- backend/src/routes/projects.rs | 89 ++++++++++++++++++- .../src/components/projects/project-list.tsx | 32 +++++++ frontend/src/pages/project-tasks.tsx | 36 +++++++- 3 files changed, 155 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/projects.rs b/backend/src/routes/projects.rs index 503e85b4..d682f246 100644 --- a/backend/src/routes/projects.rs +++ b/backend/src/routes/projects.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; use axum::{ extract::{Extension, Path, Query}, @@ -8,6 +8,7 @@ use axum::{ Json, Router, }; use sqlx::SqlitePool; +use tokio::sync::RwLock; use uuid::Uuid; use crate::models::{ @@ -348,6 +349,89 @@ pub async fn delete_project( } } +#[derive(serde::Deserialize)] +pub struct OpenEditorRequest { + editor_type: Option, +} + +pub async fn open_project_in_editor( + Path(id): Path, + Extension(pool): Extension, + Extension(config): Extension>>, + Json(payload): Json>, +) -> Result>, StatusCode> { + // Get the project + let project = match Project::find_by_id(&pool, id).await { + Ok(Some(project)) => project, + Ok(None) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to fetch project {}: {}", id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + // Get editor command from config or override + let editor_command = { + let config_guard = config.read().await; + if let Some(ref request) = payload { + if let Some(ref editor_type) = request.editor_type { + // Create a temporary editor config with the override + use crate::models::config::{EditorConfig, EditorType}; + let override_editor_type = match editor_type.as_str() { + "vscode" => EditorType::VSCode, + "cursor" => EditorType::Cursor, + "windsurf" => EditorType::Windsurf, + "intellij" => EditorType::IntelliJ, + "zed" => EditorType::Zed, + "custom" => EditorType::Custom, + _ => config_guard.editor.editor_type.clone(), + }; + let temp_config = EditorConfig { + editor_type: override_editor_type, + custom_command: config_guard.editor.custom_command.clone(), + }; + temp_config.get_command() + } else { + config_guard.editor.get_command() + } + } else { + config_guard.editor.get_command() + } + }; + + // Open editor in the project directory + let mut cmd = std::process::Command::new(&editor_command[0]); + for arg in &editor_command[1..] { + cmd.arg(arg); + } + cmd.arg(&project.git_repo_path); + + match cmd.spawn() { + Ok(_) => { + tracing::info!( + "Opened editor ({}) for project {} at path: {}", + editor_command.join(" "), + id, + project.git_repo_path + ); + Ok(ResponseJson(ApiResponse { + success: true, + data: None, + message: Some("Editor opened successfully".to_string()), + })) + } + Err(e) => { + tracing::error!( + "Failed to open editor ({}) for project {}: {}", + editor_command.join(" "), + id, + e + ); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} + pub async fn search_project_files( Path(id): Path, Query(params): Query>, @@ -488,6 +572,8 @@ async fn search_files_in_repo( } pub fn projects_router() -> Router { + use axum::routing::post; + Router::new() .route("/projects", get(get_projects).post(create_project)) .route( @@ -500,4 +586,5 @@ pub fn projects_router() -> Router { get(get_project_branches).post(create_project_branch), ) .route("/projects/:id/search", get(search_project_files)) + .route("/projects/:id/open-editor", post(open_project_in_editor)) } diff --git a/frontend/src/components/projects/project-list.tsx b/frontend/src/components/projects/project-list.tsx index 07820508..f775d53f 100644 --- a/frontend/src/components/projects/project-list.tsx +++ b/frontend/src/components/projects/project-list.tsx @@ -22,6 +22,7 @@ import { Loader2, MoreHorizontal, ExternalLink, + FolderOpen, } from 'lucide-react'; import { DropdownMenu, @@ -83,6 +84,28 @@ export function ProjectList() { setShowForm(true); }; + const handleOpenInIDE = async (projectId: string) => { + try { + const response = await makeRequest( + `/api/projects/${projectId}/open-editor`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(null), + } + ); + + if (!response.ok) { + throw new Error('Failed to open project in IDE'); + } + } catch (error) { + console.error('Failed to open project in IDE:', error); + setError('Failed to open project in IDE'); + } + }; + const handleFormSuccess = () => { setShowForm(false); setEditingProject(null); @@ -172,6 +195,15 @@ export function ProjectList() { View Project + { + e.stopPropagation(); + handleOpenInIDE(project.id); + }} + > + + Open in IDE + { e.stopPropagation(); diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index 04b774db..7b15698a 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; -import { Plus, Settings } from 'lucide-react'; +import { Plus, Settings, FolderOpen } from 'lucide-react'; import { makeRequest } from '@/lib/api'; import { TaskFormDialog } from '@/components/tasks/TaskFormDialog'; import { ProjectForm } from '@/components/projects/project-form'; @@ -55,6 +55,30 @@ export function ProjectTasks() { setIsTaskDialogOpen(true); }; + const handleOpenInIDE = async () => { + if (!projectId) return; + + try { + const response = await makeRequest( + `/api/projects/${projectId}/open-editor`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(null), + } + ); + + if (!response.ok) { + throw new Error('Failed to open project in IDE'); + } + } catch (error) { + console.error('Failed to open project in IDE:', error); + setError('Failed to open project in IDE'); + } + }; + // Setup keyboard shortcuts useKeyboardShortcuts({ navigate, @@ -359,11 +383,21 @@ export function ProjectTasks() { {project.current_branch} )} +