Release v1.0

This commit is contained in:
2025-12-11 01:01:33 +05:00
parent 2785e1749a
commit d3d0417c8d
22 changed files with 1231 additions and 613 deletions

View File

@@ -3,7 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:CodeContextGenerator.Converters" xmlns:converters="clr-namespace:CodeContextGenerator.Converters"
StartupUri="Views/MainWindow.xaml"> ShutdownMode="OnMainWindowClose">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" /> <converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />

View File

@@ -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<ISettingsService, SettingsService>();
serviceCollection.AddSingleton<IFileProcessorService, FileProcessorService>();
serviceCollection.AddSingleton<IFileScannerService, FileScannerService>();
serviceCollection.AddSingleton<IProjectLoaderService, ProjectLoaderService>();
serviceCollection.AddSingleton<IContextFileGenerator, ContextFileGenerator>();
serviceCollection.AddSingleton<IUIService, UIService>();
// Регистрация ViewModel
serviceCollection.AddTransient<MainViewModel>();
_serviceProvider = serviceCollection.BuildServiceProvider();
}
protected override void OnStartup(StartupEventArgs e)
{
// Создаем главное окно только один раз
var mainWindow = new Views.MainWindow();
// Устанавливаем DataContext
mainWindow.DataContext = _serviceProvider.GetRequiredService<MainViewModel>();
// Устанавливаем главное окно приложения
MainWindow = mainWindow;
// Показываем окно
mainWindow.Show();
base.OnStartup(e);
} }
} }

View File

@@ -8,6 +8,11 @@
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Properties\Settings.Designer.cs"> <Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput> <DesignTimeSharedInput>True</DesignTimeSharedInput>

View File

@@ -1,24 +1,26 @@
using System; using System.Globalization;
using System.Globalization;
using System.Windows; using System.Windows;
using System.Windows.Data; 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) boolValue = !boolValue;
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
} }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) return boolValue ? Visibility.Visible : Visibility.Collapsed;
{ }
throw new NotImplementedException();
} public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
} }
} }

View File

@@ -0,0 +1,6 @@
namespace CodeContextGenerator.Interfaces;
public interface IContextFileGenerator
{
Task GenerateContextFileAsync(List<string> selectedFiles, string outputPath, string projectRootPath, IProgress<int> progress, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,6 @@
namespace CodeContextGenerator.Interfaces;
public interface IFileProcessorService
{
string ProcessFileContent(string content, string fileName);
}

View File

@@ -0,0 +1,9 @@
using CodeContextGenerator.Models;
namespace CodeContextGenerator.Interfaces;
public interface IFileScannerService
{
Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress<int> progress = null, CancellationToken cancellationToken = default);
List<string> GetSelectedFiles(FileItem rootItem);
}

View File

@@ -0,0 +1,9 @@
using CodeContextGenerator.Models;
namespace CodeContextGenerator.Interfaces;
public interface IProjectLoaderService
{
Task<FileItem> LoadProjectFromPathAsync(string projectPath, IProgress<int> progress, CancellationToken cancellationToken);
string GetDefaultOutputFileName(string projectPath);
}

View File

@@ -0,0 +1,7 @@
namespace CodeContextGenerator.Interfaces;
public interface ISettingsService
{
string GetLastProjectPath();
void SaveLastProjectPath(string path);
}

View File

@@ -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);
}

View File

@@ -1,60 +1,103 @@
using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel; using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
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<FileItem> Children { get; } = new ObservableCollection<FileItem>();
// Событие, вызываемое при изменении состояния выбора
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; } if (newValue.HasValue)
public string FullName { get; set; }
public bool IsDirectory { get; set; }
public ObservableCollection<FileItem> Children { get; set; } = new ObservableCollection<FileItem>();
public FileItem Parent { get; set; }
private bool? _isSelected;
public bool? IsSelected
{ {
get => _isSelected; UpdateChildrenSelection(newValue.Value);
set }
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) child.RaiseSelectionChanged();
{
_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;
} }
} }
} }
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);
}
} }

View File

@@ -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<string> selectedFiles, string outputPath, string projectRootPath, IProgress<int> 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);
}
}

View File

