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..0cf5e1c 100644 --- a/CodeContextGenerator/App.xaml.cs +++ b/CodeContextGenerator/App.xaml.cs @@ -1,8 +1,48 @@ -using System.Windows; +using CodeContextGenerator.Interfaces; +using CodeContextGenerator.Services; +using CodeContextGenerator.ViewModels; +using Microsoft.Extensions.DependencyInjection; +using System.Windows; -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 cdf319c..4e7b36e 100644 --- a/CodeContextGenerator/CodeContextGenerator.csproj +++ b/CodeContextGenerator/CodeContextGenerator.csproj @@ -8,6 +8,11 @@ true + + + + + True diff --git a/CodeContextGenerator/Converters/BooleanToVisibilityConverter.cs b/CodeContextGenerator/Converters/BooleanToVisibilityConverter.cs index 7a27bbe..29674cf 100644 --- a/CodeContextGenerator/Converters/BooleanToVisibilityConverter.cs +++ b/CodeContextGenerator/Converters/BooleanToVisibilityConverter.cs @@ -1,24 +1,26 @@ -using System; -using System.Globalization; +using System.Globalization; using System.Windows; using System.Windows.Data; -namespace CodeContextGenerator.Converters +namespace CodeContextGenerator.Converters; + +public class BooleanToVisibilityConverter : IValueConverter { - public class BooleanToVisibilityConverter : IValueConverter + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + bool boolValue = value is true; + + // Проверяем параметр для инвертирования + if (parameter is string paramStr && paramStr.Equals("Inverted", StringComparison.OrdinalIgnoreCase)) { - if (value is bool boolValue) - { - return boolValue ? Visibility.Visible : Visibility.Collapsed; - } - return Visibility.Collapsed; + boolValue = !boolValue; } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - throw new NotImplementedException(); - } + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/CodeContextGenerator/Interfaces/IContextFileGenerator.cs b/CodeContextGenerator/Interfaces/IContextFileGenerator.cs new file mode 100644 index 0000000..eb178da --- /dev/null +++ b/CodeContextGenerator/Interfaces/IContextFileGenerator.cs @@ -0,0 +1,6 @@ +namespace CodeContextGenerator.Interfaces; + +public interface IContextFileGenerator +{ + Task GenerateContextFileAsync(List selectedFiles, string outputPath, string projectRootPath, IProgress progress, CancellationToken cancellationToken); +} diff --git a/CodeContextGenerator/Interfaces/IFileProcessorService.cs b/CodeContextGenerator/Interfaces/IFileProcessorService.cs new file mode 100644 index 0000000..624a773 --- /dev/null +++ b/CodeContextGenerator/Interfaces/IFileProcessorService.cs @@ -0,0 +1,6 @@ +namespace CodeContextGenerator.Interfaces; + +public interface IFileProcessorService +{ + string ProcessFileContent(string content, string fileName); +} diff --git a/CodeContextGenerator/Interfaces/IFileScannerService.cs b/CodeContextGenerator/Interfaces/IFileScannerService.cs new file mode 100644 index 0000000..6225c27 --- /dev/null +++ b/CodeContextGenerator/Interfaces/IFileScannerService.cs @@ -0,0 +1,9 @@ +using CodeContextGenerator.Models; + +namespace CodeContextGenerator.Interfaces; + +public interface IFileScannerService +{ + Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default); + List GetSelectedFiles(FileItem rootItem); +} diff --git a/CodeContextGenerator/Interfaces/IProjectLoaderService.cs b/CodeContextGenerator/Interfaces/IProjectLoaderService.cs new file mode 100644 index 0000000..f44039c --- /dev/null +++ b/CodeContextGenerator/Interfaces/IProjectLoaderService.cs @@ -0,0 +1,9 @@ +using CodeContextGenerator.Models; + +namespace CodeContextGenerator.Interfaces; + +public interface IProjectLoaderService +{ + Task LoadProjectFromPathAsync(string projectPath, IProgress progress, CancellationToken cancellationToken); + string GetDefaultOutputFileName(string projectPath); +} diff --git a/CodeContextGenerator/Interfaces/ISettingsService.cs b/CodeContextGenerator/Interfaces/ISettingsService.cs new file mode 100644 index 0000000..6175d7b --- /dev/null +++ b/CodeContextGenerator/Interfaces/ISettingsService.cs @@ -0,0 +1,7 @@ +namespace CodeContextGenerator.Interfaces; + +public interface ISettingsService +{ + string GetLastProjectPath(); + void SaveLastProjectPath(string path); +} diff --git a/CodeContextGenerator/Interfaces/IUIService.cs b/CodeContextGenerator/Interfaces/IUIService.cs new file mode 100644 index 0000000..31a263c --- /dev/null +++ b/CodeContextGenerator/Interfaces/IUIService.cs @@ -0,0 +1,11 @@ +using System.Windows; + +namespace CodeContextGenerator.Interfaces; + +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); +} diff --git a/CodeContextGenerator/Models/FileItem.cs b/CodeContextGenerator/Models/FileItem.cs index d711428..d3904bc 100644 --- a/CodeContextGenerator/Models/FileItem.cs +++ b/CodeContextGenerator/Models/FileItem.cs @@ -1,60 +1,103 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Runtime.CompilerServices; +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; -namespace CodeContextGenerator.Models +namespace CodeContextGenerator.Models; + +public partial class FileItem : ObservableObject { - public class FileItem : INotifyPropertyChanged + [ObservableProperty] + private string name; + + [ObservableProperty] + private string fullName; + + [ObservableProperty] + private bool isDirectory; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasSelectedChildren))] + private bool? isSelected; + + [ObservableProperty] + private FileItem parent; + + public ObservableCollection Children { get; } = new ObservableCollection(); + + // Событие, вызываемое при изменении состояния выбора + public event EventHandler SelectionChanged; + + public bool HasSelectedChildren => Children.Any(c => c.IsSelected == true || c.HasSelectedChildren); + + partial void OnIsSelectedChanged(bool? oldValue, bool? newValue) { - public string Name { get; set; } - public string FullName { get; set; } - public bool IsDirectory { get; set; } - public ObservableCollection Children { get; set; } = new ObservableCollection(); - public FileItem Parent { get; set; } - - private bool? _isSelected; - public bool? IsSelected + if (newValue.HasValue) { - get => _isSelected; - set + UpdateChildrenSelection(newValue.Value); + } + UpdateParentSelection(); + NotifySelectionChanged(); + } + + private void UpdateChildrenSelection(bool value) + { + if (!IsDirectory) return; + + foreach (var child in Children) + { + // Гарантируем вызов события для каждого ребенка + var oldValue = child.IsSelected; + child.IsSelected = value; + if (oldValue != child.IsSelected) { - if (_isSelected != value) - { - _isSelected = value; - OnPropertyChanged(); - UpdateParentSelection(); - UpdateChildrenSelection(value); - } - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - - private void UpdateParentSelection() - { - if (Parent == null) return; - - var children = Parent.Children.ToList(); - var allSelected = children.All(c => c.IsSelected == true); - var noneSelected = children.All(c => c.IsSelected == false); - - Parent.IsSelected = allSelected ? true : (noneSelected ? false : null); - Parent.UpdateParentSelection(); - } - - private void UpdateChildrenSelection(bool? value) - { - if (!IsDirectory || !value.HasValue) return; - - foreach (var child in Children) - { - child.IsSelected = value; + child.RaiseSelectionChanged(); } } } + + 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); + + bool? newParentState = hasIndeterminate ? null : (allSelected ? true : (noneSelected ? false : null)); + /*bool? newParentState; + if (hasIndeterminate) + { + newParentState = null; + } + else if (allSelected) + { + newParentState = true; + } + else if (noneSelected) + { + newParentState = false; + } + else + { + newParentState = null; + }*/ + + if (Parent.IsSelected != newParentState) + { + Parent.IsSelected = newParentState; + Parent.RaiseSelectionChanged(); + } + } + + private void NotifySelectionChanged() + { + RaiseSelectionChanged(); + } + + // Публичный метод для гарантии вызова события + public void RaiseSelectionChanged() + { + // Вызываем событие для текущего элемента + SelectionChanged?.Invoke(this, EventArgs.Empty); + } } \ No newline at end of file diff --git a/CodeContextGenerator/Services/ContextFileGenerator.cs b/CodeContextGenerator/Services/ContextFileGenerator.cs new file mode 100644 index 0000000..3b9a280 --- /dev/null +++ b/CodeContextGenerator/Services/ContextFileGenerator.cs @@ -0,0 +1,54 @@ +using CodeContextGenerator.Interfaces; +using System.IO; +using System.Text; + +namespace CodeContextGenerator.Services; + +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..22bce9d 100644 --- a/CodeContextGenerator/Services/FileProcessorService.cs +++ b/CodeContextGenerator/Services/FileProcessorService.cs @@ -1,191 +1,189 @@ -using System.Text; +using CodeContextGenerator.Interfaces; +using System.Text; using System.Text.RegularExpressions; -namespace CodeContextGenerator.Services +namespace CodeContextGenerator.Services; + +public class FileProcessorService : IFileProcessorService { - public static class FileProcessorService + 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..19c2e27 --- /dev/null +++ b/CodeContextGenerator/Services/FileScannerService.cs @@ -0,0 +1,123 @@ +using CodeContextGenerator.Interfaces; +using CodeContextGenerator.Models; +using System.IO; + +namespace CodeContextGenerator.Services; + +public class FileScannerService : IFileScannerService +{ + private static readonly string[] ExcludedDirectories = { + "bin", "obj", ".git", "packages", ".vs", "Properties", + "node_modules", ".vscode", ".idea", "Debug", "Release", + "wwwroot", "dist", "build", ".gitignore", ".dockerignore" + }; + + 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, StringComparison.OrdinalIgnoreCase) || + Path.GetFileName(d).Equals(ex, StringComparison.OrdinalIgnoreCase))) + .ToList(); + + var files = Directory.GetFiles(path) + .Where(f => IncludedExtensions.Any(ext => + f.EndsWith(ext, 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.Count > 0 || files.Count > 0) + { + 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..f23cdb0 --- /dev/null +++ b/CodeContextGenerator/Services/ProjectLoaderService.cs @@ -0,0 +1,183 @@ +using CodeContextGenerator.Interfaces; +using CodeContextGenerator.Models; +using System.IO; +using System.Text.RegularExpressions; +using System.Windows; + +namespace CodeContextGenerator.Services; + +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, solutionDir); + 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++; + if (totalProjects > 0 && progress != null) + { + progress.Report((int)((processedProjects * 100.0) / totalProjects)); + } + } + } + catch (Exception ex) + { + MessageBox.Show($"Ошибка при парсинге решения: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); + } + + return solutionItem; + } + + // Улучшенный парсинг .sln файлов + private List ParseSolutionProjects(string solutionPath, string solutionDir) + { + var projects = new List(); + var projectRegex = new Regex(@"Project\(""[^""]*""\)\s*=\s*""([^""]*)"",\s*""([^""]*)""", RegexOptions.IgnoreCase); + + try + { + foreach (string line in File.ReadAllLines(solutionPath)) + { + var match = projectRegex.Match(line); + if (match.Success && match.Groups.Count >= 3) + { + string projectName = match.Groups[1].Value; + string relativePath = match.Groups[2].Value; + + // Пропускаем служебные проекты + if (projectName.Contains("Solution Items") || + relativePath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase) || + relativePath.EndsWith(".suo", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + string absolutePath = Path.GetFullPath(Path.Combine(solutionDir, relativePath)); + + // Проверяем, что это .csproj файл + 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 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; + } + + 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/ProjectScannerService.cs b/CodeContextGenerator/Services/ProjectScannerService.cs index 1af58a4..f71077c 100644 --- a/CodeContextGenerator/Services/ProjectScannerService.cs +++ b/CodeContextGenerator/Services/ProjectScannerService.cs @@ -1,39 +1,33 @@ -using System.IO; -using CodeContextGenerator.Models; +using CodeContextGenerator.Models; +using System.IO; namespace CodeContextGenerator.Services; public static class ProjectScannerService { - private static readonly string[] ExcludedDirectories = { "bin", "obj", ".git", "packages", ".vs", "Properties", "node_modules", ".vscode" }; + private static readonly string[] ExcludedDirectories = { + "bin", "obj", ".git", "packages", ".vs", "Properties", + "node_modules", ".vscode", ".idea", ".vs", "Debug", "Release" + }; + private static readonly string[] IncludedExtensions = { ".cs", ".xaml" }; - public static async Task ScanProjectDirectoryAsync(string rootPath, IProgress progress = null, CancellationToken cancellationToken = default) - { - var rootItem = new FileItem - { - Name = Path.GetFileName(rootPath), - FullName = rootPath, - IsDirectory = true, - IsSelected = false - }; - - await BuildDirectoryTreeAsync(rootPath, rootItem, progress, cancellationToken); - return rootItem; - } - - private static async Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default) + public static 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, StringComparison.OrdinalIgnoreCase))) + .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, StringComparison.OrdinalIgnoreCase))) + .Where(f => IncludedExtensions.Any(ext => f.EndsWith(ext, System.StringComparison.OrdinalIgnoreCase))) .ToList(); int totalItems = directories.Count + files.Count; @@ -85,32 +79,13 @@ public static class ProjectScannerService progress?.Report((int)((processedItems * 100.0) / totalItems)); } } - catch (Exception ex) + catch (IOException) { - // Логируем ошибку, но продолжаем работу - Console.WriteLine($"Error scanning directory {path}: {ex.Message}"); + // Игнорируем ошибки доступа к директориям } - } - - public static List GetSelectedFiles(FileItem rootItem) - { - var selectedFiles = new List(); - CollectSelectedFiles(rootItem, selectedFiles); - return selectedFiles; - } - - private static void CollectSelectedFiles(FileItem item, List selectedFiles) - { - if (item.IsDirectory) + catch (UnauthorizedAccessException) { - 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/SettingsService.cs b/CodeContextGenerator/Services/SettingsService.cs new file mode 100644 index 0000000..9891dd0 --- /dev/null +++ b/CodeContextGenerator/Services/SettingsService.cs @@ -0,0 +1,57 @@ +using CodeContextGenerator.Interfaces; +using System.IO; +using System.Text.Json; + +namespace CodeContextGenerator.Services; + +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..e2f652f --- /dev/null +++ b/CodeContextGenerator/Services/UIService.cs @@ -0,0 +1,110 @@ +using CodeContextGenerator.Interfaces; +using Microsoft.Win32; +using System.IO; +using System.Windows; + +namespace CodeContextGenerator.Services; + +public class UIService : IUIService +{ + private readonly ISettingsService _settingsService; + + public UIService(ISettingsService settingsService) + { + _settingsService = settingsService; + } + + public string ShowFolderBrowserDialog(string initialDirectory = null) + { + // Используем специальный трюк для выбора папки в WPF + var dialog = new OpenFileDialog + { + Title = "Выберите папку с проектом", + Filter = "Папки|*.dummy", // Фильтр для отображения только папок + FileName = "выберите_папку", // Специальное имя файла + CheckFileExists = false, + CheckPathExists = true, + ValidateNames = false, + DereferenceLinks = true + }; + + if (string.IsNullOrEmpty(initialDirectory) || !Directory.Exists(initialDirectory)) + { + initialDirectory = _settingsService.GetLastProjectPath(); + } + + if (!string.IsNullOrEmpty(initialDirectory) && Directory.Exists(initialDirectory)) + { + dialog.InitialDirectory = initialDirectory; + } + + var result = dialog.ShowDialog(); + if (result == true) + { + // Возвращаем директорию, а не путь к файлу + string selectedFolder = Path.GetDirectoryName(dialog.FileName); + return selectedFolder; + } + + return null; + } + + public bool ShowOpenProjectFileDialog(out string selectedPath) + { + selectedPath = null; + + var dialog = new OpenFileDialog + { + Title = "Выберите файл решения или проекта", + Filter = "Все поддерживаемые файлы (*.sln;*.csproj)|*.sln;*.csproj|Файлы решений Visual Studio (*.sln)|*.sln|Файлы проектов C# (*.csproj)|*.csproj|Все файлы (*.*)|*.*", + Multiselect = false + }; + + var lastPath = _settingsService.GetLastProjectPath(); + if (!string.IsNullOrEmpty(lastPath) && Directory.Exists(lastPath)) + { + dialog.InitialDirectory = lastPath; + } + + 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 21de91a..6357192 100644 --- a/CodeContextGenerator/ViewModels/MainViewModel.cs +++ b/CodeContextGenerator/ViewModels/MainViewModel.cs @@ -1,344 +1,282 @@ -using CodeContextGenerator.Models; +using CodeContextGenerator.Interfaces; +using CodeContextGenerator.Models; using CodeContextGenerator.Services; -using Microsoft.Win32; -using System.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; -using System.Text; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using System.Windows; -using System.Windows.Input; namespace CodeContextGenerator.ViewModels { - public class MainViewModel : INotifyPropertyChanged + public partial class MainViewModel : ObservableObject { - private FileItem _rootDirectory; - private string _selectedFolderPath; - private bool _isProcessing; - private int _progressValue; - private string _progressText; + private readonly IProjectLoaderService _projectLoaderService; + private readonly IFileScannerService _fileScannerService; + private readonly IContextFileGenerator _contextFileGenerator; + private readonly IUIService _uiService; + private readonly ISettingsService _settingsService; + private CancellationTokenSource _cancellationTokenSource; - private string _lastProjectPath; - public FileItem RootDirectory + [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 IRelayCommand SelectProjectCommand { get; } + public IAsyncRelayCommand GenerateContextFileCommand { get; } + public IRelayCommand CancelProcessingCommand { get; } + public IRelayCommand ExitApplicationCommand { get; } + + public MainViewModel( + IProjectLoaderService projectLoaderService, + IFileScannerService fileScannerService, + IContextFileGenerator contextFileGenerator, + IUIService uiService, + ISettingsService settingsService) { - get => _rootDirectory; - set + _projectLoaderService = projectLoaderService; + _fileScannerService = fileScannerService; + _contextFileGenerator = contextFileGenerator; + _uiService = uiService; + _settingsService = settingsService; + + // Явная инициализация команд + SelectProjectCommand = new RelayCommand(SelectProject, CanSelectProject); + GenerateContextFileCommand = new AsyncRelayCommand(GenerateContextFileAsync, CanGenerate); + CancelProcessingCommand = new RelayCommand(CancelProcessing); + ExitApplicationCommand = new RelayCommand(ExitApplication); + } + + private bool CanSelectProject() => !IsProcessing; + + private bool CanGenerate() + { + bool result = !IsProcessing && + IsProjectLoaded && + RootDirectory != null && + HasSelectedFiles(RootDirectory); + + // Отладочная информация + System.Diagnostics.Debug.WriteLine($"CanGenerate: {result}, IsProcessing: {IsProcessing}, IsProjectLoaded: {IsProjectLoaded}, RootDirectory: {(RootDirectory != null)}, HasSelectedFiles: {HasSelectedFiles(RootDirectory)}"); + + return result; + } + + private void SelectProject() + { + var initialDir = !string.IsNullOrEmpty(SelectedProjectPath) && Directory.Exists(SelectedProjectPath) + ? Path.GetDirectoryName(SelectedProjectPath) + : _settingsService.GetLastProjectPath(); + + if (_uiService.ShowOpenProjectFileDialog(out var filePath)) { - _rootDirectory = value; - OnPropertyChanged(); + LoadProject(filePath); + return; + } + + var folderPath = _uiService.ShowFolderBrowserDialog(initialDir); + if (!string.IsNullOrEmpty(folderPath)) + { + LoadProject(folderPath); } } - public string SelectedFolderPath + private async Task GenerateContextFileAsync() { - get => _selectedFolderPath; - set + var selectedFiles = _fileScannerService.GetSelectedFiles(RootDirectory); + if (selectedFiles.Count == 0) { - _selectedFolderPath = value; - OnPropertyChanged(); + _uiService.ShowMessage("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxImage.Warning); + return; + } + + var defaultFileName = _projectLoaderService.GetDefaultOutputFileName(SelectedProjectPath); + var initialDir = _settingsService.GetLastProjectPath() ?? Path.GetDirectoryName(SelectedProjectPath); + + if (!_uiService.ShowSaveFileDialog(defaultFileName, initialDir, out var savePath)) + return; + + IsProcessing = true; + ProgressText = "Генерация контекстного файла..."; + ProgressValue = 0; + + try + { + _cancellationTokenSource = new CancellationTokenSource(); + var progress = new Progress(value => + { + ProgressValue = value; + ProgressText = $"Генерация: {value}%"; + }); + + await _contextFileGenerator.GenerateContextFileAsync( + selectedFiles, + savePath, + Path.GetDirectoryName(SelectedProjectPath), + progress, + _cancellationTokenSource.Token); + + _uiService.ShowMessage($"Файл успешно создан:\n{savePath}", "Успех"); + } + catch (OperationCanceledException) + { + ProgressText = "Генерация отменена"; + } + catch (IOException ex) + { + _uiService.ShowMessage($"Ошибка доступа к файлу: {ex.Message}\nПожалуйста, закройте файлы или предоставьте необходимые права доступа.", "Ошибка доступа", MessageBoxImage.Error); + } + catch (System.Exception ex) + { + _uiService.ShowMessage($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxImage.Error); + } + finally + { + IsProcessing = false; + // Принудительно обновляем команды после завершения операции + UpdateCommandsCanExecute(); } } - public bool IsProcessing + private void CancelProcessing() { - get => _isProcessing; - set + _cancellationTokenSource?.Cancel(); + ProgressText = "Операция отменена"; + } + + private void ExitApplication() + { + Application.Current.Shutdown(); + } + + private void LoadProject(string projectPath) + { + SelectedProjectPath = projectPath; + _settingsService.SaveLastProjectPath(Path.GetDirectoryName(projectPath)); + + IsProjectLoaded = false; + RootDirectory = null; + UpdateCommandsCanExecute(); + + LoadProjectAsync(projectPath); + } + + private async void LoadProjectAsync(string projectPath) + { + ProgressText = "Загрузка проекта..."; + ProgressValue = 0; + + try { - _isProcessing = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(CanSelectFolder)); - OnPropertyChanged(nameof(CanGenerate)); + var progress = new Progress(value => + { + ProgressValue = value; + ProgressText = $"Загрузка: {value}%"; + }); + + _cancellationTokenSource = new CancellationTokenSource(); + RootDirectory = await _projectLoaderService.LoadProjectFromPathAsync(projectPath, progress, _cancellationTokenSource.Token); + + IsProjectLoaded = true; + ProgressText = "Проект загружен успешно"; + + // Подписываемся на события изменения выбора + SubscribeToSelectionChanges(RootDirectory); + + // Сбрасываем выделение после загрузки + if (RootDirectory != null) + { + ClearSelections(RootDirectory); + } + + // Принудительно обновляем команды + UpdateCommandsCanExecute(); + } + catch (OperationCanceledException) + { + ProgressText = "Загрузка отменена"; + } + catch (System.Exception ex) + { + _uiService.ShowMessage($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxImage.Error); + } + finally + { + IsProcessing = false; + UpdateCommandsCanExecute(); } } - public int ProgressValue + // Рекурсивная подписка на события изменения выбора + private void SubscribeToSelectionChanges(FileItem item) { - get => _progressValue; - set + if (item == null) return; + + item.SelectionChanged += (sender, args) => { - _progressValue = value; - OnPropertyChanged(); + UpdateCommandsCanExecute(); + }; + + foreach (var child in item.Children) + { + SubscribeToSelectionChanges(child); } } - public string ProgressText + private void ClearSelections(FileItem item) { - get => _progressText; - set + if (item == null) return; + + item.IsSelected = false; + foreach (var child in item.Children) { - _progressText = value; - OnPropertyChanged(); + ClearSelections(child); } } - public bool CanSelectFolder => !IsProcessing; - public bool CanGenerate => !IsProcessing && RootDirectory != null && HasSelectedFiles(RootDirectory); - - public ICommand SelectFolderCommand { get; } - public ICommand GenerateCommand { get; } - public ICommand CancelCommand { get; } - public ICommand ExitCommand { get; } - - public MainViewModel() - { - SelectFolderCommand = new RelayCommand(SelectFolderAsync); - GenerateCommand = new RelayCommand(GenerateFileAsync, _ => CanGenerate); - CancelCommand = new RelayCommand(CancelProcessing, _ => IsProcessing); - ExitCommand = new RelayCommand(_ => Application.Current.Shutdown()); - - LoadSettings(); - } - private bool HasSelectedFiles(FileItem item) { - if (item.IsSelected == true && !item.IsDirectory) + if (item == null) return false; + + if (!item.IsDirectory && item.IsSelected == true) return true; - foreach (var child in item.Children) + if (item.IsDirectory) { - if (HasSelectedFiles(child)) - return true; + foreach (var child in item.Children) + { + if (HasSelectedFiles(child)) + return true; + } } return false; } - private async void SelectFolderAsync(object parameter) + // Метод для принудительного обновления всех команд + private void UpdateCommandsCanExecute() { - if (!CanSelectFolder) return; - - var dialog = new OpenFileDialog - { - Title = "Выберите файл проекта или папку", - CheckFileExists = false, - CheckPathExists = true, - FileName = "dummy", - Filter = "Проекты C# (*.csproj)|*.csproj|Все файлы (*.*)|*.*" - }; - - if (!string.IsNullOrEmpty(_lastProjectPath) && Directory.Exists(_lastProjectPath)) - { - dialog.InitialDirectory = _lastProjectPath; - } - - bool? result = dialog.ShowDialog(); - - if (result == true) - { - string selectedPath = dialog.FileName; - string projectDirectory = Path.GetDirectoryName(selectedPath); - - if (!string.IsNullOrEmpty(projectDirectory)) - { - _lastProjectPath = projectDirectory; - SaveSettings(); - - await LoadProjectDirectoryAsync(projectDirectory); - } - } + (SelectProjectCommand as RelayCommand)?.NotifyCanExecuteChanged(); + (GenerateContextFileCommand as AsyncRelayCommand)?.NotifyCanExecuteChanged(); } - - private async Task LoadProjectDirectoryAsync(string projectDirectory) - { - IsProcessing = true; - ProgressText = "Сканирование проекта..."; - ProgressValue = 0; - - try - { - SelectedFolderPath = projectDirectory; - var progress = new Progress(value => - { - ProgressValue = value; - ProgressText = $"Сканирование: {value}%"; - }); - - _cancellationTokenSource = new CancellationTokenSource(); - RootDirectory = await ProjectScannerService.ScanProjectDirectoryAsync( - projectDirectory, - progress, - _cancellationTokenSource.Token - ); - - ProgressText = "Сканирование завершено"; - } - catch (OperationCanceledException) - { - ProgressText = "Сканирование отменено"; - RootDirectory = null; - } - catch (Exception ex) - { - MessageBox.Show($"Ошибка при сканировании проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); - RootDirectory = null; - } - finally - { - IsProcessing = false; - } - } - - private async void GenerateFileAsync(object parameter) - { - if (!CanGenerate || RootDirectory == null) return; - - var selectedFiles = ProjectScannerService.GetSelectedFiles(RootDirectory); - - if (selectedFiles.Count == 0) - { - MessageBox.Show("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxButton.OK, MessageBoxImage.Warning); - return; - } - - var saveDialog = new SaveFileDialog - { - Title = "Сохранить контекстный файл", - Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*", - FileName = $"{Path.GetFileName(SelectedFolderPath)}_context.txt", - DefaultExt = ".txt" - }; - - if (!string.IsNullOrEmpty(_lastProjectPath) && Directory.Exists(_lastProjectPath)) - { - saveDialog.InitialDirectory = _lastProjectPath; - } - - bool? result = saveDialog.ShowDialog(); - - if (result == true) - { - IsProcessing = true; - ProgressText = "Генерация контекстного файла..."; - ProgressValue = 0; - - try - { - _cancellationTokenSource = new CancellationTokenSource(); - var progress = new Progress(value => - { - ProgressValue = value; - ProgressText = $"Генерация: {value}%"; - }); - - await GenerateContextFileAsync(selectedFiles, saveDialog.FileName, progress, _cancellationTokenSource.Token); - - MessageBox.Show($"Файл успешно создан:\n{saveDialog.FileName}", "Успех", MessageBoxButton.OK, MessageBoxImage.Information); - } - catch (OperationCanceledException) - { - ProgressText = "Генерация отменена"; - } - catch (IOException ex) - { - MessageBox.Show($"Ошибка доступа к файлу: {ex.Message}\nПожалуйста, закройте файлы или предоставьте необходимые права доступа.", "Ошибка доступа", MessageBoxButton.OK, MessageBoxImage.Error); - } - catch (Exception ex) - { - MessageBox.Show($"Ошибка при генерации файла: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); - } - finally - { - IsProcessing = false; - } - } - } - - 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 relativePath = Path.GetRelativePath(SelectedFolderPath, 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); // true для BOM - await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding); - } - - private void CancelProcessing(object parameter) - { - _cancellationTokenSource?.Cancel(); - } - - private void LoadSettings() - { - try - { - _lastProjectPath = Properties.Settings.Default.LastProjectPath; - } - catch - { - _lastProjectPath = null; - } - } - - private void SaveSettings() - { - try - { - Properties.Settings.Default.LastProjectPath = _lastProjectPath; - Properties.Settings.Default.Save(); - } - catch - { - // Игнорируем ошибки сохранения настроек - } - } - - public event PropertyChangedEventHandler? PropertyChanged; - - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - 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; - } - - public RelayCommand(Action execute, Func canExecute = null) - { - _execute = execute ?? throw new ArgumentNullException(nameof(execute)); - _canExecute = canExecute; - } - - public bool CanExecute(object parameter) => _canExecute == null || _canExecute(parameter); - public void Execute(object parameter) => _execute(parameter); } } \ No newline at end of file diff --git a/CodeContextGenerator/Views/MainWindow.xaml b/CodeContextGenerator/Views/MainWindow.xaml index 8261c9e..e194e7e 100644 --- a/CodeContextGenerator/Views/MainWindow.xaml +++ b/CodeContextGenerator/Views/MainWindow.xaml @@ -5,7 +5,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:CodeContextGenerator.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:models="clr-namespace:CodeContextGenerator.Models" Title="Code Context Generator" Width="800" Height="600" @@ -15,6 +14,16 @@ + + + + @@ -31,48 +40,70 @@ Grid.Row="0" Margin="0,0,0,5" FontWeight="Bold" - Text="Выберите папку проекта:" /> + Text="Выберите файл решения, проекта или папку:" />