using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; namespace AxCopilot.Services; public sealed record CodeLanguageCapability( string Key, string DisplayName, IReadOnlyList Extensions, IReadOnlyList Guidance, string? LspLanguageId = null, bool ShowInQuickSelect = false, string? QuickSelectKey = null, string? QuickSelectLabel = null, string? QuickSelectIcon = null); /// /// 코드 탭과 에이전트가 공통으로 참조하는 언어 지원 카탈로그. /// 파일 분류, 시스템 프롬프트 가이드, build/test/lint 힌트, LSP 가능 여부를 한 곳에서 관리합니다. /// public static class CodeLanguageCatalog { private static readonly IReadOnlyDictionary s_manifestHints = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["csharp"] = ["*.sln", "*.csproj", "Directory.Build.props", "Directory.Build.targets"], ["python"] = ["pyproject.toml", "requirements.txt", "setup.py", "environment.yml", "Pipfile"], ["java"] = ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"], ["cpp"] = ["CMakeLists.txt", "*.vcxproj", "compile_commands.json", "Makefile"], ["typescript"] = ["package.json", "tsconfig.json", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"], ["javascript"] = ["package.json", "vite.config.*", "nuxt.config.*", "pnpm-lock.yaml", "yarn.lock", "package-lock.json"], ["go"] = ["go.mod", "go.sum"], ["rust"] = ["Cargo.toml", "Cargo.lock"], ["php"] = ["composer.json", "composer.lock"], ["ruby"] = ["Gemfile", "Gemfile.lock", "*.gemspec"], ["kotlin"] = ["build.gradle.kts", "settings.gradle.kts", "gradle.properties"], ["swift"] = ["Package.swift", "*.xcodeproj", "*.xcworkspace"], ["sql"] = ["migrations/*.sql", "schema.sql", "seed.sql", "*.sqlproj"], }; private static readonly IReadOnlyDictionary s_buildHints = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["csharp"] = ["dotnet build", "dotnet msbuild"], ["python"] = ["python -m py_compile", "python -m build"], ["java"] = ["mvn compile", "gradle build"], ["cpp"] = ["cmake --build .", "msbuild", "make"], ["typescript"] = ["npm run build", "pnpm build", "tsc -p ."], ["javascript"] = ["npm run build", "pnpm build", "vite build"], ["go"] = ["go build ./..."], ["rust"] = ["cargo build"], ["php"] = ["composer install --dry-run", "php -l"], ["ruby"] = ["bundle exec rake", "ruby -c"], ["kotlin"] = ["gradle build", "./gradlew build"], ["swift"] = ["swift build", "xcodebuild build"], ["sql"] = ["apply in a disposable database first", "review migration order and dependency impact"], }; private static readonly IReadOnlyDictionary s_testHints = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["csharp"] = ["dotnet test"], ["python"] = ["pytest", "python -m unittest"], ["java"] = ["mvn test", "gradle test"], ["cpp"] = ["ctest", "cmake --build . --target test"], ["typescript"] = ["npm test", "pnpm test", "vitest", "jest"], ["javascript"] = ["npm test", "pnpm test", "vitest", "jest"], ["go"] = ["go test ./..."], ["rust"] = ["cargo test"], ["php"] = ["phpunit", "composer test"], ["ruby"] = ["bundle exec rspec", "bundle exec rake test"], ["kotlin"] = ["gradle test", "./gradlew test"], ["swift"] = ["swift test", "xcodebuild test"], ["sql"] = ["run the script against a disposable database", "verify affected row counts and dependent queries"], }; private static readonly IReadOnlyDictionary s_lintHints = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["csharp"] = ["dotnet format", "dotnet build /warnaserror"], ["python"] = ["ruff check", "black --check", "flake8"], ["cpp"] = ["clang-format --dry-run", "clang-tidy"], ["typescript"] = ["npm run lint", "pnpm lint", "eslint ."], ["javascript"] = ["npm run lint", "pnpm lint", "eslint ."], ["go"] = ["gofmt -w", "golangci-lint run"], ["rust"] = ["cargo fmt --check", "cargo clippy"], ["php"] = ["php -l", "phpcs"], ["ruby"] = ["rubocop", "standardrb"], ["kotlin"] = ["./gradlew ktlintCheck", "./gradlew detekt"], ["swift"] = ["swiftformat --lint", "swiftlint"], ["sql"] = ["check destructive statements and transaction boundaries", "review index, constraint, and view impact"], }; private static readonly ReadOnlyCollection s_all = new(new List { new( "csharp", "C# (.NET)", [".cs", ".csx", ".csproj", ".sln"], [ "Use dotnet CLI, solution/project files, and NuGet package conventions.", "Follow Microsoft naming conventions and prefer targeted edits over broad rewrites.", "Verify impact on callers, DI registration, nullable flow, and build configuration." ], LspLanguageId: "csharp", ShowInQuickSelect: true, QuickSelectKey: "csharp", QuickSelectLabel: "C# (.NET)", QuickSelectIcon: "\uD83D\uDD39"), new( "python", "Python", [".py", ".pyi", ".ipynb"], [ "Use pip/venv or conda only if already available in the environment.", "Follow PEP 8, type hints, and module/package boundaries.", "Prefer small focused functions and verify import/runtime errors after edits." ], LspLanguageId: "python", ShowInQuickSelect: true, QuickSelectKey: "python", QuickSelectLabel: "Python", QuickSelectIcon: "\uD83D\uDC0D"), new( "java", "Java", [".java", ".gradle", ".pom"], [ "Use Maven or Gradle conventions already present in the repository.", "Follow package structure, visibility rules, and style consistent with the existing codebase.", "Check interfaces, implementations, and test fixtures together when modifying shared behavior." ], LspLanguageId: "java", ShowInQuickSelect: true, QuickSelectKey: "java", QuickSelectLabel: "Java", QuickSelectIcon: "\u2615"), new( "cpp", "C / C++", [".c", ".cc", ".cxx", ".cpp", ".h", ".hh", ".hpp", ".inl"], [ "Respect the repository's existing build system, usually CMake, MSBuild, or compiler-specific scripts.", "Be careful with headers, include order, ownership, ABI-sensitive changes, and platform guards.", "Validate both declaration and implementation impact when editing shared types." ], LspLanguageId: "cpp", ShowInQuickSelect: true, QuickSelectKey: "cpp", QuickSelectLabel: "C/C++", QuickSelectIcon: "\u2699"), new( "typescript", "TypeScript", [".ts", ".tsx", ".mts", ".cts"], [ "Use the existing package manager and tsconfig structure.", "Prefer explicit types on public boundaries and check build/lint config before changing module format.", "Preserve framework conventions already used by the project." ], LspLanguageId: "typescript"), new( "javascript", "JavaScript / Vue", [".js", ".jsx", ".mjs", ".cjs", ".vue"], [ "Use the existing Node package manager and lint/format rules.", "For Vue, preserve the current component style and API pattern used by the project.", "Check module boundaries, imports, and runtime side effects after edits." ], LspLanguageId: "javascript", ShowInQuickSelect: true, QuickSelectKey: "javascript", QuickSelectLabel: "JavaScript / Vue", QuickSelectIcon: "\uD83C\uDF10"), new( "go", "Go", [".go", ".mod", ".sum"], [ "Preserve package boundaries, error-first flow, and gofmt-style formatting.", "Check interfaces, exported identifiers, and concurrency-sensitive changes together." ], LspLanguageId: "go"), new( "rust", "Rust", [".rs", ".toml"], [ "Respect Cargo workspace structure, ownership/borrowing rules, and crate boundaries.", "Prefer explicit enums/results and verify compiler diagnostics after edits." ], LspLanguageId: "rust"), new( "php", "PHP", [".php", ".phtml"], [ "Follow the framework and autoloading structure already present in the project.", "Be careful with runtime includes, container wiring, and mixed template/application files." ], LspLanguageId: "php"), new( "ruby", "Ruby", [".rb", ".rake", ".gemspec"], [ "Preserve gem structure, Rails or plain Ruby conventions already used in the repository.", "Check dynamic dispatch, concerns/modules, and tests together after edits." ], LspLanguageId: "ruby"), new( "kotlin", "Kotlin", [".kt", ".kts"], [ "Preserve Gradle structure, package layout, and nullability intent.", "Be careful with JVM interop boundaries and Android-specific module structure when present." ], LspLanguageId: "kotlin"), new( "swift", "Swift", [".swift"], [ "Preserve target structure, Apple framework imports, and protocol-oriented design already in use.", "Check app lifecycle and platform-specific behavior after edits." ], LspLanguageId: "swift"), new( "scala", "Scala", [".scala", ".sc"], [ "Respect sbt/module structure, functional style, and existing typeclass or Akka patterns if present.", "Keep public APIs simple and avoid unnecessary type-level churn." ]), new( "shell", "Shell", [".sh", ".bash", ".zsh"], [ "Prefer safe quoting, explicit exit handling, and repository-local scripts over one-off inline shell.", "Check portability assumptions and environment-specific commands." ]), new( "powershell", "PowerShell", [".ps1", ".psm1", ".psd1", ".bat", ".cmd"], [ "Prefer native PowerShell cmdlets and safe path handling.", "Be careful with Windows-specific side effects, quoting, and admin-sensitive operations." ]), new( "sql", "SQL", [".sql"], [ "Preserve migration ordering, dialect assumptions, and transaction boundaries.", "Call out destructive DDL, broad DML, and index or constraint impact explicitly." ]), new( "web", "HTML / CSS / SCSS", [".html", ".htm", ".css", ".scss", ".sass", ".less", ".xaml"], [ "Preserve the existing design system, layout structure, and accessibility semantics.", "Prefer incremental visual changes and keep selectors/components scoped." ]), new( "markup", "JSON / YAML / XML / Markdown", [".json", ".jsonc", ".xml", ".yaml", ".yml", ".md", ".txt"], [ "Preserve schema shape, indentation style, and comment/document conventions already used in the repository.", "Validate references, keys, and generated consumer impact after edits." ]), }); private static readonly ReadOnlyDictionary s_byKey = new(s_all.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase)); private static readonly ReadOnlyDictionary s_byExtension = new(s_all .SelectMany(cap => cap.Extensions.Select(ext => new KeyValuePair(ext, cap))) .ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)); private static readonly HashSet s_codeExtensions = new( s_all.SelectMany(cap => cap.Extensions), StringComparer.OrdinalIgnoreCase); public static IReadOnlyList All => s_all; public static IReadOnlyCollection CodeExtensions => s_codeExtensions; public static IReadOnlyList QuickSelectLanguages => s_all.Where(x => x.ShowInQuickSelect).ToList(); public static IReadOnlyList LspBackedLanguages => s_all.Where(x => !string.IsNullOrWhiteSpace(x.LspLanguageId)).ToList(); public static bool IsCodeLikeFile(string? extension) => !string.IsNullOrWhiteSpace(extension) && s_codeExtensions.Contains(extension); public static CodeLanguageCapability? FindByKey(string? key) { if (string.IsNullOrWhiteSpace(key)) return null; return s_byKey.TryGetValue(key.Trim(), out var found) ? found : null; } public static CodeLanguageCapability? FindByExtension(string? extension) { if (string.IsNullOrWhiteSpace(extension)) return null; return s_byExtension.TryGetValue(extension.Trim(), out var found) ? found : null; } public static string? DetectLspLanguageId(string? filePath) => FindByExtension(Path.GetExtension(filePath ?? string.Empty))?.LspLanguageId; public static string GetQuickSelectLabel(string? key) => FindByKey(key)?.DisplayName ?? key ?? "Auto"; public static string BuildQuickSelectSupportDescription() => string.Join(", ", QuickSelectLanguages.Select(x => x.DisplayName)); public static string BuildSelectedLanguagePrompt(string? key) { if (string.IsNullOrWhiteSpace(key) || string.Equals(key, "auto", StringComparison.OrdinalIgnoreCase)) return string.Empty; var capability = FindByKey(key); if (capability == null) return string.Empty; return $"IMPORTANT: User selected language: {capability.DisplayName}. Prioritize this language for code analysis and generation."; } public static IEnumerable GetGuidanceLines(string? selectedKey) { var selected = FindByKey(selectedKey); if (selected != null) { foreach (var line in selected.Guidance) yield return $"- {selected.DisplayName}: {line}"; yield break; } foreach (var capability in s_all.Where(x => x.Key is "csharp" or "python" or "java" or "cpp" or "typescript" or "javascript" or "go" or "rust" or "kotlin" or "swift" or "sql")) { var summary = capability.Guidance.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(summary)) yield return $"- {capability.DisplayName}: {summary}"; } } public static string BuildLspSupportDescription() => string.Join(", ", LspBackedLanguages.Select(x => x.DisplayName)); public static string BuildStaticSupportDescription() => string.Join(", ", s_all.Select(x => x.DisplayName)); public static string BuildSupportSummaryDescription() => $"빠른 선택: {BuildQuickSelectSupportDescription()} | 내장 분석: {BuildStaticSupportDescription()} | LSP 심화 분석: {BuildLspSupportDescription()}"; public static string BuildCodeTabSupportDescription() { var sb = new StringBuilder(); sb.Append("정적 분류/검색/프롬프트 지원: "); sb.Append(BuildStaticSupportDescription()); sb.Append(" | LSP 심화 분석: "); sb.Append(BuildLspSupportDescription()); return sb.ToString(); } public static IReadOnlyList GetManifestHints(string? key) { var normalizedKey = FindByKey(key)?.Key; if (string.IsNullOrWhiteSpace(normalizedKey)) return []; return s_manifestHints.TryGetValue(normalizedKey, out var hints) ? hints : []; } public static IReadOnlyList GetBuildHints(string? key) { var normalizedKey = FindByKey(key)?.Key; if (string.IsNullOrWhiteSpace(normalizedKey)) return []; return s_buildHints.TryGetValue(normalizedKey, out var hints) ? hints : []; } public static IReadOnlyList GetTestHints(string? key) { var normalizedKey = FindByKey(key)?.Key; if (string.IsNullOrWhiteSpace(normalizedKey)) return []; return s_testHints.TryGetValue(normalizedKey, out var hints) ? hints : []; } public static IReadOnlyList GetLintHints(string? key) { var normalizedKey = FindByKey(key)?.Key; if (string.IsNullOrWhiteSpace(normalizedKey)) return []; return s_lintHints.TryGetValue(normalizedKey, out var hints) ? hints : []; } public static string BuildWorkflowSummary(string? key, int maxHintsPerKind = 2) { var capability = FindByKey(key); if (capability == null) return string.Empty; var parts = new List(); AppendHintBlock(parts, "manifests", GetManifestHints(capability.Key), maxHintsPerKind); AppendHintBlock(parts, "build", GetBuildHints(capability.Key), maxHintsPerKind); AppendHintBlock(parts, "test", GetTestHints(capability.Key), maxHintsPerKind); AppendHintBlock(parts, "lint", GetLintHints(capability.Key), maxHintsPerKind); var primaryGuidance = capability.Guidance.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(primaryGuidance)) parts.Add("focus: " + primaryGuidance); if (string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase)) parts.Add("analysis: detect dialect, script intent, destructive risk, migration order, and object dependencies"); if (parts.Count == 0) return capability.DisplayName; return $"{capability.DisplayName}: {string.Join(" | ", parts)}"; } public static IReadOnlyList BuildWorkspaceWorkflowSummaries( IEnumerable extensionsOrKeys, string? preferredKey = null, int maxLanguages = 3, int maxHintsPerKind = 2) { var results = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase); void TryAppend(CodeLanguageCapability? capability) { if (capability == null || !seen.Add(capability.Key)) return; var summary = BuildWorkflowSummary(capability.Key, maxHintsPerKind); if (!string.IsNullOrWhiteSpace(summary)) results.Add(summary); } TryAppend(ResolveCapabilityFromKeyOrExtension(preferredKey)); foreach (var value in extensionsOrKeys ?? []) { TryAppend(ResolveCapabilityFromKeyOrExtension(value)); if (results.Count >= Math.Max(1, maxLanguages)) break; } return results; } public static string BuildFallbackSupportDescription() => "LSP 서버가 없거나 연결되지 않아도 확장자, 매니페스트, build/test/lint 힌트 기반의 정적 fallback 분석을 계속 제공합니다."; public static string BuildFallbackSummary(string? filePathOrExtension) { var capability = ResolveCapabilityFromKeyOrExtension(filePathOrExtension); if (capability != null && string.Equals(capability.Key, "sql", StringComparison.OrdinalIgnoreCase)) return SqlAnalysisService.BuildFallbackSummary(filePathOrExtension); if (capability == null) return "정적 fallback: 확장자와 프로젝트 매니페스트를 먼저 확인하고 관련 build/test/lint 힌트를 따라 수동 검증하세요."; var sb = new StringBuilder(); sb.AppendLine($"정적 fallback 분석: {capability.DisplayName}"); if (s_manifestHints.TryGetValue(capability.Key, out var manifests) && manifests.Length > 0) sb.AppendLine("주요 매니페스트: " + string.Join(", ", manifests)); if (s_buildHints.TryGetValue(capability.Key, out var buildHints) && buildHints.Length > 0) sb.AppendLine("권장 build 힌트: " + string.Join(" | ", buildHints)); if (s_testHints.TryGetValue(capability.Key, out var testHints) && testHints.Length > 0) sb.AppendLine("권장 test 힌트: " + string.Join(" | ", testHints)); if (s_lintHints.TryGetValue(capability.Key, out var lintHints) && lintHints.Length > 0) sb.AppendLine("권장 lint/format 힌트: " + string.Join(" | ", lintHints)); var primaryGuidance = capability.Guidance.FirstOrDefault(); if (!string.IsNullOrWhiteSpace(primaryGuidance)) sb.Append("분석 포인트: ").Append(primaryGuidance); return sb.ToString().TrimEnd(); } private static CodeLanguageCapability? ResolveCapabilityFromKeyOrExtension(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; var normalized = value.Trim(); var capability = FindByKey(normalized); if (capability != null) return capability; if (normalized.StartsWith('.')) return FindByExtension(normalized); capability = QuickSelectLanguages .FirstOrDefault(x => string.Equals(x.QuickSelectKey, normalized, StringComparison.OrdinalIgnoreCase)); if (capability != null) return capability; var extension = Path.GetExtension(normalized); return string.IsNullOrWhiteSpace(extension) ? null : FindByExtension(extension); } private static void AppendHintBlock(List parts, string label, IReadOnlyList hints, int maxHintsPerKind) { if (hints.Count == 0) return; var limited = hints .Where(hint => !string.IsNullOrWhiteSpace(hint)) .Take(Math.Max(1, maxHintsPerKind)) .ToList(); if (limited.Count == 0) return; parts.Add($"{label}: {string.Join(", ", limited)}"); } }