@@ -1,191 +1,189 @@
using System.Text; using CodeContextGenerator.Interfaces;
using System.Text;
using System.Text.RegularExpressions; 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);
{ }
return RemoveXamlComments(content); else if (fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase) ||
} fileName.EndsWith(".razor", StringComparison.OrdinalIgnoreCase))
else if (fileName.EndsWith(".cs", StringComparison.OrdinalIgnoreCase)) {
{ return RemoveCSharpComments(content);
return RemoveCSharpComments(content); }
} return 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<int>();
bool inComment = false;
bool inString = false; bool inString = false;
bool inChar = false; bool inChar = false;
bool escapeNext = 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) if (escapeNext)
{ {
escapeNext = false; escapeNext = false;
if (!inComment) result.Append(c);
continue; continue;
} }
if (c == '\\' && (inString || inChar)) if (c == '\\' && (inString || inChar))
{ {
escapeNext = true; escapeNext = true;
if (!inComment) result.Append(c);
continue; continue;
} }
// Обработка строковых литералов if (!inChar && c == '"')
if (!inComment)
{ {
if (!inString && !inChar && c == '"') inString = !inString;
{
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; continue;
} }
// Обработка многострочных комментариев if (!inString && c == '\'')
if (i < content.Length - 1)
{ {
if (!inComment && c == '/' && content[i + 1] == '*') inChar = !inChar;
{ continue;
inComment = true;
stack.Push(i);
i++; // Пропускаем следующий символ
continue;
}
if (inComment && c == '*' && content[i + 1] == '/')
{
inComment = false;
stack.Pop();
i++; // Пропускаем следующий символ
continue;
}
}
if (!inComment)
{
result.Append(c);
} }
} }
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();
} }
} }

View File

@@ -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<int> 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<string> GetSelectedFiles(FileItem rootItem)
{
var selectedFiles = new List<string>();
CollectSelectedFiles(rootItem, selectedFiles);
return selectedFiles;
}
private void CollectSelectedFiles(FileItem item, List<string> selectedFiles)
{
if (item.IsDirectory)
{
foreach (var child in item.Children)
{
CollectSelectedFiles(child, selectedFiles);
}
}
else if (item.IsSelected == true)
{
selectedFiles.Add(item.FullName);
}
}
}

View File

@@ -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<FileItem> LoadProjectFromPathAsync(string projectPath, IProgress<int> 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<FileItem> LoadDirectoryAsProjectAsync(string directoryPath, IProgress<int> 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<FileItem> LoadSolutionAsync(string solutionPath, IProgress<int> 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<string> ParseSolutionProjects(string solutionPath, string solutionDir)
{
var projects = new List<string>();
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<FileItem> LoadCsprojAsync(string csprojPath, IProgress<int> 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";
}
}

View File

@@ -1,39 +1,33 @@
using System.IO; using CodeContextGenerator.Models;
using CodeContextGenerator.Models; using System.IO;
namespace CodeContextGenerator.Services; namespace CodeContextGenerator.Services;
public static class ProjectScannerService 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" }; private static readonly string[] IncludedExtensions = { ".cs", ".xaml" };
public static async Task<FileItem> ScanProjectDirectoryAsync(string rootPath, IProgress<int> progress = null, CancellationToken cancellationToken = default) public static async Task BuildDirectoryTreeAsync(string path, FileItem parentItem, IProgress<int> 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<int> progress = null, CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
try try
{ {
if (!Directory.Exists(path))
return;
var directories = Directory.GetDirectories(path) 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(); .ToList();
var files = Directory.GetFiles(path) 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(); .ToList();
int totalItems = directories.Count + files.Count; int totalItems = directories.Count + files.Count;
@@ -85,32 +79,13 @@ public static class ProjectScannerService
progress?.Report((int)((processedItems * 100.0) / totalItems)); progress?.Report((int)((processedItems * 100.0) / totalItems));
} }
} }
catch (Exception ex) catch (IOException)
{ {
// Логируем ошибку, но продолжаем работу // Игнорируем ошибки доступа к директориям
Console.WriteLine($"Error scanning directory {path}: {ex.Message}");
} }
} catch (UnauthorizedAccessException)
public static List<string> GetSelectedFiles(FileItem rootItem)
{
var selectedFiles = new List<string>();
CollectSelectedFiles(rootItem, selectedFiles);
return selectedFiles;
}
private static void CollectSelectedFiles(FileItem item, List<string> selectedFiles)
{
if (item.IsDirectory)
{ {
foreach (var child in item.Children) // Игнорируем ошибки доступа
{
CollectSelectedFiles(child, selectedFiles);
}
}
else if (item.IsSelected==true)
{
selectedFiles.Add(item.FullName);
} }
} }
} }

View File

@@ -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<Settings>(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; }
}
}

View File

@@ -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);
}
}

View File

