Mesen2/UI/Debugger/ViewModels/DisassemblyViewModel.cs

418 lines
12 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Threading;
using Mesen.Config;
using Mesen.Debugger.Controls;
using Mesen.Debugger.Disassembly;
using Mesen.Debugger.Utilities;
using Mesen.Debugger.Views;
using Mesen.Interop;
using Mesen.Utilities;
using Mesen.ViewModels;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Linq;
using System.Text;
namespace Mesen.Debugger.ViewModels
{
public class DisassemblyViewModel : DisposableViewModel, ISelectableModel
{
public ICodeDataProvider DataProvider { get; }
public CpuType CpuType { get; }
public DebuggerWindowViewModel Debugger { get; }
public DisassemblyViewStyleProvider StyleProvider { get; }
[Reactive] public int ScrollPosition { get; set; } = 0;
[Reactive] public int MaxScrollPosition { get; private set; } = 1000000000;
[Reactive] public int TopAddress { get; private set; } = 0;
[Reactive] public CodeLineData[] Lines { get; private set; } = Array.Empty<CodeLineData>();
[Reactive] public int? ActiveAddress { get; set; }
[Reactive] public int SelectedRowAddress { get; set; }
[Reactive] public int SelectionAnchor { get; set; }
[Reactive] public int SelectionStart { get; set; }
[Reactive] public int SelectionEnd { get; set; }
public QuickSearchViewModel QuickSearch { get; } = new QuickSearchViewModel();
public NavigationHistory<int> History { get; } = new();
public bool ShowScrollBarMarkers => CpuType != CpuType.NecDsp;
public DebugConfig Config { get; private set; }
public int VisibleRowCount { get; set; } = 100;
private DisassemblyViewer? _viewer = null;
private int _ignoreScrollUpdates = 0;
private Action? _refreshScrollbar = null;
[Obsolete("For designer only")]
public DisassemblyViewModel(): this(new DebuggerWindowViewModel(), new DebugConfig(), CpuType.Snes) { }
public DisassemblyViewModel(DebuggerWindowViewModel debugger, DebugConfig config, CpuType cpuType)
{
Config = config;
CpuType = cpuType;
Debugger = debugger;
StyleProvider = new DisassemblyViewStyleProvider(cpuType, this);
DataProvider = new CodeDataProvider(cpuType);
if(Design.IsDesignMode) {
return;
}
QuickSearch.OnFind += QuickSearch_OnFind;
AddDisposable(this.WhenAnyValue(x => x.TopAddress).Subscribe(x => Refresh()));
AddDisposable(this.WhenAnyValue(x => x.QuickSearch.IsSearchBoxVisible).Subscribe(x => {
if(!QuickSearch.IsSearchBoxVisible) {
_viewer?.Focus();
}
}));
int lastValue = ScrollPosition;
AddDisposable(this.WhenAnyValue(x => x.ScrollPosition).Subscribe(scrollPos => {
if(_viewer == null) {
ScrollPosition = lastValue;
return;
}
int gap = scrollPos - lastValue;
lastValue = scrollPos;
if(_ignoreScrollUpdates > 0) {
return;
}
if(gap != 0) {
if(Math.Abs(gap) <= 10) {
Scroll(gap);
} else {
int lineCount = DataProvider.GetLineCount();
TopAddress = Math.Max(0, Math.Min(lineCount - 1, (int)((double)lineCount / MaxScrollPosition * ScrollPosition)));
}
}
}));
}
private void QuickSearch_OnFind(OnFindEventArgs e)
{
DisassemblySearchOptions options = new() { SearchBackwards = e.Direction == SearchDirection.Backward, SkipFirstLine = e.SkipCurrent };
int findAddress = DebugApi.SearchDisassembly(CpuType, e.SearchString.ToLowerInvariant(), SelectedRowAddress, options);
if(findAddress >= 0) {
Dispatcher.UIThread.Post(() => {
SetSelectedRow(findAddress, true, true);
});
e.Success = true;
} else {
e.Success = false;
}
}
public void SetViewer(DisassemblyViewer? viewer)
{
_viewer = viewer;
}
public void Scroll(int lineNumberOffset)
{
if(lineNumberOffset == 0) {
return;
}
SetTopAddress(DataProvider.GetRowAddress(TopAddress, lineNumberOffset));
}
public void ScrollToTop(bool extendSelection)
{
if(extendSelection) {
ResizeSelectionTo(0);
} else {
SetSelectedRow(0, false, true);
ScrollToAddress(0, ScrollDisplayPosition.Top);
}
}
public void ScrollToBottom(bool extendSelection)
{
if(extendSelection) {
ResizeSelectionTo(DataProvider.GetLineCount() - 1);
} else {
int address = DataProvider.GetLineCount() - 1;
SetSelectedRow(address, false, true);
ScrollToAddress((uint)address, ScrollDisplayPosition.Bottom);
}
}
public void InvalidateVisual()
{
//Force DisassemblyViewer to refresh
Lines = Lines.ToArray();
}
public void Refresh()
{
CodeLineData[] lines = DataProvider.GetCodeLines(TopAddress, VisibleRowCount);
Lines = lines;
if(lines.Length > 0 && lines[0].Address >= 0) {
SetTopAddress(lines[0].Address);
}
_refreshScrollbar?.Invoke();
}
public void SetRefreshScrollBar(Action? refreshScrollbar)
{
_refreshScrollbar = refreshScrollbar;
}
private void SetTopAddress(int address)
{
int lineCount = DataProvider.GetLineCount();
address = Math.Max(0, Math.Min(lineCount - 1, address));
_ignoreScrollUpdates++;
TopAddress = address;
ScrollPosition = (int)(TopAddress / (double)lineCount * MaxScrollPosition);
_ignoreScrollUpdates--;
}
public void SetActiveAddress(int? pc)
{
ActiveAddress = pc;
if(pc != null) {
int currentAddress = SelectedRowAddress;
SetSelectedRow((int)pc);
bool scrolled = ScrollToAddress((uint)pc, ScrollDisplayPosition.Center, Config.Debugger.KeepActiveStatementInCenter);
if(scrolled) {
if(currentAddress != 0 || History.CanGoBack()) {
History.AddHistory(currentAddress);
}
History.AddHistory((int)pc);
}
}
}
public bool IsSelected(int address)
{
return address >= SelectionStart && address <= SelectionEnd;
}
public AddressInfo? GetSelectedRowAddress()
{
return new AddressInfo() {
Address = SelectedRowAddress,
Type = CpuType.ToMemoryType()
};
}
public void GoBack()
{
SetSelectedRow(History.GoBack(), true, false);
}
public void GoForward()
{
SetSelectedRow(History.GoForward(), true, false);
}
public void SetSelectedRow(int address)
{
SetSelectedRow(address, false);
}
public void SetSelectedRow(int address, bool scrollToRow = false, bool addToHistory = false)
{
int currentAddress = SelectedRowAddress;
SelectionStart = address;
SelectionEnd = address;
SelectedRowAddress = address;
SelectionAnchor = address;
if(addToHistory) {
if(currentAddress != 0 || History.CanGoBack()) {
History.AddHistory(currentAddress);
}
History.AddHistory(address);
}
InvalidateVisual();
if(scrollToRow) {
ScrollToAddress((uint)address);
}
}
public void MoveCursor(int rowOffset, bool extendSelection)
{
int address = DataProvider.GetRowAddress(SelectedRowAddress, rowOffset);
if(extendSelection) {
ResizeSelectionTo(address);
} else {
SetSelectedRow(address);
ScrollToAddress((uint)address, rowOffset < 0 ? ScrollDisplayPosition.Top : ScrollDisplayPosition.Bottom);
}
}
public void ResizeSelectionTo(int address)
{
if(SelectedRowAddress == address) {
return;
}
bool anchorTop = SelectionAnchor == SelectionStart;
if(anchorTop) {
if(address < SelectionStart) {
SelectionEnd = SelectionStart;
SelectionStart = address;
} else {
SelectionEnd = address;
}
} else {
if(address < SelectionEnd) {
SelectionStart = address;
} else {
SelectionStart = SelectionEnd;
SelectionEnd = address;
}
}
ScrollDisplayPosition displayPos = SelectedRowAddress < address ? ScrollDisplayPosition.Bottom : ScrollDisplayPosition.Top;
SelectedRowAddress = address;
ScrollToAddress((uint)address, displayPos);
InvalidateVisual();
}
private bool IsAddressVisible(int address)
{
for(int i = 1; i < VisibleRowCount - 2 && i < Lines.Length; i++) {
if(Lines[i].Address == address) {
return true;
}
}
return false;
}
private bool ScrollToAddress(uint pc, ScrollDisplayPosition position = ScrollDisplayPosition.Center, bool forceScroll = false)
{
if(!forceScroll && IsAddressVisible((int)pc)) {
//Row is already visible, don't scroll
return false;
}
ICodeDataProvider dp = DataProvider;
switch(position) {
case ScrollDisplayPosition.Top: TopAddress = dp.GetRowAddress((int)pc, -1); break;
case ScrollDisplayPosition.Center: TopAddress = dp.GetRowAddress((int)pc, -VisibleRowCount / 2 + 1); break;
case ScrollDisplayPosition.Bottom: TopAddress = dp.GetRowAddress((int)pc, -VisibleRowCount + 2); break;
}
if(!IsAddressVisible((int)pc)) {
TopAddress = dp.GetRowAddress(TopAddress, TopAddress < pc ? 1 : -1);
}
return true;
}
public void CopySelection()
{
DebuggerConfig cfg = Config.Debugger;
string code = GetSelection(cfg.CopyAddresses, cfg.CopyByteCode, cfg.CopyComments, cfg.CopyBlockHeaders, out _, false);
ApplicationHelper.GetMainWindow()?.Clipboard?.SetTextAsync(code);
}
public string GetSelection(bool getAddresses, bool getByteCode, bool getComments, bool getHeaders, out int byteCount, bool skipGeneratedJmpSubLabels)
{
ICodeDataProvider dp = DataProvider;
const int commentSpacingCharCount = 25;
int addrSize = dp.CpuType.GetAddressSize();
string addrFormat = "X" + addrSize;
StringBuilder sb = new StringBuilder();
int i = SelectionStart;
int endAddress = 0;
CodeLineData? prevLine = null;
AddressDisplayType addressDisplayType = Config.Debugger.AddressDisplayType;
do {
CodeLineData[] data = dp.GetCodeLines(i, 5000);
for(int j = 0; j < data.Length; j++) {
CodeLineData lineData = data[j];
if(prevLine?.Address == lineData.Address && prevLine?.Text == lineData.Text) {
continue;
}
if(lineData.Address > SelectionEnd) {
i = lineData.Address;
break;
}
string codeString = lineData.Text.Trim();
if(lineData.Flags.HasFlag(LineFlags.BlockEnd) || lineData.Flags.HasFlag(LineFlags.BlockStart)) {
if(getHeaders) {
codeString = "--------" + codeString + "--------";
} else {
if(j == data.Length - 1) {
i = lineData.Address;
}
continue;
}
}
prevLine = lineData;
int padding = Math.Max(commentSpacingCharCount, codeString.Length);
if(codeString.Length == 0) {
padding = 0;
}
codeString = codeString.PadRight(padding);
bool indentText = !(lineData.Flags.HasFlag(LineFlags.ShowAsData) || lineData.Flags.HasFlag(LineFlags.BlockStart) || lineData.Flags.HasFlag(LineFlags.BlockEnd) || lineData.Flags.HasFlag(LineFlags.Label) || (lineData.Flags.HasFlag(LineFlags.Comment) && lineData.Text.Length == 0));
string line = (indentText ? " " : "") + codeString;
if(getByteCode) {
line = lineData.ByteCodeStr.PadRight(13) + line;
}
if(getAddresses) {
string addressText = lineData.GetAddressText(addressDisplayType, addrFormat);
line = addressText.PadRight(addrSize) + " " + line;
}
if(getComments && !string.IsNullOrWhiteSpace(lineData.Comment)) {
line = line + lineData.Comment;
}
//Skip lines that contain a jump/sub "label" (these aren't
bool skipLine = skipGeneratedJmpSubLabels && lineData.Flags.HasFlag(LineFlags.Label) && lineData.Text.StartsWith("$");
string result = line.TrimEnd();
if(!skipLine && result.Length > 0) {
sb.AppendLine(result);
}
i = lineData.Address;
endAddress = lineData.Address + lineData.OpSize - 1;
}
} while(i < SelectionEnd);
if(SelectionStart <= endAddress) {
byteCount = endAddress - SelectionStart + 1;
} else {
byteCount = 0;
}
return sb.ToString();
}
}
public enum ScrollDisplayPosition
{
Top,
Center,
Bottom
}
}