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
This commit is contained in:
Louis Knight-Webb
2025-06-30 16:21:05 +01:00
committed by GitHub
parent 76277b279a
commit 97af4d3f30
3 changed files with 155 additions and 2 deletions

View File

@@ -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<String>,
}
pub async fn open_project_in_editor(
Path(id): Path<Uuid>,
Extension(pool): Extension<SqlitePool>,
Extension(config): Extension<Arc<RwLock<crate::models::config::Config>>>,
Json(payload): Json<Option<OpenEditorRequest>>,
) -> Result<ResponseJson<ApiResponse<()>>, 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<Uuid>,
Query(params): Query<HashMap<String, String>>,
@@ -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))
}

View File

@@ -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() {
<ExternalLink className="mr-2 h-4 w-4" />
View Project
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handleOpenInIDE(project.id);
}}
>
<FolderOpen className="mr-2 h-4 w-4" />
Open in IDE
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();

View File

@@ -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}
</span>
)}
<Button
variant="ghost"
size="sm"
onClick={handleOpenInIDE}
className="h-8 w-8 p-0"
title="Open in IDE"
>
<FolderOpen className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsProjectSettingsOpen(true)}
className="h-8 w-8 p-0"
title="Project Settings"
>
<Settings className="h-4 w-4" />
</Button>