From b6ddadc6d3fd670509689acf54f617942916f853 Mon Sep 17 00:00:00 2001 From: golem84 Date: Wed, 10 Dec 2025 18:59:27 +0500 Subject: [PATCH] iteration 1 --- CodeContextGenerator/Models/FileItem.cs | 37 ++- .../Services/ProjectScannerService.cs | 162 +++++------ .../ViewModels/MainViewModel.cs | 258 +++++++++++++++--- CodeContextGenerator/Views/MainWindow.xaml | 161 +++++------ CodeContextGenerator/Views/MainWindow.xaml.cs | 30 +- 5 files changed, 413 insertions(+), 235 deletions(-) diff --git a/CodeContextGenerator/Models/FileItem.cs b/CodeContextGenerator/Models/FileItem.cs index d711428..a076661 100644 --- a/CodeContextGenerator/Models/FileItem.cs +++ b/CodeContextGenerator/Models/FileItem.cs @@ -23,7 +23,15 @@ namespace CodeContextGenerator.Models _isSelected = value; OnPropertyChanged(); UpdateParentSelection(); - UpdateChildrenSelection(value); + + // Если это директория и установлено конкретное значение (не null), применяем ко всем детям + if (IsDirectory && value.HasValue) + { + foreach (var child in Children) + { + child.IsSelected = value.Value; + } + } } } } @@ -43,18 +51,27 @@ namespace CodeContextGenerator.Models 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(); - } + // Если есть дети с null - устанавливаем null + bool hasIndeterminate = children.Any(c => c.IsSelected == null); - private void UpdateChildrenSelection(bool? value) - { - if (!IsDirectory || !value.HasValue) return; - - foreach (var child in Children) + if (hasIndeterminate) { - child.IsSelected = value; + Parent.IsSelected = null; } + else if (allSelected) + { + Parent.IsSelected = true; + } + else if (noneSelected) + { + Parent.IsSelected = false; + } + else + { + Parent.IsSelected = null; + } + + Parent.UpdateParentSelection(); } } } \ No newline at end of file diff --git a/CodeContextGenerator/Services/ProjectScannerService.cs b/CodeContextGenerator/Services/ProjectScannerService.cs index 1af58a4..69545d5 100644 --- a/CodeContextGenerator/Services/ProjectScannerService.cs +++ b/CodeContextGenerator/Services/ProjectScannerService.cs @@ -1,116 +1,96 @@ using System.IO; using CodeContextGenerator.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Threading; -namespace CodeContextGenerator.Services; - -public static class ProjectScannerService +namespace CodeContextGenerator.Services { - private static readonly string[] ExcludedDirectories = { "bin", "obj", ".git", "packages", ".vs", "Properties", "node_modules", ".vscode" }; - private static readonly string[] IncludedExtensions = { ".cs", ".xaml" }; - - public static async Task ScanProjectDirectoryAsync(string rootPath, IProgress progress = null, CancellationToken cancellationToken = default) + public static class ProjectScannerService { - var rootItem = new FileItem - { - Name = Path.GetFileName(rootPath), - FullName = rootPath, - IsDirectory = true, - IsSelected = false + private static readonly string[] ExcludedDirectories = { + "bin", "obj", ".git", "packages", ".vs", "Properties", + "node_modules", ".vscode", ".idea", ".vs", "Debug", "Release" }; - await BuildDirectoryTreeAsync(rootPath, rootItem, progress, cancellationToken); - return rootItem; - } + private static readonly string[] IncludedExtensions = { ".cs", ".xaml" }; - private static async Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default) - { - cancellationToken.ThrowIfCancellationRequested(); - - try + public static async Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress progress = null, CancellationToken cancellationToken = default) { - var directories = Directory.GetDirectories(path) - .Where(d => !ExcludedDirectories.Any(ex => d.EndsWith(ex, StringComparison.OrdinalIgnoreCase))) - .ToList(); + cancellationToken.ThrowIfCancellationRequested(); - 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) + try { - cancellationToken.ThrowIfCancellationRequested(); + if (!Directory.Exists(path)) + return; - var dirName = Path.GetFileName(dir); - var dirItem = new FileItem + 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) { - Name = dirName, - FullName = dir, - IsDirectory = true, - Parent = parentItem, - IsSelected = false - }; + cancellationToken.ThrowIfCancellationRequested(); - await BuildDirectoryTreeAsync(dir, dirItem, progress, cancellationToken); + var dirName = Path.GetFileName(dir); + var dirItem = new FileItem + { + Name = dirName, + FullName = dir, + IsDirectory = true, + Parent = parentItem, + IsSelected = false + }; - // Добавляем директорию только если в ней есть файлы или поддиректории с файлами - if (dirItem.Children.Any()) - { - parentItem.Children.Add(dirItem); + await BuildDirectoryTreeAsync(dir, dirItem, progress, cancellationToken); + + // Добавляем директорию только если в ней есть файлы или поддиректории с файлами + if (dirItem.Children.Any()) + { + parentItem.Children.Add(dirItem); + } + + processedItems++; + progress?.Report((int)((processedItems * 100.0) / totalItems)); } - processedItems++; - progress?.Report((int)((processedItems * 100.0) / totalItems)); - } - - // Обрабатываем файлы - foreach (var file in files) - { - cancellationToken.ThrowIfCancellationRequested(); - - var fileItem = new FileItem + // Обрабатываем файлы + foreach (var file in files) { - Name = Path.GetFileName(file), - FullName = file, - IsDirectory = false, - Parent = parentItem, - IsSelected = false - }; + cancellationToken.ThrowIfCancellationRequested(); - parentItem.Children.Add(fileItem); - processedItems++; - progress?.Report((int)((processedItems * 100.0) / totalItems)); + var fileItem = new FileItem + { + Name = Path.GetFileName(file), + FullName = file, + IsDirectory = false, + Parent = parentItem, + IsSelected = false + }; + + parentItem.Children.Add(fileItem); + processedItems++; + progress?.Report((int)((processedItems * 100.0) / totalItems)); + } } - } - catch (Exception ex) - { - // Логируем ошибку, но продолжаем работу - 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) - { - foreach (var child in item.Children) + catch (IOException) { - CollectSelectedFiles(child, selectedFiles); + // Игнорируем ошибки доступа к директориям + } + catch (UnauthorizedAccessException) + { + // Игнорируем ошибки доступа } - } - else if (item.IsSelected==true) - { - selectedFiles.Add(item.FullName); } } } \ No newline at end of file diff --git a/CodeContextGenerator/ViewModels/MainViewModel.cs b/CodeContextGenerator/ViewModels/MainViewModel.cs index 21de91a..d95ec48 100644 --- a/CodeContextGenerator/ViewModels/MainViewModel.cs +++ b/CodeContextGenerator/ViewModels/MainViewModel.cs @@ -1,24 +1,29 @@ -using CodeContextGenerator.Models; -using CodeContextGenerator.Services; -using Microsoft.Win32; -using System.ComponentModel; -using System.IO; +using System.ComponentModel; using System.Runtime.CompilerServices; -using System.Text; using System.Windows; using System.Windows.Input; +using Microsoft.Win32; +using CodeContextGenerator.Models; +using CodeContextGenerator.Services; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; +using System.Linq; namespace CodeContextGenerator.ViewModels { public class MainViewModel : INotifyPropertyChanged { private FileItem _rootDirectory; - private string _selectedFolderPath; + private string _selectedProjectFilePath; private bool _isProcessing; private int _progressValue; private string _progressText; private CancellationTokenSource _cancellationTokenSource; private string _lastProjectPath; + private bool _isProjectLoaded; public FileItem RootDirectory { @@ -30,12 +35,12 @@ namespace CodeContextGenerator.ViewModels } } - public string SelectedFolderPath + public string SelectedProjectFilePath { - get => _selectedFolderPath; + get => _selectedProjectFilePath; set { - _selectedFolderPath = value; + _selectedProjectFilePath = value; OnPropertyChanged(); } } @@ -72,17 +77,27 @@ namespace CodeContextGenerator.ViewModels } } - public bool CanSelectFolder => !IsProcessing; - public bool CanGenerate => !IsProcessing && RootDirectory != null && HasSelectedFiles(RootDirectory); + public bool IsProjectLoaded + { + get => _isProjectLoaded; + set + { + _isProjectLoaded = value; + OnPropertyChanged(); + } + } - public ICommand SelectFolderCommand { get; } + public bool CanSelectFolder => !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() { - SelectFolderCommand = new RelayCommand(SelectFolderAsync); + SelectProjectFileCommand = new RelayCommand(SelectProjectFileAsync); GenerateCommand = new RelayCommand(GenerateFileAsync, _ => CanGenerate); CancelCommand = new RelayCommand(CancelProcessing, _ => IsProcessing); ExitCommand = new RelayCommand(_ => Application.Current.Shutdown()); @@ -92,6 +107,8 @@ namespace CodeContextGenerator.ViewModels private bool HasSelectedFiles(FileItem item) { + if (item == null) return false; + if (item.IsSelected == true && !item.IsDirectory) return true; @@ -104,17 +121,17 @@ namespace CodeContextGenerator.ViewModels return false; } - private async void SelectFolderAsync(object parameter) + private async void SelectProjectFileAsync(object parameter) { if (!CanSelectFolder) return; var dialog = new OpenFileDialog { - Title = "Выберите файл проекта или папку", - CheckFileExists = false, + Title = "Выберите файл решения или проекта", + CheckFileExists = true, CheckPathExists = true, - FileName = "dummy", - Filter = "Проекты C# (*.csproj)|*.csproj|Все файлы (*.*)|*.*" + Filter = "Файлы решений Visual Studio (*.sln)|*.sln|Файлы проектов C# (*.csproj)|*.csproj|Все поддерживаемые файлы (*.sln;*.csproj)|*.sln;*.csproj|Все файлы (*.*)|*.*", + Multiselect = false }; if (!string.IsNullOrEmpty(_lastProjectPath) && Directory.Exists(_lastProjectPath)) @@ -126,52 +143,65 @@ namespace CodeContextGenerator.ViewModels if (result == true) { - string selectedPath = dialog.FileName; - string projectDirectory = Path.GetDirectoryName(selectedPath); + string selectedFilePath = dialog.FileName; + string projectDirectory = Path.GetDirectoryName(selectedFilePath); if (!string.IsNullOrEmpty(projectDirectory)) { _lastProjectPath = projectDirectory; SaveSettings(); - await LoadProjectDirectoryAsync(projectDirectory); + SelectedProjectFilePath = selectedFilePath; + IsProjectLoaded = false; + + await LoadProjectAsync(selectedFilePath); } } } - private async Task LoadProjectDirectoryAsync(string projectDirectory) + private async Task LoadProjectAsync(string projectFilePath) { IsProcessing = true; - ProgressText = "Сканирование проекта..."; + ProgressText = "Загрузка проекта..."; ProgressValue = 0; try { - SelectedFolderPath = projectDirectory; + string projectDirectory = Path.GetDirectoryName(projectFilePath); + SelectedProjectFilePath = projectFilePath; + var progress = new Progress(value => { ProgressValue = value; - ProgressText = $"Сканирование: {value}%"; + ProgressText = $"Загрузка: {value}%"; }); _cancellationTokenSource = new CancellationTokenSource(); - RootDirectory = await ProjectScannerService.ScanProjectDirectoryAsync( - projectDirectory, - progress, - _cancellationTokenSource.Token - ); - ProgressText = "Сканирование завершено"; + // Определяем тип файла + 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 = "Сканирование отменено"; + ProgressText = "Загрузка отменена"; RootDirectory = null; + IsProjectLoaded = false; } catch (Exception ex) { - MessageBox.Show($"Ошибка при сканировании проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); + MessageBox.Show($"Ошибка при загрузке проекта: {ex.Message}", "Ошибка", MessageBoxButton.OK, MessageBoxImage.Error); RootDirectory = null; + IsProjectLoaded = false; } finally { @@ -179,11 +209,137 @@ namespace CodeContextGenerator.ViewModels } } + 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 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(new[] { '"' }, 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 void ClearAllSelections(FileItem item) + { + item.IsSelected = false; + foreach (var child in item.Children) + { + ClearAllSelections(child); + } + } + private async void GenerateFileAsync(object parameter) { if (!CanGenerate || RootDirectory == null) return; - var selectedFiles = ProjectScannerService.GetSelectedFiles(RootDirectory); + var selectedFiles = GetSelectedFiles(RootDirectory); if (selectedFiles.Count == 0) { @@ -195,7 +351,7 @@ namespace CodeContextGenerator.ViewModels { Title = "Сохранить контекстный файл", Filter = "Текстовые файлы (*.txt)|*.txt|Все файлы (*.*)|*.*", - FileName = $"{Path.GetFileName(SelectedFolderPath)}_context.txt", + FileName = $"{Path.GetFileNameWithoutExtension(SelectedProjectFilePath)}_context.txt", DefaultExt = ".txt" }; @@ -244,6 +400,28 @@ namespace CodeContextGenerator.ViewModels } } + private 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); + } + } + private async Task GenerateContextFileAsync(List selectedFiles, string outputPath, IProgress progress, CancellationToken cancellationToken) { var outputContent = new StringBuilder(); @@ -256,7 +434,8 @@ namespace CodeContextGenerator.ViewModels try { - string relativePath = Path.GetRelativePath(SelectedFolderPath, filePath); + string projectDir = Path.GetDirectoryName(SelectedProjectFilePath); + string relativePath = Path.GetRelativePath(projectDir, filePath); string fileContent = await File.ReadAllTextAsync(filePath, Encoding.UTF8); // Обработка комментариев @@ -273,13 +452,12 @@ namespace CodeContextGenerator.ViewModels } catch (IOException ex) { - // Если файл заблокирован или недоступен - останавливаем процесс throw new IOException($"Файл '{Path.GetFileName(filePath)}' заблокирован или недоступен: {ex.Message}", ex); } } - // Сохраняем результат с кодировкой UTF-8 с BOM для максимальной совместимости - var encoding = new UTF8Encoding(true); // true для BOM + // Сохраняем результат с кодировкой UTF-8 с BOM + var encoding = new UTF8Encoding(true); await File.WriteAllTextAsync(outputPath, outputContent.ToString(), encoding); } diff --git a/CodeContextGenerator/Views/MainWindow.xaml b/CodeContextGenerator/Views/MainWindow.xaml index 8261c9e..7883c80 100644 --- a/CodeContextGenerator/Views/MainWindow.xaml +++ b/CodeContextGenerator/Views/MainWindow.xaml @@ -1,72 +1,68 @@ - + + + + + - - - - - + + + + + - + - -