""" VS Code / Monaco Editor Integration API Router Provides code editing capabilities: - File editing with syntax highlighting - Project file tree - Code snippets - Language server features """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from typing import List, Optional from datetime import datetime from ..database import get_db from ..auth import get_current_user from ..models import User, File, Folder from pydantic import BaseModel router = APIRouter(prefix="/api/vscode", tags=["vscode"]) class CodeFile(BaseModel): name: str path: str content: str language: str = "plaintext" folder_id: Optional[int] = None class CodeSnippet(BaseModel): name: str language: str code: str description: Optional[str] = None @router.get("/files") async def list_code_files( folder_id: Optional[int] = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """List all code files in the user's workspace""" query = select(File).where(File.user_id == current_user.id) if folder_id: query = query.where(File.folder_id == folder_id) result = await db.execute(query) files = result.scalars().all() return { "files": [ { "id": f.id, "name": f.name, "path": f.path, "size": f.size, "mime_type": f.file_type or None, "folder_id": f.folder_id, "created_at": f.created_at.isoformat(), "updated_at": f.updated_at.isoformat(), "language": detect_language(f.name) } for f in files ], "total": len(files) } @router.get("/files/{file_id}/content") async def get_file_content( file_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get file content for editing""" result = await db.execute( select(File).where(File.id == file_id, File.user_id == current_user.id) ) file = result.scalar_one_or_none() if not file: raise HTTPException(status_code=404, detail="File not found") # In a real implementation, fetch content from S3 or file system # For now, return metadata return { "id": file.id, "name": file.name, "path": file.path, "language": detect_language(file.name), "content": "// File content would be loaded here\n// from S3 or file system", "metadata": { "size": file.size, "mime_type": file.file_type or None, "created_at": file.created_at.isoformat(), "updated_at": file.updated_at.isoformat() } } @router.put("/files/{file_id}/content") async def update_file_content( file_id: int, content: str, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update file content""" result = await db.execute( select(File).where(File.id == file_id, File.user_id == current_user.id) ) file = result.scalar_one_or_none() if not file: raise HTTPException(status_code=404, detail="File not found") # In a real implementation, save to S3 or file system file.updated_at = datetime.utcnow() file.size = len(content.encode('utf-8')) await db.commit() return { "message": "File updated successfully", "file_id": file.id, "size": file.size } @router.get("/tree") async def get_file_tree( db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get hierarchical file tree for the sidebar""" # Get all folders folders_result = await db.execute( select(Folder).where(Folder.user_id == current_user.id) ) folders = folders_result.scalars().all() # Get all files files_result = await db.execute( select(File).where(File.user_id == current_user.id) ) files = files_result.scalars().all() # Build tree structure def build_tree(): tree = [] folder_map = {} # Create folder nodes for folder in folders: folder_node = { "id": f"folder-{folder.id}", "name": folder.name, "type": "folder", "path": folder.path, "children": [] } folder_map[folder.id] = folder_node if folder.parent_id is None: tree.append(folder_node) else: parent = folder_map.get(folder.parent_id) if parent: parent["children"].append(folder_node) # Add files to folders for file in files: file_node = { "id": f"file-{file.id}", "name": file.name, "type": "file", "path": file.path, "language": detect_language(file.name), "size": file.size } if file.folder_id: folder = folder_map.get(file.folder_id) if folder: folder["children"].append(file_node) else: tree.append(file_node) return tree return {"tree": build_tree()} @router.get("/languages") async def list_supported_languages( current_user: User = Depends(get_current_user) ): """List all supported programming languages""" languages = [ {"id": "javascript", "name": "JavaScript", "extensions": [".js", ".jsx"]}, {"id": "typescript", "name": "TypeScript", "extensions": [".ts", ".tsx"]}, {"id": "python", "name": "Python", "extensions": [".py"]}, {"id": "java", "name": "Java", "extensions": [".java"]}, {"id": "csharp", "name": "C#", "extensions": [".cs"]}, {"id": "cpp", "name": "C++", "extensions": [".cpp", ".hpp", ".h"]}, {"id": "c", "name": "C", "extensions": [".c", ".h"]}, {"id": "go", "name": "Go", "extensions": [".go"]}, {"id": "rust", "name": "Rust", "extensions": [".rs"]}, {"id": "ruby", "name": "Ruby", "extensions": [".rb"]}, {"id": "php", "name": "PHP", "extensions": [".php"]}, {"id": "html", "name": "HTML", "extensions": [".html", ".htm"]}, {"id": "css", "name": "CSS", "extensions": [".css"]}, {"id": "scss", "name": "SCSS", "extensions": [".scss"]}, {"id": "json", "name": "JSON", "extensions": [".json"]}, {"id": "yaml", "name": "YAML", "extensions": [".yaml", ".yml"]}, {"id": "markdown", "name": "Markdown", "extensions": [".md"]}, {"id": "sql", "name": "SQL", "extensions": [".sql"]}, {"id": "shell", "name": "Shell", "extensions": [".sh", ".bash"]}, {"id": "dockerfile", "name": "Dockerfile", "extensions": ["Dockerfile"]}, ] return {"languages": languages} @router.get("/snippets") async def list_snippets( language: Optional[str] = None, current_user: User = Depends(get_current_user) ): """Get code snippets""" # Default snippets for various languages snippets = { "python": [ { "name": "Function", "prefix": "def", "code": "def ${1:function_name}(${2:params}):\n ${3:pass}" }, { "name": "Class", "prefix": "class", "code": "class ${1:ClassName}:\n def __init__(self, ${2:params}):\n ${3:pass}" }, { "name": "For Loop", "prefix": "for", "code": "for ${1:item} in ${2:iterable}:\n ${3:pass}" } ], "javascript": [ { "name": "Function", "prefix": "func", "code": "function ${1:functionName}(${2:params}) {\n ${3:// code}\n}" }, { "name": "Arrow Function", "prefix": "arrow", "code": "const ${1:functionName} = (${2:params}) => {\n ${3:// code}\n}" }, { "name": "Class", "prefix": "class", "code": "class ${1:ClassName} {\n constructor(${2:params}) {\n ${3:// code}\n }\n}" } ], "go": [ { "name": "Function", "prefix": "func", "code": "func ${1:functionName}(${2:params}) ${3:returnType} {\n ${4:// code}\n}" }, { "name": "Struct", "prefix": "struct", "code": "type ${1:StructName} struct {\n ${2:// fields}\n}" } ] } if language: return {"snippets": snippets.get(language, [])} return {"snippets": snippets} @router.get("/themes") async def list_themes( current_user: User = Depends(get_current_user) ): """List available editor themes""" themes = [ {"id": "vs", "name": "Visual Studio Light"}, {"id": "vs-dark", "name": "Visual Studio Dark"}, {"id": "hc-black", "name": "High Contrast Dark"}, {"id": "monokai", "name": "Monokai"}, {"id": "github", "name": "GitHub"}, {"id": "solarized-dark", "name": "Solarized Dark"}, {"id": "solarized-light", "name": "Solarized Light"}, ] return {"themes": themes} def detect_language(filename: str) -> str: """Detect programming language from file extension""" extension_map = { ".js": "javascript", ".jsx": "javascript", ".ts": "typescript", ".tsx": "typescript", ".py": "python", ".java": "java", ".cs": "csharp", ".cpp": "cpp", ".hpp": "cpp", ".c": "c", ".h": "c", ".go": "go", ".rs": "rust", ".rb": "ruby", ".php": "php", ".html": "html", ".htm": "html", ".css": "css", ".scss": "scss", ".json": "json", ".yaml": "yaml", ".yml": "yaml", ".md": "markdown", ".sql": "sql", ".sh": "shell", ".bash": "shell" } ext = "." + filename.split(".")[-1] if "." in filename else "" return extension_map.get(ext.lower(), "plaintext")