Name Message Date
📄 MainViewModel.cs One-shot 5 days ago
📄 ui/src/ViewModels/MainViewModel.cs
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Cfa.Ui.Graph;
using Cfa.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Cfa.Ui.ViewModels;

public sealed class MainViewModel : ObservableObject, IDisposable
{
    private readonly IDesktopServices desktop;
    private readonly IScanSessionFactory sessions;
    private readonly CancellationTokenSource lifetime = new();
    private IScanSession? scanner;
    private FileInfo? solution;
    private bool isBusy;
    private bool disposed;
    private string address = "";
    private string status = "Open a solution to explore its call graph.";
    private GraphScene? scene;

    public MainViewModel(IDesktopServices desktop, IScanSessionFactory sessions)
    {
        this.desktop = desktop;
        this.sessions = sessions;
        OpenSolutionCommand = new AsyncRelayCommand(OpenSolutionAsync, () => !IsBusy);
        AnalyzeCommand = new AsyncRelayCommand(AnalyzeAsync, () => CanAnalyze);
        OpenSourceCommand = new AsyncRelayCommand<GraphNode>(OpenSourceAsync);
    }

    public IAsyncRelayCommand OpenSolutionCommand { get; }
    public IAsyncRelayCommand AnalyzeCommand { get; }
    public IAsyncRelayCommand<GraphNode> OpenSourceCommand { get; }
    public bool CanAnalyze => scanner is not null && !IsBusy;
    public bool HasScene => Scene is not null;
    public string Address
    {
        get => address;
        set => SetProperty(ref address, value);
    }
    public string Status
    {
        get => status;
        private set => SetProperty(ref status, value);
    }
    public bool IsBusy
    {
        get => isBusy;
        private set
        {
            if (SetProperty(ref isBusy, value))
            {
                OnPropertyChanged(nameof(CanAnalyze));
                OpenSolutionCommand.NotifyCanExecuteChanged();
                AnalyzeCommand.NotifyCanExecuteChanged();
            }
        }
    }
    public GraphScene? Scene
    {
        get => scene;
        private set
        {
            if (SetProperty(ref scene, value))
            {
                OnPropertyChanged(nameof(HasScene));
            }
        }
    }

    private async Task OpenSolutionAsync()
    {
        IsBusy = true;
        try
        {
            var file = await desktop.PickSolutionAsync();
            if (file is null)
            {
                return;
            }
            Status = "Loading " + file.Name + "...";
            var loaded = await Task.Run(() => sessions.OpenAsync(file, lifetime.Token), lifetime.Token);
            if (disposed)
            {
                loaded.Dispose();
                return;
            }
            scanner?.Dispose();
            scanner = loaded;
            solution = file;
            Scene = null;
            Status = file.Name + " loaded. Enter a source path:line.";
        }
        catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { }
        catch (Exception exception)
        {
            Status = "Solution loading failed.";
            await desktop.ShowErrorAsync(exception.Message);
        }
        finally
        {
            FinishOperation();
        }
    }

    private async Task AnalyzeAsync()
    {
        IsBusy = true;
        try
        {
            var separator = Address.LastIndexOf(':');
            if (separator <= 0 || !int.TryParse(Address[(separator + 1)..], out var line) || line < 1)
            {
                throw new ArgumentException("Enter a source file and line, for example core/src/Scanner.cs:50.");
            }
            var path = Address[..separator].Trim();
            var file = new FileInfo(Path.GetFullPath(path, solution!.DirectoryName!));
            Status = "Analyzing call graph...";
            var current = scanner!;
            var result = await Task.Run(
                async () =>
                {
                    var analysis = await current.AnalyzeAsync(file, line, lifetime.Token);
                    return GraphLayout.Build(analysis, lifetime.Token);
                },
                lifetime.Token
            );
            if (!disposed)
            {
                Scene = result;
                Status =
                    $"{result.Nodes.Count} occurrences, {result.Edges.Count} calls. Right/middle-drag to pan; scroll to zoom.";
            }
        }
        catch (OperationCanceledException) when (lifetime.IsCancellationRequested) { }
        catch (Exception exception)
        {
            Status = "Analysis failed.";
            await desktop.ShowErrorAsync(exception.Message);
        }
        finally
        {
            FinishOperation();
        }
    }

    private async Task OpenSourceAsync(GraphNode? node)
    {
        if (node?.SourceLink is not { } link)
        {
            return;
        }
        try
        {
            await desktop.OpenSourceAsync(link);
        }
        catch (Exception exception)
        {
            await desktop.ShowErrorAsync(exception.Message);
        }
    }

    public void Dispose()
    {
        if (disposed)
        {
            return;
        }
        disposed = true;
        lifetime.Cancel();
        if (!IsBusy)
        {
            scanner?.Dispose();
            scanner = null;
            lifetime.Dispose();
        }
    }

    private void FinishOperation()
    {
        IsBusy = false;
        if (disposed)
        {
            scanner?.Dispose();
            scanner = null;
            lifetime.Dispose();
        }
    }
}