Release v1.0
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
CodeContextGenerator/Interfaces/IContextFileGenerator.cs
Normal file
6
CodeContextGenerator/Interfaces/IContextFileGenerator.cs
Normal 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);
|
||||||
|
}
|
||||||
6
CodeContextGenerator/Interfaces/IFileProcessorService.cs
Normal file
6
CodeContextGenerator/Interfaces/IFileProcessorService.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CodeContextGenerator.Interfaces;
|
||||||
|
|
||||||
|
public interface IFileProcessorService
|
||||||
|
{
|
||||||
|
string ProcessFileContent(string content, string fileName);
|
||||||
|
}
|
||||||
9
CodeContextGenerator/Interfaces/IFileScannerService.cs
Normal file
9
CodeContextGenerator/Interfaces/IFileScannerService.cs
Normal 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);
|
||||||
|
}
|
||||||
9
CodeContextGenerator/Interfaces/IProjectLoaderService.cs
Normal file
9
CodeContextGenerator/Interfaces/IProjectLoaderService.cs
Normal 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);
|
||||||
|
}
|
||||||
7
CodeContextGenerator/Interfaces/ISettingsService.cs
Normal file
7
CodeContextGenerator/Interfaces/ISettingsService.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace CodeContextGenerator.Interfaces;
|
||||||
|
|
||||||
|
public interface ISettingsService
|
||||||
|
{
|
||||||
|
string GetLastProjectPath();
|
||||||
|
void SaveLastProjectPath(string path);
|
||||||
|
}
|
||||||
11
CodeContextGenerator/Interfaces/IUIService.cs
Normal file
11
CodeContextGenerator/Interfaces/IUIService.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
54
CodeContextGenerator/Services/ContextFileGenerator.cs
Normal file
54
CodeContextGenerator/Services/ContextFileGenerator.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
123
CodeContextGenerator/Services/FileScannerService.cs
Normal file
123
CodeContextGenerator/Services/FileScannerService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
CodeContextGenerator/Services/ProjectLoaderService.cs
Normal file
183
CodeContextGenerator/Services/ProjectLoaderService.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
57
CodeContextGenerator/Services/SettingsService.cs
Normal file
57
CodeContextGenerator/Services/SettingsService.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
110
CodeContextGenerator/Services/UIService.cs
Normal file
110
CodeContextGenerator/Services/UIService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user