@@ -1,344 +1,282 @@
using CodeContextGenerator.Models; using CodeContextGenerator.Interfaces;
using CodeContextGenerator.Models;
using CodeContextGenerator.Services; using CodeContextGenerator.Services;
using Microsoft.Win32; using CommunityToolkit.Mvvm.ComponentModel;
using System.ComponentModel; using CommunityToolkit.Mvvm.Input;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Linq;
using System.Text; using System.Threading;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input;
namespace CodeContextGenerator.ViewModels namespace CodeContextGenerator.ViewModels
{ {
public class MainViewModel : INotifyPropertyChanged public partial class MainViewModel : ObservableObject
{ {
private FileItem _rootDirectory; private readonly IProjectLoaderService _projectLoaderService;
private string _selectedFolderPath; private readonly IFileScannerService _fileScannerService;
private bool _isProcessing; private readonly IContextFileGenerator _contextFileGenerator;
private int _progressValue; private readonly IUIService _uiService;
private string _progressText; private readonly ISettingsService _settingsService;
private CancellationTokenSource _cancellationTokenSource; 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; _projectLoaderService = projectLoaderService;
set _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; LoadProject(filePath);
OnPropertyChanged(); return;
}
var folderPath = _uiService.ShowFolderBrowserDialog(initialDir);
if (!string.IsNullOrEmpty(folderPath))
{
LoadProject(folderPath);
} }
} }
public string SelectedFolderPath private async Task GenerateContextFileAsync()
{ {
get => _selectedFolderPath; var selectedFiles = _fileScannerService.GetSelectedFiles(RootDirectory);
set if (selectedFiles.Count == 0)
{ {
_selectedFolderPath = value; _uiService.ShowMessage("Пожалуйста, выберите хотя бы один файл для обработки.", "Предупреждение", MessageBoxImage.Warning);
OnPropertyChanged(); 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<int>(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; _cancellationTokenSource?.Cancel();
set 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; var progress = new Progress<int>(value =>
OnPropertyChanged(); {
OnPropertyChanged(nameof(CanSelectFolder)); ProgressValue = value;
OnPropertyChanged(nameof(CanGenerate)); 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; if (item == null) return;
set
item.SelectionChanged += (sender, args) =>
{ {
_progressValue = value; UpdateCommandsCanExecute();
OnPropertyChanged(); };
foreach (var child in item.Children)
{
SubscribeToSelectionChanges(child);
} }
} }
public string ProgressText private void ClearSelections(FileItem item)
{ {
get => _progressText; if (item == null) return;
set
item.IsSelected = false;
foreach (var child in item.Children)
{ {
_progressText = value; ClearSelections(child);
OnPropertyChanged();
} }
} }
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) private bool HasSelectedFiles(FileItem item)
{ {
if (item.IsSelected == true && !item.IsDirectory) if (item == null) return false;
if (!item.IsDirectory && item.IsSelected == true)
return true; return true;
foreach (var child in item.Children) if (item.IsDirectory)
{ {
if (HasSelectedFiles(child)) foreach (var child in item.Children)
return true; {
if (HasSelectedFiles(child))
return true;
}
} }
return false; return false;
} }
private async void SelectFolderAsync(object parameter) // Метод для принудительного обновления всех команд
private void UpdateCommandsCanExecute()
{ {
if (!CanSelectFolder) return; (SelectProjectCommand as RelayCommand)?.NotifyCanExecuteChanged();
(GenerateContextFileCommand as AsyncRelayCommand)?.NotifyCanExecuteChanged();
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);
}
}
} }
private async Task LoadProjectDirectoryAsync(string projectDirectory)
{
IsProcessing = true;
ProgressText = "Сканирование проекта...";
ProgressValue = 0;
try
{
SelectedFolderPath = projectDirectory;
var progress = new Progress<int>(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<int>(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<string> selectedFiles, string outputPath, IProgress<int> 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<object> _execute;
private readonly Func<object, bool> _canExecute;
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
public RelayCommand(Action<object> execute, Func<object, bool> 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);
} }
} }

View File

@@ -5,7 +5,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:CodeContextGenerator.Views" xmlns:local="clr-namespace:CodeContextGenerator.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:CodeContextGenerator.Models"
Title="Code Context Generator" Title="Code Context Generator"
Width="800" Width="800"
Height="600" Height="600"
@@ -15,6 +14,16 @@
<Window.Resources> <Window.Resources>
<Style TargetType="TreeViewItem"> <Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" /> <Setter Property="IsExpanded" Value="True" />
<Setter Property="Focusable" Value="False" />
</Style>
<Style TargetType="Button">
<Setter Property="Padding" Value="10,5" />
<Setter Property="Margin" Value="5,0" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="VerticalAlignment" Value="Center" />
</Style> </Style>
</Window.Resources> </Window.Resources>
@@ -31,48 +40,70 @@
Grid.Row="0" Grid.Row="0"
Margin="0,0,0,5" Margin="0,0,0,5"
FontWeight="Bold" FontWeight="Bold"
Text="Выберите папку проекта:" /> Text="Выберите файл решения, проекта или папку:" />
<StackPanel <StackPanel
Grid.Row="1" Grid.Row="1"
Margin="0,0,0,10" Margin="0,0,0,10"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Margin="0,0,10,0" Width="100"
Padding="10,5" Command="{Binding SelectProjectCommand}"
Command="{Binding SelectFolderCommand}" Content="Выбрать..."
Content="Выбрать папку..." IsEnabled="{Binding CanSelectProject}" />
IsEnabled="{Binding CanSelectFolder}" />
<TextBlock <TextBlock
MaxWidth="600" MaxWidth="600"
Margin="10,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{Binding SelectedFolderPath}" Text="{Binding SelectedProjectPath}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<Border <!-- Контейнер для дерева и сообщения -->
Grid.Row="2" <Grid Grid.Row="2">
Padding="5" <Border
BorderBrush="Gray" Padding="5"
BorderThickness="1" BorderBrush="Gray"
CornerRadius="4"> BorderThickness="1"
<ScrollViewer> CornerRadius="4"
<TreeView ItemsSource="{Binding RootDirectory.Children}" Visibility="{Binding RootDirectory, Converter={StaticResource NullToVisibilityConverter}}"> Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}}">
<TreeView.ItemTemplate> <ScrollViewer
<HierarchicalDataTemplate ItemsSource="{Binding Children}"> HorizontalScrollBarVisibility="Auto"
<StackPanel Orientation="Horizontal"> PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"
<CheckBox VerticalScrollBarVisibility="Auto">
Margin="0,0,5,0" <TreeView
VerticalAlignment="Center" x:Name="ProjectTree"
IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}" ItemsSource="{Binding RootDirectory.Children}"
IsThreeState="True" /> VirtualizingPanel.IsVirtualizing="True"
<TextBlock VerticalAlignment="Center" Text="{Binding Name}" /> VirtualizingPanel.VirtualizationMode="Recycling">
</StackPanel> <TreeView.ItemTemplate>
</HierarchicalDataTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}">
</TreeView.ItemTemplate> <StackPanel MinHeight="24" Orientation="Horizontal">
</TreeView> <CheckBox
</ScrollViewer> Margin="2,0,5,0"
</Border> VerticalAlignment="Center"
Click="CheckBox_Click"
IsChecked="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
IsThreeState="True" />
<TextBlock
VerticalAlignment="Center"
Text="{Binding Name}"
ToolTip="{Binding FullName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</ScrollViewer>
</Border>
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="14"
Foreground="Gray"
Text="Проект еще не загружен. Выберите файл решения или проекта."
Visibility="{Binding IsProjectLoaded, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverted}" />
</Grid>
<ProgressBar <ProgressBar
Grid.Row="3" Grid.Row="3"
@@ -95,25 +126,17 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
Orientation="Horizontal"> Orientation="Horizontal">
<Button <Button
Margin="0,0,10,0"
Padding="15,5"
Background="#FFDC3545" Background="#FFDC3545"
Command="{Binding CancelCommand}" Command="{Binding CancelProcessingCommand}"
Content="Отмена" Content="Отмена"
Foreground="White" Foreground="White"
Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}" /> Visibility="{Binding IsProcessing, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button Command="{Binding ExitApplicationCommand}" Content="В главное меню" />
<Button <Button
Margin="0,0,10,0"
Padding="15,5"
Command="{Binding ExitCommand}"
Content="Закрыть" />
<Button
Padding="15,5"
Background="#FF28A745" Background="#FF28A745"
Command="{Binding GenerateCommand}" Command="{Binding GenerateContextFileCommand}"
Content="Сформировать" Content="Сформировать"
Foreground="White" Foreground="White" />
IsEnabled="{Binding CanGenerate}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,14 +1,33 @@
using System.Windows; using CodeContextGenerator.Models;
using CodeContextGenerator.ViewModels; using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace CodeContextGenerator.Views namespace CodeContextGenerator.Views;
public partial class MainWindow : Window
{ {
public partial class MainWindow : Window public MainWindow()
{ {
public MainWindow() InitializeComponent();
}
private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (sender is ScrollViewer scrollViewer)
{ {
InitializeComponent(); scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - e.Delta / 3);
DataContext = new MainViewModel(); e.Handled = true;
}
}
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
if (sender is CheckBox checkBox && checkBox.DataContext is FileItem fileItem)
{
// Синхронизируем состояние чекбокса с ViewModel
fileItem.IsSelected = checkBox.IsChecked;
e.Handled = true;
} }
} }
} }

View File

@@ -1,3 +1,3 @@
# CodeContextGenerator # CodeContextGenerator
Генератор контекста из проекта для нейросети Генератор контекста для нейросети из проекта Visual Studio / VS Code