diff --git a/CodeContextGenerator/App.xaml b/CodeContextGenerator/App.xaml index d67c08c..8ee2187 100644 --- a/CodeContextGenerator/App.xaml +++ b/CodeContextGenerator/App.xaml @@ -3,7 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:CodeContextGenerator.Converters" - StartupUri="Views/MainWindow.xaml"> + ShutdownMode="OnMainWindowClose"> diff --git a/CodeContextGenerator/App.xaml.cs b/CodeContextGenerator/App.xaml.cs index 0d908e9..3d753f7 100644 --- a/CodeContextGenerator/App.xaml.cs +++ b/CodeContextGenerator/App.xaml.cs @@ -1,8 +1,47 @@ using System.Windows; +using CodeContextGenerator.Services; +using CodeContextGenerator.ViewModels; +using Microsoft.Extensions.DependencyInjection; -namespace CodeContextGenerator +namespace CodeContextGenerator; + +public partial class App : Application { - public partial class App : Application + private readonly IServiceProvider _serviceProvider; + + public App() { + // Инициализируем сервисы + var serviceCollection = new ServiceCollection(); + + // Регистрация сервисов + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + + // Регистрация ViewModel + serviceCollection.AddTransient(); + + _serviceProvider = serviceCollection.BuildServiceProvider(); + } + + protected override void OnStartup(StartupEventArgs e) + { + // Создаем главное окно только один раз + var mainWindow = new Views.MainWindow(); + + // Устанавливаем DataContext + mainWindow.DataContext = _serviceProvider.GetRequiredService(); + + // Устанавливаем главное окно приложения + MainWindow = mainWindow; + + // Показываем окно + mainWindow.Show(); + + base.OnStartup(e); } } \ No newline at end of file diff --git a/CodeContextGenerator/CodeContextGenerator.csproj b/CodeContextGenerator/CodeContextGenerator.csproj index d9b5680..4e7b36e 100644 --- a/CodeContextGenerator/CodeContextGenerator.csproj +++ b/CodeContextGenerator/CodeContextGenerator.csproj @@ -10,6 +10,7 @@ + diff --git a/CodeContextGenerator/Models/FileItem.cs b/CodeContextGenerator/Models/FileItem.cs index f213d2f..caabda2 100644 --- a/CodeContextGenerator/Models/FileItem.cs +++ b/CodeContextGenerator/Models/FileItem.cs @@ -1,66 +1,56 @@ using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; -namespace CodeContextGenerator.Models +namespace CodeContextGenerator.Models; + +public partial class FileItem : ObservableObject { - public partial class FileItem : ObservableObject + [ObservableProperty] + private string name; + + [ObservableProperty] + private string fullName; + + [ObservableProperty] + private bool isDirectory; + + [ObservableProperty] + private bool? isSelected; + + [ObservableProperty] + private FileItem parent; + + public ObservableCollection Children { get; } = new ObservableCollection(); + + partial void OnIsSelectedChanged(bool? oldValue, bool? newValue) { - [ObservableProperty] - private string? name; - - [ObservableProperty] - private string? fullName; - - [ObservableProperty] - private bool isDirectory; - - [ObservableProperty] - private bool? isSelected; - - [ObservableProperty] - private FileItem? parent; - - public ObservableCollection Children { get; } = new ObservableCollection(); - - /*partial void OnIsSelectedChanged(bool? oldValue, bool? newValue) + if (newValue.HasValue) { - if (newValue.HasValue) - { - UpdateChildrenSelection(newValue.Value); - } - UpdateParentSelection(); - }*/ - - private void UpdateChildrenSelection(bool value) - { - if (!IsDirectory) return; - - foreach (var child in Children) - { - child.IsSelected = value; - } + UpdateChildrenSelection(newValue.Value); } + UpdateParentSelection(); + } - private void UpdateParentSelection() + private void UpdateChildrenSelection(bool value) + { + if (!IsDirectory) return; + + foreach (var child in Children) { - if (Parent == null) return; - - var children = Parent.Children; - var allSelected = children.All(c => c.IsSelected == true); - var noneSelected = children.All(c => c.IsSelected == false); - var hasIndeterminate = children.Any(c => c.IsSelected == null); - - Parent.IsSelected = hasIndeterminate ? null : (allSelected ? true : (noneSelected ? false : null)); - Parent.UpdateParentSelection(); - } - - public void ClearSelection() - { - IsSelected = false; - foreach (var child in Children) - { - child.ClearSelection(); - } + child.IsSelected = value; } } + + private void UpdateParentSelection() + { + if (Parent == null) return; + + var children = Parent.Children; + var allSelected = children.All(c => c.IsSelected == true); + var noneSelected = children.All(c => c.IsSelected == false); + var hasIndeterminate = children.Any(c => c.IsSelected == null); + + Parent.IsSelected = hasIndeterminate ? null : (allSelected ? true : (noneSelected ? false : null)); + Parent.UpdateParentSelection(); + } } \ No newline at end of file diff --git a/CodeContextGenerator/Services/ContextFileGenerator.cs b/CodeContextGenerator/Services/ContextFileGenerator.cs new file mode 100644 index 0000000..ead5f64 --- /dev/null +++ b/CodeContextGenerator/Services/ContextFileGenerator.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Text; + +namespace CodeContextGenerator.Services; + +public interface IContextFileGenerator +{ + Task GenerateContextFileAsync(List selectedFiles, string outputPath, string projectRootPath, IProgress progress, CancellationToken cancellationToken); +} + +public class ContextFileGenerator : IContextFileGenerator +{ + private readonly IFileProcessorService _fileProcessorService; + + public ContextFileGenerator(IFileProcessorService fileProcessorService) + { + _fileProcessorService = fileProcessorService; + } + + public async Task GenerateContextFileAsync(List selectedFiles, string outputPath, string projectRootPath, IProgress progress, CancellationToken cancellationToken) + { + var outputContent = new StringBuilder(); + int totalFiles = selectedFiles.Count; + int processedFiles = 0; + + foreach (var filePath in selectedFiles) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + string relativePath = Path.GetRelativePath(projectRootPath, filePath); + string fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8); + + // Обработка комментариев + fileContent = _fileProcessorService.ProcessFileContent(fileContent, Path.GetFileName(filePath)); + + outputContent.AppendLine($"=== Файл: {relativePath} ==="); + outputContent.AppendLine(fileContent); + outputContent.AppendLine(); // Пустая строка между файлами + + processedFiles++; + if (totalFiles > 0 && progress != null) + { + progress.Report((int)((processedFiles * 100.0) / totalFiles)); + } + } + catch (IOException ex) + { + throw new IOException($"Файл '{Path.GetFileName(filePath)}' заблокирован или недоступен: {ex.Message}", ex); + } + } + + // Сохраняем результат с кодировкой UTF-8 с BOM + var encoding = new UTF8Encoding(true); + await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding); + } +} \ No newline at end of file diff --git a/CodeContextGenerator/Services/FileProcessorService.cs b/CodeContextGenerator/Services/FileProcessorService.cs index 85c379f..e3d31e2 100644 --- a/CodeContextGenerator/Services/FileProcessorService.cs +++ b/CodeContextGenerator/Services/FileProcessorService.cs @@ -1,191 +1,193 @@ using System.Text; using System.Text.RegularExpressions; -namespace CodeContextGenerator.Services +namespace CodeContextGenerator.Services; + +public interface IFileProcessorService { - public static class FileProcessorService + string ProcessFileContent(string content, string fileName); +} + +public class FileProcessorService : IFileProcessorService +{ + public string ProcessFileContent(string content, string fileName) { - public static string ProcessFileContent(string content, string fileName) + if (fileName.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) { - if (fileName.EndsWith(".xaml", StringComparison.OrdinalIgnoreCase)) - { - return RemoveXamlComments(content); - } - else if (fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) - { - return RemoveCSharpComments(content); - } - return content; + return RemoveXamlComments(content); + } + else if (fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) || + fileName.EndsWith(".razor", StringComparison.OrdinalIgnoreCase)) + { + return RemoveCSharpComments(content); + } + return content; + } + + private string RemoveXamlComments(string content) + { + // Удаляем XAML комментарии + return Regex.Replace(content, @"", "", RegexOptions.Singleline | RegexOptions.Compiled); + } + + private string RemoveCSharpComments(string content) + { + var sb = new StringBuilder(); + var lines = content.Split('\n'); + + foreach (var line in lines) + { + string processedLine = ProcessCSharpLine(line); + sb.AppendLine(processedLine); } - private static string RemoveXamlComments(string content) + return RemoveMultiLineComments(sb.ToString()); + } + + private string ProcessCSharpLine(string line) + { + if (string.IsNullOrWhiteSpace(line)) + return line; + + // Удаляем XML-комментарии /// + if (line.TrimStart().StartsWith("///", StringComparison.Ordinal)) + return string.Empty; + + // Проверяем на однострочные комментарии + int commentIndex = line.IndexOf("//", StringComparison.Ordinal); + if (commentIndex >= 0) { - // Удаляем XAML комментарии - return Regex.Replace(content, @"", "", RegexOptions.Singleline | RegexOptions.Compiled); - } - - private static string RemoveCSharpComments(string content) - { - var sb = new StringBuilder(); - var lines = content.Split('\n'); - - foreach (var line in lines) - { - string processedLine = ProcessCSharpLine(line); - sb.AppendLine(processedLine); - } - - return sb.ToString(); - } - - private static string ProcessCSharpLine(string line) - { - if (string.IsNullOrWhiteSpace(line)) - return line; - - // Удаляем XML-комментарии /// - if (line.TrimStart().StartsWith("///", StringComparison.Ordinal)) - return string.Empty; - - // Проверяем на однострочные комментарии - int commentIndex = line.IndexOf("//", StringComparison.Ordinal); - if (commentIndex >= 0) - { - // Проверяем, не находится ли // внутри строки - bool inString = false; - bool inChar = false; - bool escapeNext = false; - - for (int i = 0; i < commentIndex; i++) - { - char c = line[i]; - - if (escapeNext) - { - escapeNext = false; - continue; - } - - if (c == '\\') - { - escapeNext = true; - continue; - } - - if (!inChar && c == '"') - { - inString = !inString; - continue; - } - - if (!inString && c == '\'') - { - inChar = !inChar; - continue; - } - } - - if (!inString && !inChar) - { - return line.Substring(0, commentIndex).TrimEnd(); - } - } - - return line.TrimEnd(); - } - - public static string RemoveMultiLineComments(string content) - { - var result = new StringBuilder(); - var stack = new Stack(); - bool inComment = false; + // Проверяем, не находится ли // внутри строки bool inString = false; bool inChar = false; bool escapeNext = false; - for (int i = 0; i < content.Length; i++) + for (int i = 0; i < commentIndex; i++) { - char c = content[i]; + char c = line[i]; if (escapeNext) { escapeNext = false; - if (!inComment) result.Append(c); continue; } if (c == '\\' && (inString || inChar)) { escapeNext = true; - if (!inComment) result.Append(c); continue; } - // Обработка строковых литералов - if (!inComment) + if (!inChar && c == '"') { - if (!inString && !inChar && c == '"') - { - inString = true; - result.Append(c); - continue; - } - - if (!inString && !inChar && c == '\'') - { - inChar = true; - result.Append(c); - continue; - } - - if (inString && c == '"') - { - inString = false; - result.Append(c); - continue; - } - - if (inChar && c == '\'') - { - inChar = false; - result.Append(c); - continue; - } - } - - if (inString || inChar) - { - result.Append(c); + inString = !inString; continue; } - // Обработка многострочных комментариев - if (i < content.Length - 1) + if (!inString && c == '\'') { - if (!inComment && c == '/' && content[i + 1] == '*') - { - inComment = true; - stack.Push(i); - i++; // Пропускаем следующий символ - continue; - } - - if (inComment && c == '*' && content[i + 1] == '/') - { - inComment = false; - stack.Pop(); - i++; // Пропускаем следующий символ - continue; - } - } - - if (!inComment) - { - result.Append(c); + inChar = !inChar; + continue; } } - return result.ToString(); + if (!inString && !inChar) + { + return line.Substring(0, commentIndex).TrimEnd(); + } } + + return line.TrimEnd(); + } + + private string RemoveMultiLineComments(string content) + { + var result = new StringBuilder(); + bool inComment = false; + bool inString = false; + bool inChar = false; + bool escapeNext = false; + + for (int i = 0; i < content.Length; i++) + { + char c = content[i]; + + if (escapeNext) + { + escapeNext = false; + if (!inComment) result.Append(c); + continue; + } + + if (c == '\\' && (inString || inChar)) + { + escapeNext = true; + if (!inComment) result.Append(c); + continue; + } + + // Обработка строковых литералов + if (!inComment) + { + if (!inString && !inChar && c == '"') + { + inString = true; + result.Append(c); + continue; + } + + if (!inString && !inChar && c == '\'') + { + inChar = true; + result.Append(c); + continue; + } + + if (inString && c == '"') + { + inString = false; + result.Append(c); + continue; + } + + if (inChar && c == '\'') + { + inChar = false; + result.Append(c); + continue; + } + } + + if (inString || inChar) + { + result.Append(c); + continue; + } + + // Обработка многострочных комментариев + if (i < content.Length - 1) + { + if (!inComment && c == '/' && content[i + 1] == '*') + { + inComment = true; + i++; // Пропускаем следующий символ + continue; + } + + if (inComment && c == '*' && content[i + 1] == '/') + { + inComment = false; + i++; // Пропускаем следующий символ + continue; + } + } + + if (!inComment) + { + result.Append(c); + } + } + + return result.ToString(); } } \ No newline at end of file diff --git a/CodeContextGenerator/Services/FileScannerService.cs b/CodeContextGenerator/Services/FileScannerService.cs new file mode 100644 index 0000000..fb14574 --- /dev/null +++ b/CodeContextGenerator/Services/FileScannerService.cs @@ -0,0 +1,128 @@ +using CodeContextGenerator.Models; +using System.IO; + +namespace CodeContextGenerator.Services; + +public interface IFileScannerService +{ + Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default); + List GetSelectedFiles(FileItem rootItem); +} + +public class FileScannerService : IFileScannerService +{ + private static readonly string[] ExcludedDirectories = { + "bin", "obj", ".git", "packages", ".vs", "Properties", + "node_modules", ".vscode", ".idea", "Debug", "Release", + "wwwroot", "dist", "build", "node_modules" + }; + + private static readonly string[] IncludedExtensions = { ".cs", ".xaml" }; + + public async Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + if (!Directory.Exists(path)) + return; + + var directories = Directory.GetDirectories(path) + .Where(d => !ExcludedDirectories.Any(ex => + d.EndsWith(ex, System.StringComparison.OrdinalIgnoreCase) || + Path.GetFileName(d).Equals(ex, System.StringComparison.OrdinalIgnoreCase))) + .ToList(); + + var files = Directory.GetFiles(path) + .Where(f => IncludedExtensions.Any(ext => + f.EndsWith(ext, System.StringComparison.OrdinalIgnoreCase))) + .ToList(); + + int totalItems = directories.Count + files.Count; + int processedItems = 0; + + // Обрабатываем поддиректории + foreach (var dir in directories) + { + cancellationToken.ThrowIfCancellationRequested(); + + var dirName = Path.GetFileName(dir); + var dirItem = new FileItem + { + Name = dirName, + FullName = dir, + IsDirectory = true, + Parent = parentItem, + IsSelected = false + }; + + await BuildDirectoryTreeAsync(dir, dirItem, progress, cancellationToken); + + // Добавляем директорию только если в ней есть файлы или поддиректории с файлами + if (dirItem.Children.Any()) + { + parentItem.Children.Add(dirItem); + } + + processedItems++; + if (totalItems > 0 && progress != null) + { + progress.Report((int)((processedItems * 100.0) / totalItems)); + } + } + + // Обрабатываем файлы + foreach (var file in files) + { + cancellationToken.ThrowIfCancellationRequested(); + + var fileItem = new FileItem + { + Name = Path.GetFileName(file), + FullName = file, + IsDirectory = false, + Parent = parentItem, + IsSelected = false + }; + + parentItem.Children.Add(fileItem); + processedItems++; + if (totalItems > 0 && progress != null) + { + progress.Report((int)((processedItems * 100.0) / totalItems)); + } + } + } + catch (IOException) + { + // Игнорируем ошибки доступа к директориям + } + catch (UnauthorizedAccessException) + { + // Игнорируем ошибки доступа + } + } + + public List GetSelectedFiles(FileItem rootItem) + { + var selectedFiles = new List(); + CollectSelectedFiles(rootItem, selectedFiles); + return selectedFiles; + } + + private void CollectSelectedFiles(FileItem item, List selectedFiles) + { + if (item.IsDirectory) + { + foreach (var child in item.Children) + { + CollectSelectedFiles(child, selectedFiles); + } + } + else if (item.IsSelected == true) + { + selectedFiles.Add(item.FullName); + } + } +} \ No newline at end of file diff --git a/CodeContextGenerator/Services/ProjectLoaderService.cs b/CodeContextGenerator/Services/ProjectLoaderService.cs new file mode 100644 index 0000000..a00d737 --- /dev/null +++ b/CodeContextGenerator/Services/ProjectLoaderService.cs @@ -0,0 +1,177 @@ +using CodeContextGenerator.Models; +using System.IO; +using System.Windows; + +namespace CodeContextGenerator.Services; + +public interface IProjectLoaderService +{ + Task LoadProjectFromPathAsync(string projectPath, IProgress progress, CancellationToken cancellationToken); + string GetDefaultOutputFileName(string projectPath); +} + +public class ProjectLoaderService : IProjectLoaderService +{ + private readonly IFileScannerService _fileScannerService; + + public ProjectLoaderService(IFileScannerService fileScannerService) + { + _fileScannerService = fileScannerService; + } + + public async Task LoadProjectFromPathAsync(string projectPath, IProgress progress, CancellationToken cancellationToken) + { + if (Directory.Exists(projectPath)) + { + return await LoadDirectoryAsProjectAsync(projectPath, progress, cancellationToken); + } + else if (File.Exists(projectPath)) + { + if (projectPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) + { + return await LoadSolutionAsync(projectPath, progress, cancellationToken); + } + else if (projectPath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) + { + return await LoadCsprojAsync(projectPath, progress, cancellationToken); + } + } + + throw new Exception("Указанный путь не является папкой проекта, решением (.sln) или проектом (.csproj)"); + } + + private async Task LoadDirectoryAsProjectAsync(string directoryPath, IProgress progress, CancellationToken cancellationToken) + { + var projectName = Path.GetFileName(directoryPath); + var rootItem = new FileItem + { + Name = projectName, + FullName = directoryPath, + IsDirectory = true, + IsSelected = false + }; + + await _fileScannerService.BuildDirectoryTreeAsync(directoryPath, rootItem, progress, cancellationToken); + return rootItem; + } + + private async Task LoadSolutionAsync(string solutionPath, IProgress progress, CancellationToken cancellationToken) + { + string solutionDir = Path.GetDirectoryName(solutionPath); + var solutionItem = new FileItem + { + Name = Path.GetFileName(solutionPath), + FullName = solutionPath, + IsDirectory = true, + IsSelected = false + }; + + try + { + var projectPaths = ParseSolutionProjects(solutionPath); + int totalProjects = projectPaths.Count; + int processedProjects = 0; + + foreach (var projectPath in projectPaths) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (File.Exists(projectPath)) + { + string projectDir = Path.GetDirectoryName(projectPath); + string projectName = Path.GetFileNameWithoutExtension(projectPath); + + var projectItem = new FileItem + { + Name = projectName, + FullName = projectDir, + IsDirectory = true, + Parent = solutionItem, + IsSelected = false + }; + + await _fileScannerService.BuildDirectoryTreeAsync(projectDir, projectItem, progress, cancellationToken); + + if (projectItem.Children.Any()) + { + solutionItem.Children.Add(projectItem); + } + } + + processedProjects++; + progress?.Report((int)((processedProjects * 100.0) / totalProjects)); + } + } + catch (Exception ex) + { + MessageBox.Show($"Ошибка при парсинге решения: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); + } + + return solutionItem; + } + + private async Task LoadCsprojAsync(string csprojPath, IProgress progress, CancellationToken cancellationToken) + { + string projectDir = Path.GetDirectoryName(csprojPath); + string projectName = Path.GetFileNameWithoutExtension(csprojPath); + + var projectItem = new FileItem + { + Name = projectName, + FullName = projectDir, + IsDirectory = true, + IsSelected = false + }; + + await _fileScannerService.BuildDirectoryTreeAsync(projectDir, projectItem, progress, cancellationToken); + return projectItem; + } + + private List ParseSolutionProjects(string solutionPath) + { + var projects = new List(); + string solutionDir = Path.GetDirectoryName(solutionPath); + + try + { + foreach (string line in File.ReadAllLines(solutionPath)) + { + if (line.Trim().StartsWith("Project(", StringComparison.OrdinalIgnoreCase)) + { + var parts = line.Split(new[] { '"' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 3) + { + string relativePath = parts[2].Trim(); + if (relativePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) + { + string absolutePath = Path.GetFullPath(Path.Combine(solutionDir, relativePath)); + if (File.Exists(absolutePath)) + { + projects.Add(absolutePath); + } + } + } + } + } + } + catch (Exception ex) + { + throw new Exception($"Ошибка парсинга файла решения: {ex.Message}", ex); + } + + return projects; + } + + public string GetDefaultOutputFileName(string projectPath) + { + if (Directory.Exists(projectPath)) + { + return $"{Path.GetFileName(projectPath)}_context.txt"; + } + else if (File.Exists(projectPath)) + { + return $"{Path.GetFileNameWithoutExtension(projectPath)}_context.txt"; + } + return "project_context.txt"; + } +} \ No newline at end of file diff --git a/CodeContextGenerator/Services/SettingsService.cs b/CodeContextGenerator/Services/SettingsService.cs new file mode 100644 index 0000000..a6dc06b --- /dev/null +++ b/CodeContextGenerator/Services/SettingsService.cs @@ -0,0 +1,62 @@ +using System.IO; +using System.Text.Json; + +namespace CodeContextGenerator.Services; + +public interface ISettingsService +{ + string GetLastProjectPath(); + void SaveLastProjectPath(string path); +} + +public class SettingsService : ISettingsService +{ + private const string SettingsFileName = "app_settings.json"; + private readonly string _settingsFilePath; + + public SettingsService() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + string appFolder = Path.Combine(appDataPath, "CodeContextGenerator"); + Directory.CreateDirectory(appFolder); + _settingsFilePath = Path.Combine(appFolder, SettingsFileName); + } + + public string GetLastProjectPath() + { + try + { + if (File.Exists(_settingsFilePath)) + { + var json = File.ReadAllText(_settingsFilePath); + var settings = JsonSerializer.Deserialize(json); + return settings?.LastProjectPath; + } + } + catch + { + // Игнорируем ошибки чтения настроек + } + return null; + } + + public void SaveLastProjectPath(string path) + { + try + { + var settings = new Settings { LastProjectPath = path }; + var options = new JsonSerializerOptions { WriteIndented = true }; + string json = JsonSerializer.Serialize(settings, options); + File.WriteAllText(_settingsFilePath, json); + } + catch + { + // Игнорируем ошибки сохранения настроек + } + } + + private class Settings + { + public string LastProjectPath { get; set; } + } +} diff --git a/CodeContextGenerator/Services/UIService.cs b/CodeContextGenerator/Services/UIService.cs new file mode 100644 index 0000000..24370c6 --- /dev/null +++ b/CodeContextGenerator/Services/UIService.cs @@ -0,0 +1,98 @@ +using Microsoft.Win32; +using System.IO; +using System.Windows; + +namespace CodeContextGenerator.Services; + +public interface IUIService +{ + string ShowFolderBrowserDialog(string initialDirectory = null); + bool ShowOpenProjectFileDialog(out string selectedPath); + bool ShowSaveFileDialog(string defaultFileName, string initialDirectory, out string savePath); + void ShowMessage(string message, string title, MessageBoxImage icon = MessageBoxImage.Information); +} + +public class UIService : IUIService +{ + public string ShowFolderBrowserDialog(string initialDirectory = null) + { + // Используем OpenFileDialog для выбора папки - это стандартный способ в WPF + var dialog = new OpenFileDialog + { + Title = "Выберите папку с проектом", + Filter = "Папки|*.folder", // Фильтр для отображения только папок + FileName = "select_folder", // Имя файла для обхода проверки существования файла + CheckFileExists = false, + CheckPathExists = true, + ValidateNames = false, // Отключаем валидацию имен для выбора папок + DereferenceLinks = true + }; + + if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) + { + dialog.InitialDirectory = initialDirectory; + } + + var result = dialog.ShowDialog(); + if (result == true) + { + // Возвращаем папку, а не файл + return Path.GetDirectoryName(dialog.FileName); + } + + return null; + } + + public bool ShowOpenProjectFileDialog(out string selectedPath) + { + selectedPath = null; + + var dialog = new OpenFileDialog + { + Title = "Выберите файл решения, проекта или папку", + Filter = "Все поддерживаемые файлы (*.sln;*.csproj;*.razor)|*.sln;*.csproj;*.razor|Файлы решений (*.sln)|*.sln|Файлы проектов (*.csproj)|*.csproj|Файлы Razor (*.razor)|*.razor|Все файлы (*.*)|*.*", + Multiselect = false + }; + + bool? result = dialog.ShowDialog(); + if (result == true) + { + selectedPath = dialog.FileName; + return true; + } + + return false; + } + + public bool ShowSaveFileDialog(string defaultFileName, string initialDirectory, out string savePath) + { + savePath = null; + + var dialog = new SaveFileDialog + { + Title = "Сохранить контекстный файл", + Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*", + FileName = defaultFileName, + DefaultExt = ".txt" + }; + + if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) + { + dialog.InitialDirectory = initialDirectory; + } + + bool? result = dialog.ShowDialog(); + if (result == true) + { + savePath = dialog.FileName; + return true; + } + + return false; + } + + public void ShowMessage(string message, string title, MessageBoxImage icon = MessageBoxImage.Information) + { + MessageBox.Show(message, title, MessageBoxButton.OK, icon); + } +} \ No newline at end of file diff --git a/CodeContextGenerator/ViewModels/MainViewModel.cs b/CodeContextGenerator/ViewModels/MainViewModel.cs index f585294..4007d39 100644 --- a/CodeContextGenerator/ViewModels/MainViewModel.cs +++ b/CodeContextGenerator/ViewModels/MainViewModel.cs @@ -2,285 +2,96 @@ using CodeContextGenerator.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Microsoft.Win32; -using System.ComponentModel; using System.IO; -using System.Text; using System.Windows; namespace CodeContextGenerator.ViewModels; -public partial class MainViewModel : ObservableObject, INotifyPropertyChanged +public partial class MainViewModel : ObservableObject { - [ObservableProperty] - private FileItem? _rootDirectory = new(); - [ObservableProperty] - private string _selectedProjectFilePath = string.Empty; - [ObservableProperty] - private bool _isProcessing = false; - [ObservableProperty] - private int _progressValue; - [ObservableProperty] - private string _progressText = string.Empty; - [ObservableProperty] - private string? _lastProjectPath = string.Empty; - [ObservableProperty] - private bool _isProjectLoaded; + private readonly IProjectLoaderService _projectLoaderService; + private readonly IFileScannerService _fileScannerService; + private readonly IContextFileGenerator _contextFileGenerator; + private readonly IUIService _uiService; + private readonly ISettingsService _settingsService; - private CancellationTokenSource _cancellationTokenSource = new(); + private CancellationTokenSource _cancellationTokenSource; - public bool CanSelectFolder => !IsProcessing; + [ObservableProperty] + private FileItem rootDirectory; + + [ObservableProperty] + private string selectedProjectPath; + + [ObservableProperty] + private bool isProcessing; + + [ObservableProperty] + private int progressValue; + + [ObservableProperty] + private string progressText; + + [ObservableProperty] + private bool isProjectLoaded; + + public MainViewModel( + IProjectLoaderService projectLoaderService, + IFileScannerService fileScannerService, + IContextFileGenerator contextFileGenerator, + IUIService uiService, + ISettingsService settingsService) + { + _projectLoaderService = projectLoaderService; + _fileScannerService = fileScannerService; + _contextFileGenerator = contextFileGenerator; + _uiService = uiService; + _settingsService = settingsService; + } + + public bool CanSelectProject => !IsProcessing; public bool CanGenerate => !IsProcessing && IsProjectLoaded && HasSelectedFiles(RootDirectory); - //public ICommand SelectProjectFileCommand { get; } - //public ICommand GenerateCommand { get; } - //public ICommand CancelCommand { get; } - //public ICommand ExitCommand { get; } - - public MainViewModel() + [RelayCommand(CanExecute = nameof(CanSelectProject))] + private void SelectProject() { - //ExitCommand = new RelayCommand(_ => Application.Current.Shutdown()); - LoadSettings(); - } + var initialDir = _settingsService.GetLastProjectPath(); - private bool HasSelectedFiles(FileItem item) - { - if (item == null) return false; - if (item.IsSelected == true && !item.IsDirectory) return true; - return item.Children.Any(HasSelectedFiles); - } - - //[RelayCommand(CanExecute = nameof(CanSelectFolder))] - [RelayCommand(CanExecute = nameof(CanSelectFolder))] - private void Exit() - { - Application.Current.Shutdown(); - } - - //[RelayCommand(CanExecute = nameof(CanSelectFolder))] - [RelayCommand(CanExecute = nameof(CanGenerate))] - private void Cancel() - { - CancelProcessing(); - } - - [RelayCommand(CanExecute = nameof(CanSelectFolder))] - private async Task SelectProjectFileAsync() - { - var dialog = new OpenFileDialog + // Сначала пробуем выбрать файл проекта/решения + if (_uiService.ShowOpenProjectFileDialog(out var filePath)) { - Title = "Выберите файл решения или проекта", - CheckFileExists = true, - CheckPathExists = true, - Filter = "Файлы решений (*.sln)|*.sln|Файлы проектов (*.csproj)|*.csproj|Все поддерживаемые (*.sln;*.csproj)|*.sln;*.csproj|Все файлы (*.*)|*.*", - InitialDirectory = !string.IsNullOrEmpty(LastProjectPath) && Directory.Exists(LastProjectPath) ? LastProjectPath : null - }; - - if (dialog.ShowDialog() != true) return; - - SelectedProjectFilePath = dialog.FileName; - LastProjectPath = Path.GetDirectoryName(SelectedProjectFilePath); - SaveSettings(); - - IsProjectLoaded = false; - await LoadProjectAsync(SelectedProjectFilePath); - } - - private async Task LoadProjectAsync(string projectFilePath) - { - IsProcessing = true; - ProgressText = "Загрузка проекта..."; - ProgressValue = 0; - - try - { - string? projectDirectory = Path.GetDirectoryName(projectFilePath); - var progress = new Progress(value => - { - ProgressValue = value; - ProgressText = $"Загрузка: {value}%"; - }); - - _cancellationTokenSource = new CancellationTokenSource(); - - if (projectFilePath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase)) - { - await LoadSolutionAsync(projectFilePath, progress, _cancellationTokenSource.Token); - } - else if (projectFilePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase)) - { - await LoadProjectDirectoryAsync(projectDirectory, progress, _cancellationTokenSource.Token); - } - - IsProjectLoaded = true; - ProgressText = "Проект загружен успешно"; - } - catch (OperationCanceledException) - { - ProgressText = "Загрузка отменена"; - RootDirectory = null; - IsProjectLoaded = false; - } - catch (Exception ex) - { - MessageBox.Show($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); - RootDirectory = null; - IsProjectLoaded = false; - } - finally - { - IsProcessing = false; - } - } - - private async Task LoadSolutionAsync(string solutionPath, IProgress progress, CancellationToken cancellationToken) - { - try - { - string? solutionDirectory = Path.GetDirectoryName(solutionPath); - var solutionProjects = ParseSolutionProjects(solutionPath); - - var solutionItem = new FileItem - { - Name = Path.GetFileName(solutionPath), - FullName = solutionPath, - IsDirectory = true, - IsSelected = false - }; - - int totalProjects = solutionProjects.Count; - int processedProjects = 0; - - foreach (var projectPath in solutionProjects) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (File.Exists(projectPath)) - { - string? projectDir = Path.GetDirectoryName(projectPath); - string? projectName = Path.GetFileName(projectDir); - - var projectItem = new FileItem - { - Name = projectName, - FullName = projectDir, - IsDirectory = true, - Parent = solutionItem, - IsSelected = false - }; - - await ProjectScannerService.BuildDirectoryTreeAsync(projectDir, projectItem, progress, cancellationToken); - - if (projectItem.Children.Any()) - { - solutionItem.Children.Add(projectItem); - } - } - - processedProjects++; - progress?.Report((int)((processedProjects * 100.0) / totalProjects)); - } - - RootDirectory = solutionItem; - - // После загрузки решения - не выбираем ничего по умолчанию - ClearAllSelections(solutionItem); - } - catch (Exception ex) - { - throw new Exception($"Ошибка при обработке решения: {ex.Message}", ex); - } - } - - private static List ParseSolutionProjects(string solutionPath) - { - var projects = new List(); - try - { - string? solutionDirectory = Path.GetDirectoryName(solutionPath); - - foreach (string line in File.ReadAllLines(solutionPath)) - { - if (line.Trim().StartsWith("Project(", StringComparison.OrdinalIgnoreCase)) - { - var parts = line.Split(['"'], StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 3) - { - string relativePath = parts[2].Trim(); - string absolutePath = Path.GetFullPath(Path.Combine(solutionDirectory, relativePath)); - - if (absolutePath.EndsWith(".csproj", StringComparison.OrdinalIgnoreCase) && File.Exists(absolutePath)) - { - projects.Add(absolutePath); - } - } - } - } - } - catch (Exception ex) - { - throw new Exception($"Ошибка парсинга файла решения: {ex.Message}", ex); - } - return projects; - } - - private async Task LoadProjectDirectoryAsync(string projectDirectory, IProgress progress, CancellationToken cancellationToken) - { - try - { - var projectName = Path.GetFileName(projectDirectory); - var rootItem = new FileItem - { - Name = projectName, - FullName = projectDirectory, - IsDirectory = true, - IsSelected = false - }; - - await ProjectScannerService.BuildDirectoryTreeAsync(projectDirectory, rootItem, progress, cancellationToken); - - RootDirectory = rootItem; - - // После загрузки проекта - не выбираем ничего по умолчанию - ClearAllSelections(rootItem); - } - catch (Exception ex) - { - throw new Exception($"Ошибка при загрузке проекта: {ex.Message}", ex); - } - } - - private static void ClearAllSelections(FileItem item) - { - item.IsSelected = false; - foreach (var child in item.Children) - { - ClearAllSelections(child); - } - } - - [RelayCommand(CanExecute = nameof(CanGenerate))] - private async Task GenerateFileAsync() - { - var selectedFiles = GetSelectedFiles(RootDirectory); - if (selectedFiles.Count == 0) - { - MessageBox.Show("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning); + LoadProject(filePath); return; } - var saveDialog = new SaveFileDialog + // Если отменили выбор файла - предлагаем выбрать папку + var folderPath = _uiService.ShowFolderBrowserDialog(initialDir); + if (!string.IsNullOrEmpty(folderPath)) { - Title = "Сохранить контекстный файл", - Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*", - FileName = $"{Path.GetFileNameWithoutExtension(SelectedProjectFilePath)}_context.txt", - DefaultExt = ".txt", - InitialDirectory = LastProjectPath - }; + LoadProject(folderPath); + } + } - if (saveDialog.ShowDialog() != true) return; + [RelayCommand(CanExecute = nameof(CanGenerate))] + private async Task GenerateContextFileAsync() + { + var selectedFiles = _fileScannerService.GetSelectedFiles(RootDirectory); + if (selectedFiles.Count == 0) + { + _uiService.ShowMessage("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxImage.Warning); + return; + } + + var defaultFileName = _projectLoaderService.GetDefaultOutputFileName(SelectedProjectPath); + var initialDir = _settingsService.GetLastProjectPath(); + + if (!_uiService.ShowSaveFileDialog(defaultFileName, initialDir, out var savePath)) + return; IsProcessing = true; + ProgressText = "Генерация контекстного файла..."; + ProgressValue = 0; + try { _cancellationTokenSource = new CancellationTokenSource(); @@ -290,8 +101,14 @@ public partial class MainViewModel : ObservableObject, INotifyPropertyChanged ProgressText = $"Генерация: {value}%"; }); - await GenerateContextFileAsync(selectedFiles, saveDialog.FileName, progress, _cancellationTokenSource.Token); - MessageBox.Show($"Файл успешно создан:\n{saveDialog.FileName}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information); + await _contextFileGenerator.GenerateContextFileAsync( + selectedFiles, + savePath, + Path.GetDirectoryName(SelectedProjectPath), + progress, + _cancellationTokenSource.Token); + + _uiService.ShowMessage($"Файл успешно создан:\n{savePath}", "Успех"); } catch (OperationCanceledException) { @@ -299,11 +116,11 @@ public partial class MainViewModel : ObservableObject, INotifyPropertyChanged } catch (IOException ex) { - MessageBox.Show($"Ошибка доступа к файлу: {ex.Message}", "Ошибка доступа", MessageBoxButton.OK, MessageBoxImage.Error); + _uiService.ShowMessage($"Ошибка доступа к файлу: {ex.Message}\nПожалуйста, закройте файлы или предоставьте необходимые права доступа.", "Ошибка доступа", MessageBoxImage.Error); } - catch (Exception ex) + catch (System.Exception ex) { - MessageBox.Show($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); + _uiService.ShowMessage($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxImage.Error); } finally { @@ -311,121 +128,81 @@ public partial class MainViewModel : ObservableObject, INotifyPropertyChanged } } - private List GetSelectedFiles(FileItem rootItem) - { - var selectedFiles = new List(); - CollectSelectedFiles(rootItem, selectedFiles); - return selectedFiles; - } - - private static void CollectSelectedFiles(FileItem item, List selectedFiles) - { - if (item.IsDirectory) - { - foreach (var child in item.Children) - { - CollectSelectedFiles(child, selectedFiles); - } - } - else if (item.IsSelected == true) - { - selectedFiles.Add(item.FullName); - } - } - - private async Task GenerateContextFileAsync(List selectedFiles, string outputPath, IProgress progress, CancellationToken cancellationToken) - { - var outputContent = new StringBuilder(); - int totalFiles = selectedFiles.Count; - int processedFiles = 0; - - foreach (var filePath in selectedFiles) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - string? projectDir = Path.GetDirectoryName(SelectedProjectFilePath); - string? relativePath = Path.GetRelativePath(projectDir, filePath); - string? fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8); - - // Обработка комментариев - fileContent = FileProcessorService.ProcessFileContent(fileContent, Path.GetFileName(filePath)); - fileContent = FileProcessorService.RemoveMultiLineComments(fileContent); - - outputContent.AppendLine($"=== Файл: {relativePath} ==="); - outputContent.AppendLine(fileContent); - outputContent.AppendLine(); // Пустая строка между файлами - - processedFiles++; - int progressValue = (int)((processedFiles * 100.0) / totalFiles); - progress?.Report(progressValue); - } - catch (IOException ex) - { - throw new IOException($"Файл '{Path.GetFileName(filePath)}' заблокирован или недоступен: {ex.Message}", ex); - } - } - - // Сохраняем результат с кодировкой UTF-8 с BOM - var encoding = new UTF8Encoding(true); - await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding); - } - [RelayCommand] private void CancelProcessing() { _cancellationTokenSource?.Cancel(); } - private void LoadSettings() + [RelayCommand] + private void ExitApplication() { + Application.Current.Shutdown(); + } + + private void LoadProject(string projectPath) + { + SelectedProjectPath = projectPath; + _settingsService.SaveLastProjectPath(Path.GetDirectoryName(projectPath)); + + IsProjectLoaded = false; + LoadProjectAsync(projectPath); + } + + private async void LoadProjectAsync(string projectPath) + { + IsProcessing = true; + ProgressText = "Загрузка проекта..."; + ProgressValue = 0; + try { - LastProjectPath = Properties.Settings.Default.LastProjectPath; + var progress = new Progress(value => + { + ProgressValue = value; + ProgressText = $"Загрузка: {value}%"; + }); + + _cancellationTokenSource = new CancellationTokenSource(); + RootDirectory = await _projectLoaderService.LoadProjectFromPathAsync(projectPath, progress, _cancellationTokenSource.Token); + + IsProjectLoaded = true; + ProgressText = "Проект загружен успешно"; + + // Сбрасываем выделение после загрузки + ClearSelections(RootDirectory); } - catch + catch (OperationCanceledException) { - LastProjectPath = string.Empty; + ProgressText = "Загрузка отменена"; } - } - - private void SaveSettings() - { - try + catch (System.Exception ex) { - Properties.Settings.Default.LastProjectPath = LastProjectPath; - Properties.Settings.Default.Save(); + _uiService.ShowMessage($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxImage.Error); + } + finally + { + IsProcessing = false; } - catch { } } - /*public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + private void ClearSelections(FileItem item) { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - }*/ -} - -/* -public class RelayCommand : ICommand -{ - private readonly Action _execute; - private readonly Func _canExecute; - - public event EventHandler? CanExecuteChanged - { - add => CommandManager.RequerySuggested += value; - remove => CommandManager.RequerySuggested -= value; + item.IsSelected = false; + foreach (var child in item.Children) + { + ClearSelections(child); + } } - public RelayCommand(Action execute, Func canExecute = null) + private bool HasSelectedFiles(FileItem item) { - _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - _canExecute = canExecute; + if (item == null) return false; + if (item.IsSelected == true && !item.IsDirectory) return true; + foreach (var child in item.Children) + { + if (HasSelectedFiles(child)) return true; + } + return false; } - - public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); - public void Execute(object parameter) => _execute(parameter); -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/CodeContextGenerator/Views/MainWindow.xaml b/CodeContextGenerator/Views/MainWindow.xaml index cf64e66..9738fb8 100644 --- a/CodeContextGenerator/Views/MainWindow.xaml +++ b/CodeContextGenerator/Views/MainWindow.xaml @@ -1,95 +1,68 @@ - + - - - - - + + + + + - + - -