diff --git a/SourceGenWPF/DisplayList.cs b/SourceGenWPF/DisplayList.cs index a52da10..f3a4999 100644 --- a/SourceGenWPF/DisplayList.cs +++ b/SourceGenWPF/DisplayList.cs @@ -14,49 +14,276 @@ * limitations under the License. */ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; using System.Diagnostics; -using System.Windows.Media; +using System.ComponentModel; +using System.Linq; using System.Text; - -using Asm65; +using System.Threading.Tasks; namespace SourceGenWPF { /// - /// Converts file data and Anattrib contents into a series of strings and format metadata. - /// This is used as the backing store for ProjectView's codeListView. + /// List of items formatted for display. /// - public class DisplayList { - /// - /// List of display lines. - /// - private List mLineList; + /// + /// This is intended to be useful as an ItemSource for a WPF ListView. We need to implement + /// plain IList to cause ListView to perform data virtualization, and the property/collection + /// changed events so the view will pick up our changes. + /// + /// The ItemsControl.ItemsSource property wants an IEnumerable (which IList implements). + /// According to various articles, if the object implements IList, and the UI element + /// is providing UI virtualization, you will also get data virtualization. This behavior + /// doesn't seem to be documented anywhere, but the consensus is that it's expected to work. + /// + /// Implementing generic IList doesn't seem necessary for XAML, but is useful for other + /// customers of the data (e.g. the assembler source generator). + /// + public class DisplayList : IList, IList, + INotifyCollectionChanged, INotifyPropertyChanged { + + // TODO: check VirtualizingStackPanel.VirtualizationMode == recycling (page 259) /// - /// Project that contains the data we're formatting, notably the FileData and - /// Anattribs arrays. + /// List of formatted parts. The idea is that the list is initially populated with + /// null references, and FormattedParts objects are generated on demand. /// - private DisasmProject mProject; + private List mList; /// - /// Code/data formatter. + /// Constructs an empty collection, with the default initial capacity. /// - private Formatter mFormatter; + public DisplayList() { + mList = new List(); + } - /// - /// If set, prepend cycle counts to EOL comments. - /// - private bool mShowCycleCounts; + public DisplayList(int count) { + mList = new List(count); + for (int i = 0; i < count; i++) { + mList.Add(null); + } + } + + + + #region Property / Collection Changed + + public event NotifyCollectionChangedEventHandler CollectionChanged; + public event PropertyChangedEventHandler PropertyChanged; + + // See ObservableCollection class, e.g. + // https://github.com/Microsoft/referencesource/blob/master/System/compmod/system/collections/objectmodel/observablecollection.cs + + private const string CountString = "Count"; + private const string IndexerName = "Item[]"; + +#if false + protected override void ClearItems() { + base.ClearItems(); + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionReset(); + } + + protected override void RemoveItem(int index) { + FormattedParts removedItem = this[index]; + + base.RemoveItem(index); + + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index); + } + + protected override void InsertItem(int index, FormattedParts item) { + base.InsertItem(index, item); + + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); + } + + protected override void SetItem(int index, FormattedParts item) { + FormattedParts originalItem = this[index]; + base.SetItem(index, item); + + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Replace, originalItem, item, index); + } +#endif + + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { + PropertyChanged?.Invoke(this, e); + } + + private void OnPropertyChanged(string propertyName) { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { + CollectionChanged?.Invoke(this, e); + } + + private void OnCollectionChanged(NotifyCollectionChangedAction action, + object item, int index) { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index)); + } + + private void OnCollectionChanged(NotifyCollectionChangedAction action, + object item, int index, int oldIndex) { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex)); + } + + private void OnCollectionChanged(NotifyCollectionChangedAction action, + object oldItem, object newItem, int index) { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index)); + } + + private void OnCollectionReset() { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + #endregion Property / Collection Changed + + + + #region IList / IList + public int Count => ((IList)mList).Count; + + public bool IsReadOnly => ((IList)mList).IsReadOnly; + + public bool IsFixedSize => ((IList)mList).IsFixedSize; + + public object SyncRoot => ((IList)mList).SyncRoot; + + public bool IsSynchronized => ((IList)mList).IsSynchronized; + + public void Add(FormattedParts item) { + ((IList)mList).Add(item); + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionReset(); + } + + public int Add(object value) { + int posn = ((IList)mList).Add(value); + if (posn >= 0) { + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Add, value, posn); + } + return posn; + } + + public void Clear() { + ((IList)mList).Clear(); + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionReset(); + } + + public bool Contains(FormattedParts item) { + return ((IList)mList).Contains(item); + } + bool IList.Contains(object value) { + return Contains((FormattedParts)value); + } + + public void CopyTo(FormattedParts[] array, int arrayIndex) { + ((IList)mList).CopyTo(array, arrayIndex); + } + + public void CopyTo(Array array, int index) { + ((IList)mList).CopyTo(array, index); + } + + public IEnumerator GetEnumerator() { + // Use the indexer, rather than mList's enumerator, to get on-demand string gen. + for (int i = 0; i < Count; i++) { + yield return this[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public int IndexOf(FormattedParts item) { + return ((IList)mList).IndexOf(item); + } + int IList.IndexOf(object value) { + return IndexOf((FormattedParts)value); + } + + public void Insert(int index, FormattedParts item) { + ((IList)mList).Insert(index, item); + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); + } + void IList.Insert(int index, object value) { + Insert(index, (FormattedParts)value); + } + + public void RemoveAt(int index) { + FormattedParts removed = mList[index]; + ((IList)mList).RemoveAt(index); + OnPropertyChanged(CountString); + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Remove, removed, index); + } + + public bool Remove(FormattedParts item) { + // NotifyCollectionChangedAction.Remove wants an index. We can find the index + // of the first matching item and then do a RemoveAt, but this call just isn't + // all that interesting for us, so it's easier to ignore it. + //return ((IList)mList).Remove(item); + throw new NotSupportedException(); + } + + void IList.Remove(object value) { + //Remove((FormattedParts)value); + throw new NotSupportedException(); + } + + object IList.this[int index] { + // forward to generic impl + get { return this[index]; } + set { this[index] = (FormattedParts)value; } + } + + // For IList. + public FormattedParts this[int index] { + get { + FormattedParts parts = mList[index]; + if (parts == null) { + parts = mList[index] = GetEntry(index); + } + return parts; + } + set { + FormattedParts orig = mList[index]; + mList[index] = value; + OnPropertyChanged(IndexerName); + OnCollectionChanged(NotifyCollectionChangedAction.Replace, orig, value, index); + } + } + + #endregion IList / IList - /// - /// Names for pseudo-ops. - /// - private PseudoOp.PseudoOpNames mPseudoOpNames; /// - /// Holds a collection of formatted strings. Instances are immutable. + /// Retrieves the Nth element. /// + private FormattedParts GetEntry(int index) { + Debug.WriteLine("GEN " + index); + return FormattedParts.Create("off" + index, "addr" + index, "12 34", + "vncidmx", "", "yup:", "LDA", "$1234", "a & b"); + } + public class FormattedParts { public string Offset { get; private set; } public string Addr { get; private set; } @@ -68,12 +295,12 @@ namespace SourceGenWPF { public string Operand { get; private set; } public string Comment { get; private set; } - // Use factory methods. + // Construct with factory methods. private FormattedParts() { } public static FormattedParts Create(string offset, string addr, string bytes, string flags, string attr, string label, string opcode, string operand, - string comment, string debug) { + string comment) { FormattedParts parts = new FormattedParts(); parts.Offset = offset; parts.Addr = addr; @@ -84,1141 +311,9 @@ namespace SourceGenWPF { parts.Opcode = opcode; parts.Operand = operand; parts.Comment = comment; + return parts; } - - public static FormattedParts CreateBlankLine() { - FormattedParts parts = new FormattedParts(); - return parts; - } - - public static FormattedParts CreateLongComment(string comment) { - FormattedParts parts = new FormattedParts(); - parts.Comment = comment; - return parts; - } - - public static FormattedParts CreateDirective(string opstr, string addrStr) { - FormattedParts parts = new FormattedParts(); - parts.Opcode = opstr; - parts.Operand = addrStr; - return parts; - } - - public static FormattedParts CreateEquDirective(string label, string opstr, - string addrStr, string comment) { - FormattedParts parts = new FormattedParts(); - parts.Label = label; - parts.Opcode = opstr; - parts.Operand = addrStr; - parts.Comment = comment; - return parts; - } - } - - /// - /// One of these per line of output in the display. It should be possible to draw - /// all of the output without needing to refer back to the project data. (Currently - /// making an exception for some selection-dependent field highlighting.) - /// - /// Base fields are immutable, but the Parts property is set after creation. - /// - public class Line { - // Extremely-negative offset value ensures it's at the very top. - public const int HEADER_COMMENT_OFFSET = int.MinValue + 1; - - [FlagsAttribute] - public enum Type { - Unclassified = 0, - - // Primary functional items. - Code = 1 << 0, - Data = 1 << 1, // includes inline data - CodeOrData = (Code | Data), - - // Decorative items, added by user or formatter. - LongComment = 1 << 2, - Note = 1 << 3, - Blank = 1 << 4, - - // Assembler directives. - OrgDirective = 1 << 5, - EquDirective = 1 << 6, - RegWidthDirective = 1 << 7, - } - - /// - /// Line type. - /// - public Type LineType { get; private set; } - - /// - /// Numeric offset value. Used to map a line item to the Anattrib. Note this is - /// set for all lines, and is the same for all lines in a multi-line sequence, - /// e.g. every line in a long comment has the file offset with which it is associated. - /// - public int FileOffset { get; private set; } - - /// - /// Number of offsets this line covers. Will be > 0 for code and data, zero for - /// everything else. The same value is used for all lines in a multi-line sequence. - /// - public int OffsetSpan { get; private set; } - - /// - /// For multi-line entries, this indicates which line is represented. For - /// single-line entries, this will be zero. - /// - public int SubLineIndex { get; private set; } - - /// - /// Strings for display. Creation may be deferred. Use the DisplayList - /// GetFormattedParts() method to access this property. - /// - public FormattedParts Parts { get; set; } - - /// - /// Background color, used for notes. - /// - public Color BackgroundColor { get; set; } - - /// - /// String for searching. May be created on demand when the Line is first searched. - /// - public string SearchString { get; set; } - - - public Line(int offset, int span, Type type) : this(offset, span, type, 0) { } - - public Line(int offset, int span, Type type, int subLineIndex) { - FileOffset = offset; - OffsetSpan = span; - LineType = type; - SubLineIndex = subLineIndex; - } - - /// - /// True if this line is code or data. - /// - public bool IsCodeOrData { - get { - return LineType == Type.Code || LineType == Type.Data; - } - } - - /// - /// Returns true if the specified offset is represented by this line. There - /// will be only one code/data line for a given offset, but there may be - /// multiple others (comments, notes, etc.) associated with it. - /// - /// - /// - public bool Contains(int offset) { - // Note OffsetSpan can be zero. - return (offset == FileOffset || - (offset >= FileOffset && offset < FileOffset + OffsetSpan)); - } - - public override string ToString() { - return "Line type=" + LineType + " off=+" + FileOffset.ToString("x6") + - " span=" + OffsetSpan; - } - } - - /// - /// Captures the set of selected lines. Lines are identified by offset and type. - /// - /// The idea is to save the selection, rebuild the list -- potentially moving - /// stuff around -- and then rebuild the selection bitmap by finding matching - /// items. - /// - /// We don't try to identify parts of multi-line things. If you've selected - /// part of a multi-line string, then when we restore the selection you'll have - /// the entire string selected. For the operations that are possible across - /// multiple offsets, this seems like reasonable behavior. - /// - /// We can't precisely restore the selection in terms of which file offsets - /// are selected. If you select one byte and apply a code hint, we'll restore - /// the selection to a line with 1-4 bytes. This gets weird if you hit "undo", - /// as you will then have 1-4 bytes selected rather than the original one. It - /// might be better to just clear the selection on "undo". - /// - public class SavedSelection { - private class Tag { - public int mOffset; - public int mSpan; - public Line.Type mTypes; - - public Tag(int offset, int span, Line.Type lineType) { - //Debug.Assert(offset >= 0); - Debug.Assert(span >= 0); - mOffset = offset; - mSpan = (span == 0) ? 1 : span; - mTypes = lineType; - } - } - - private List mSelectionTags = new List(); - - /// - /// This is a place to save the file offset associated with the ListView's - /// TopItem, so we can position the list appropriately. - /// - private int mTopOffset; - - // Use Generate(). - private SavedSelection() { } - - /// - /// Creates a new SavedSelection object, generating a list of tags from the - /// lines that are currently selected. - /// - /// If nothing is selected, SavedSelection will have no members. - /// - /// Display list, with list of Lines. - /// Bit vector specifying which lines are selected. - /// New SavedSelection object. - public static SavedSelection Generate(DisplayList dl, VirtualListViewSelection sel, - int topOffset) { - SavedSelection savedSel = new SavedSelection(); - //Debug.Assert(topOffset >= 0); - savedSel.mTopOffset = topOffset; - - List lineList = dl.mLineList; - Debug.Assert(lineList.Count == sel.Length); - - // Generate tags, which are a combination of the offset, span, and a merge - // of types of all the lines associated with that offset. - // - // We may want to consider some sort of optimization for a "select all" - // operation, although there aren't many changes you can make after selecting - // all lines in a very large file. - Tag tag = null; - int curOffset = -1; - for (int i = 0; i < lineList.Count; i++) { - if (!sel[i]) { - continue; - } - Line line = lineList[i]; - // Code hinting can transform code to data and vice-versa, so we - // want the tag to reflect the fact that both could exist. - Line.Type lineType = line.LineType; - if (lineType == Line.Type.Code || lineType == Line.Type.Data) { - lineType = Line.Type.CodeOrData; - } - if (line.FileOffset != curOffset) { - // advanced to new offset, flush previous - if (tag != null) { - savedSel.mSelectionTags.Add(tag); - } - curOffset = line.FileOffset; - - tag = new Tag(line.FileOffset, line.OffsetSpan, lineType); - } else { - // another item at same offset - tag.mSpan = Math.Max(tag.mSpan, line.OffsetSpan); - tag.mTypes |= lineType; - } - } - if (curOffset == -1) { - // It's hard to cause an action that requires save/restore when you don't - // have anything selected in the ListView. However, this can happen if - // you do a sequence like: - // - Open a file that starts with a JMP followed by data. - // - Click on the blank line below the code, which has the code's offset, - // and select "remove hint". This causes the blank line to vanish, - // so the Restore() won't select anything. - // - Click "undo". - Debug.WriteLine("NOTE: no selection found"); - } else { - // Add the in-progress tag to the list. - savedSel.mSelectionTags.Add(tag); - } - - return savedSel; - } - - /// - /// Creates a selection set by identifying the set of lines in the display list - /// that correspond to items in the SavedSelection tag list. - /// - /// Display list, with list of Lines. - /// Set of selected lines. - public VirtualListViewSelection Restore(DisplayList dl, out int topIndex) { - List lineList = dl.mLineList; - VirtualListViewSelection sel = new VirtualListViewSelection(lineList.Count); - - topIndex = -1; - - // Walk through the tag list, which is ordered by ascending offset, and - // through the display list, which is similarly ordered. - int tagIndex = 0; - int lineIndex = 0; - while (tagIndex < mSelectionTags.Count && lineIndex < lineList.Count) { - Tag tag = mSelectionTags[tagIndex]; - int lineOffset = lineList[lineIndex].FileOffset; - - // If a line encompassing this offset was at the top of the ListView - // control before, use this line's index as the top. - if (topIndex < 0 && lineList[lineIndex].Contains(mTopOffset)) { - topIndex = lineIndex; - } - - if (lineOffset >= tag.mOffset && lineOffset < tag.mOffset + tag.mSpan) { - // Intersection. If the line type matches, add it to the set. - if ((tag.mTypes & lineList[lineIndex].LineType) != 0) { - sel[lineIndex] = true; - } - - // Advance to the next line entry. - lineIndex++; - } else if (tag.mOffset < lineOffset) { - // advance tag - tagIndex++; - } else { - Debug.Assert(tag.mOffset > lineOffset); - lineIndex++; - } - } - - // Continue search for topIndex, if necessary. - while (topIndex < 0 && lineIndex < lineList.Count) { - if (lineList[lineIndex].Contains(mTopOffset)) { - topIndex = lineIndex; - break; - } - lineIndex++; - } - Debug.WriteLine("TopOffset +" + mTopOffset.ToString("x6") + - " --> index " + topIndex); - if (topIndex < 0) { - // This can happen if you delete the header comment while scrolled - // to the top of the list. - topIndex = 0; - } - return sel; - } - - public void DebugDump() { - Debug.WriteLine("Selection (" + mSelectionTags.Count + " offsets):"); - foreach (Tag tag in mSelectionTags) { - Debug.WriteLine(" +" + tag.mOffset.ToString("x6") + "/" + - tag.mSpan + ": " + tag.mTypes); - } - } - } - - - - /// - /// Constructor. - /// - /// Project object. - /// Formatter object. - public DisplayList(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames) { - mProject = proj; - mFormatter = formatter; - mPseudoOpNames = opNames; - - mLineList = new List(); - mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, - false); - } - - /// - /// Changes the Formatter object. Clears the display list, instigating a full re-render. - /// - /// Formatter object. - public void SetFormatter(Formatter formatter) { - mFormatter = formatter; - mLineList.Clear(); - - // We probably just changed settings, so update this as well. - mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, - false); - } - - /// - /// Changes the pseudo-op name object. Clears the display list, instigating a - /// full re-render. - /// - /// Pseudo-op names. - public void SetPseudoOpNames(PseudoOp.PseudoOpNames opNames) { - mPseudoOpNames = opNames; - mLineList.Clear(); - } - - /// - /// Number of lines in the list. - /// - public int Count { get { return mLineList.Count; } } - - /// - /// Retrieves the Nth element. - /// - public Line this[int key] { - get { - return mLineList[key]; - } - } - - /// - /// Returns the Line's FormattedParts object, generating it first if necessary. - /// - /// Object with formatted strings. - public FormattedParts GetFormattedParts(int index) { - Line line = mLineList[index]; - if (line.Parts == null) { - FormattedParts parts; - switch (line.LineType) { - case Line.Type.Code: - parts = GenerateInstructionLine(mProject, mFormatter, - line.FileOffset, line.OffsetSpan, mShowCycleCounts); - break; - case Line.Type.Data: - parts = GenerateDataLine(mProject, mFormatter, mPseudoOpNames, - line.FileOffset, line.SubLineIndex); - break; - case Line.Type.Blank: - // Nothing to do. - parts = FormattedParts.CreateBlankLine(); - break; - case Line.Type.OrgDirective: - case Line.Type.RegWidthDirective: - case Line.Type.LongComment: - case Line.Type.Note: - // should have been done already - default: - Debug.Assert(false); - parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x", - "x", "x"); - break; - } - line.Parts = parts; - } - return line.Parts; - } - - /// - /// Returns a string with the concatenation of the searchable portions of the line. - /// Different sections are separated with an unlikely unicode character. The goal - /// is to have a single string per line that can be searched quickly, without having - /// adjacent fields spill into each other. - /// - /// Line index. - /// Formatted line contents. - public string GetSearchString(int index) { - Line line = mLineList[index]; - if (line.SearchString == null) { - const char sep = '\u203b'; // REFERENCE MARK - - FormattedParts parts = GetFormattedParts(index); - StringBuilder sb = new StringBuilder(); - // Some parts may be null, e.g. for long comments. Append() can deal. - sb.Append(parts.Label); - sb.Append(sep); - sb.Append(parts.Opcode); - sb.Append(sep); - sb.Append(parts.Operand); - sb.Append(sep); - sb.Append(parts.Comment); - line.SearchString = sb.ToString(); - } - return line.SearchString; - } - - /// - /// Finds the first line entry that encompasses the specified offset. - /// - /// Offset to search for. Negative values are allowed. - /// Line list index, or -1 if not found. - private static int FindLineByOffset(List lineList, int offset) { - if (lineList.Count == 0) { - return -1; - } - - int low = 0; - int high = lineList.Count - 1; - int mid = -1; - bool found = false; - while (low <= high) { - mid = (low + high) / 2; - Line line = lineList[mid]; - - if (line.Contains(offset)) { - // found a match - found = true; - break; - } else if (line.FileOffset > offset) { - // too big, move the high end in - high = mid - 1; - } else if (line.FileOffset < offset) { - // too small, move the low end in - low = mid + 1; - } else { - // WTF - throw new Exception("Bad binary search"); - } - } - - if (!found) { - return -1; - } - - // We found *a* matching line. Seek backward to find the *first* matching line. - while (mid > 0) { - Line upLine = lineList[mid - 1]; - if (upLine.Contains(offset)) { - mid--; - } else { - break; - } - } - - return mid; - } - - /// - /// Finds the first line entry that encompasses the specified offset. - /// - /// Offset to search for. - /// Line list index, or -1 if not found. - public int FindLineIndexByOffset(int offset) { - return FindLineByOffset(mLineList, offset); - } - - /// - /// Finds the code or data line entry that encompasses the specified offset. - /// - /// Offset to search for. - /// Line list index, or -1 if not found. - public int FindCodeDataIndexByOffset(int offset) { - if (offset < 0) { - // Header offset. No code or data here. - return -1; - } - int index = FindLineByOffset(mLineList, offset); - if (index < 0) { - return -1; - } - while (mLineList[index].LineType != Line.Type.Code && - mLineList[index].LineType != Line.Type.Data) { - index++; - } - return index; - } - - /// - /// Generates Lines for the entire project. - /// - public void GenerateAll() { - mLineList.Clear(); - GenerateHeaderLines(mProject, mFormatter, mPseudoOpNames, mLineList); - GenerateLineList(mProject, mFormatter, mPseudoOpNames, - 0, mProject.FileData.Length - 1, mLineList); - - Debug.Assert(ValidateLineList(), "Display list failed validation"); - } - - /// - /// Generates a list of Lines for the specified range of offsets, replacing - /// existing values. - /// - /// First offset. Must be the start of an instruction - /// or data area. - /// End offset (inclusive). - public void GenerateRange(int startOffset, int endOffset) { - if (startOffset < 0) { - ClearHeaderLines(); - GenerateHeaderLines(mProject, mFormatter, mPseudoOpNames, mLineList); - if (endOffset < 0) { - // nothing else to do - return; - } - // do the rest - startOffset = 0; - } - Debug.Assert(startOffset >= 0); - Debug.Assert(endOffset < mProject.FileData.Length); - Debug.Assert(endOffset >= startOffset); - //Debug.WriteLine("DL gen range [" + startOffset + "," + endOffset + "]"); - - // Find the start index. The start offset should always appear at the - // start of a Line because it comes from item selection. - int startIndex = FindLineByOffset(mLineList, startOffset); - if (startIndex < 0) { - Debug.Assert(false, "Unable to find startOffset " + startOffset); - GenerateAll(); - return; - } - // Find the end index. The end offset can be part of a multi-line data item, like - // a long string. Find the first Line that starts at an offset larger than endOffset. - int endIndex; - if (startOffset == endOffset) { - // Simple optimization for single-offset groups. - endIndex = startIndex; - } else { - endIndex = FindLineByOffset(mLineList, endOffset); - } - if (endIndex < 0) { - Debug.Assert(false, "Unable to find endOffset " + endOffset); - GenerateAll(); - return; - } - // There may be more than one line involved, so we need to scan forward. - for (endIndex++; endIndex < mLineList.Count; endIndex++) { - if (mLineList[endIndex].FileOffset > endOffset) { - endIndex--; - break; - } - } - if (endIndex == mLineList.Count) { - // whoops, loop ended before we had a chance to decrement - endIndex = mLineList.Count - 1; - } - Debug.WriteLine("GenerateRange: offset [+" + startOffset.ToString("x6") + ",+" + - endOffset.ToString("x6") + - "] maps to index [" + startIndex + "," + endIndex + "]"); - Debug.Assert(endIndex >= startIndex); - - // Create temporary list to hold new lines. Set the initial capacity to - // the previous size, on the assumption that it won't change much. - List newLines = new List(endIndex - startIndex + 1); - GenerateLineList(mProject, mFormatter, mPseudoOpNames, startOffset, endOffset, newLines); - - // Out with the old, in with the new. - mLineList.RemoveRange(startIndex, endIndex - startIndex + 1); - mLineList.InsertRange(startIndex, newLines); - - Debug.Assert(ValidateLineList(), "Display list failed validation"); - } - - /// - /// Validates the line list, confirming that every offset is represented exactly once. - /// - /// True if all is well. - private bool ValidateLineList() { - int expectedOffset = 0; - int lastOffset = Int32.MinValue; - foreach (Line line in mLineList) { - // Header lines aren't guaranteed to be sequential and don't have a span. - // They are expected to be in sorted order, and to be unique (with the - // notable exception of the header comment, which is multi-line). - if (line.FileOffset < 0) { - if (line.FileOffset < lastOffset || (line.LineType != Line.Type.LongComment && - line.FileOffset == lastOffset)) { - Debug.WriteLine("Header offsets went backward: cur=" + - line.FileOffset + " last=" + lastOffset); - return false; - } - lastOffset = line.FileOffset; - continue; - } - - // Blank lines and comments can appear before or after code/data. They - // must have the offset of the associated line, and a span of zero. - if (line.FileOffset != expectedOffset && line.FileOffset != lastOffset) { - Debug.WriteLine("ValidateLineList: bad offset " + line.FileOffset + - " (last=" + lastOffset + ", expected next=" + expectedOffset + ")"); - return false; - } - - if (line.SubLineIndex != 0) { - // In the middle of a multi-line thing, don't advance last/expected. - Debug.Assert(line.FileOffset == lastOffset); - } else { - lastOffset = expectedOffset; - expectedOffset += line.OffsetSpan; - } - } - - if (expectedOffset != mProject.FileData.Length) { - Debug.WriteLine("ValidateLineList: did not cover entire file: last offset " + - expectedOffset + ", file has " + mProject.FileData.Length); - return false; - } - - return true; - } - - /// - /// Removes all header lines from the display list. - /// - private void ClearHeaderLines() { - // Find the first non-header item. - int endIndex = FindLineByOffset(mLineList, 0); - if (endIndex == 0) { - // no header lines present - Debug.WriteLine("No header lines found"); - return; - } - Debug.WriteLine("Removing " + endIndex + " header lines"); - mLineList.RemoveRange(0, endIndex); - } - - /// - /// Generates a synthetic offset for the FileOffset field from an index value. The - /// index arg is the index of an entry in the DisasmProject.ActiveDefSymbolList. - /// (The exact algorithm isn't too important, as these offsets are not stored in the - /// project file.) - /// - private static int DefSymOffsetFromIndex(int index) { - Debug.Assert(index >= 0 && index < (1 << 24)); - return index - (1 << 24); - } - - /// - /// Returns the DisasmProject.ActiveDefSymbolList index for an EQU line with - /// the specified file offset. - /// - public static int DefSymIndexFromOffset(int offset) { - Debug.Assert(offset < 0); - return offset + (1 << 24); - } - - /// - /// Generates the header lines (header comment, EQU directives), and inserts them at - /// the top of the list. - /// - /// This does not currently do incremental generation. Call ClearHeaderLines() before - /// calling here if you're not starting with an empty list. - /// - /// Project reference. - /// Output formatter. - /// Pseudo-op names. - /// List to add output lines to. - private static void GenerateHeaderLines(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames, List fullLines) { - List tmpLines = new List(); - Line line; - FormattedParts parts; - - // Check for header comment. - if (proj.LongComments.TryGetValue(Line.HEADER_COMMENT_OFFSET, - out MultiLineComment headerComment)) { - List formatted = headerComment.FormatText(formatter, string.Empty); - StringListToLines(formatted, Line.HEADER_COMMENT_OFFSET, Line.Type.LongComment, - Color.FromArgb(0, 0, 0, 0), tmpLines); - } - - // Format symbols. - int index = 0; - foreach (DefSymbol defSym in proj.ActiveDefSymbolList) { - line = new Line(DefSymOffsetFromIndex(index), 0, Line.Type.EquDirective); - // Use an operand length of 1 so things are shown as concisely as possible. - string valueStr = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, - null, defSym.DataDescriptor, defSym.Value, 1, - PseudoOp.FormatNumericOpFlags.None); - string comment = formatter.FormatEolComment(defSym.Comment); - parts = FormattedParts.CreateEquDirective(defSym.Label, - formatter.FormatPseudoOp(opNames.EquDirective), - valueStr, comment); - line.Parts = parts; - tmpLines.Add(line); - index++; - } - - if (proj.ActiveDefSymbolList.Count != 0) { - // We had some EQUs, throw a blank line at the end. - index++; - line = new Line(DefSymOffsetFromIndex(index), 0, Line.Type.Blank); - tmpLines.Add(line); - } - - fullLines.InsertRange(0, tmpLines); - } - - /// - /// Generates lines for the specified range of file offsets. - /// - /// Does not generate formatted parts in most cases; that usually happens on demand. - /// Complicated items, such as word-wrapped long comments, may be generated now - /// and saved off. - /// - /// This still needs a formatter arg even when no text is rendered because some - /// options, like maximum per-line operand length, might affect how many lines - /// are generated. - /// - /// Project reference. - /// Output formatter. - /// Offset of first byte. - /// Offset of last byte. - /// List to add output lines to. - private static void GenerateLineList(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames, int startOffset, int endOffset, List lines) { - //Debug.WriteLine("GenerateRange [+" + startOffset.ToString("x6") + ",+" + - // endOffset.ToString("x6") + "]"); - - Debug.Assert(startOffset >= 0); - Debug.Assert(endOffset >= startOffset); - - // Find the previous status flags for M/X tracking. - StatusFlags prevFlags = StatusFlags.AllIndeterminate; - if (proj.CpuDef.HasEmuFlag) { - for (int scanoff = startOffset - 1; scanoff >= 0; scanoff--) { - Anattrib attr = proj.GetAnattrib(scanoff); - if (attr.IsInstructionStart) { - prevFlags = attr.StatusFlags; - // Apply the same tweak here that we do to curFlags below. - prevFlags.M = attr.StatusFlags.ShortM ? 1 : 0; - prevFlags.X = attr.StatusFlags.ShortX ? 1 : 0; - Debug.WriteLine("GenerateLineList startOff=+" + - startOffset.ToString("x6") + " using initial flags from +" + - scanoff.ToString("x6") + ": " + prevFlags); - break; - } - } - } - - // Configure the initial value of addBlank. The specific case we're handling is - // a no-continue instruction (e.g. JMP) followed by an instruction with a label. - // When we rename the label, we don't want the blank to disappear during the - // partial-list generation. - bool addBlank = false; - if (startOffset > 0) { - int baseOff = DataAnalysis.GetBaseOperandOffset(proj, startOffset - 1); - if (proj.GetAnattrib(baseOff).DoesNotContinue) { - addBlank = true; - } - } - - int offset = startOffset; - while (offset <= endOffset) { - Anattrib attr = proj.GetAnattrib(offset); - if (attr.IsInstructionStart && offset > 0 && - proj.GetAnattrib(offset - 1).IsData) { - // Transition from data to code. (Don't add blank line for inline data.) - lines.Add(GenerateBlankLine(offset)); - } else if (addBlank) { - // Previous instruction wanted to be followed by a blank line. - lines.Add(GenerateBlankLine(offset)); - } - addBlank = false; - - // Insert long comments and notes. These may span multiple display lines, - // and require word-wrap, so it's easiest just to render them fully here. - if (proj.Notes.TryGetValue(offset, out MultiLineComment noteData)) { - List formatted = noteData.FormatText(formatter, "NOTE: "); - StringListToLines(formatted, offset, Line.Type.Note, - noteData.BackgroundColor, lines); - } - if (proj.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { - List formatted = longComment.FormatText(formatter, string.Empty); - StringListToLines(formatted, offset, Line.Type.LongComment, - longComment.BackgroundColor, lines); - } - - if (attr.IsInstructionStart) { - // Generate reg width directive, if necessary. - if (proj.CpuDef.HasEmuFlag) { - // Changing from "ambiguous but assumed short" to "definitively short" - // merits a directive, notably at the start of the file. The tricky - // part is that E=1 means definitively M=1 X=1. And maybe - // indeterminate E also means that. - // - // We don't want to mess with Anattrib, but we do need to tell the - // assembler something. So we tweak our local copy and propagate it. - string operandStr = string.Empty; - StatusFlags curFlags = attr.StatusFlags; - curFlags.M = attr.StatusFlags.ShortM ? 1 : 0; - curFlags.X = attr.StatusFlags.ShortX ? 1 : 0; - if (curFlags.M != prevFlags.M) { - operandStr = (curFlags.M == 0) ? "longm" : "shortm"; - } - - if (curFlags.X != prevFlags.X) { - if (operandStr.Length > 0) { - operandStr += ","; - } - operandStr += (curFlags.X == 0) ? "longx" : "shortx"; - } - - if (operandStr.Length > 0) { - Line rwLine = new Line(offset, 0, Line.Type.RegWidthDirective); - // FormatPseudoOp isn't quite right for the operand, but there - // isn't anything more suitable, and there are only eight - // possible values. Having the operand capitalization match the - // pseudo-op's feels reasonable. - rwLine.Parts = FormattedParts.CreateDirective( - formatter.FormatPseudoOp(opNames.RegWidthDirective), - formatter.FormatPseudoOp(operandStr)); - lines.Add(rwLine); - } - prevFlags = curFlags; - } - - // Look for embedded instructions. - int len; - for (len = 1; len < attr.Length; len++) { - if (proj.GetAnattrib(offset + len).IsInstructionStart) { - break; - } - } - - // Create Line entry. Offset span only covers the instruction up to - // the point where the embedded instruction starts. - Line line = new Line(offset, len, Line.Type.Code); - lines.Add(line); - - // Insert blank after an instruction that doesn't continue. Provides a - // break in code, and before a data area. - // TODO(maybe): Might also want to do this if the next offset is data, - // to make things look nicer when code runs directly into data. - // - // We don't want to add it with the current line's offset. If we do that, - // the binary search will get confused, because blank lines have a span - // of zero. If the code is at offset 10 with length 3, and we search for - // the byte at offset 11, then a blank line (with span=0) at offset 10 will - // cause the binary search to assume that the target is farther down, when - // it's actually one line up. We deal with this by setting a flag and - // generating the blank line on the next trip through the loop. - if (attr.DoesNotContinue) { - addBlank = true; - } - - offset += len; - } else { - Debug.Assert(attr.DataDescriptor != null); - int numLines = - PseudoOp.ComputeRequiredLineCount(formatter, attr.DataDescriptor); - for (int i = 0; i < numLines; i++) { - Line line = new Line(offset, attr.Length, Line.Type.Data, i); - lines.Add(line); - } - offset += attr.Length; - } - } - - // See if there were any address shifts in this section. If so, insert an ORG - // statement as the first entry for the offset. We're expecting to have very - // few AddressMap entries (usually just one), so it's more efficient to process - // them here and walk through the sub-list than it is to ping the address map - // at every line. - // - // It should not be possible for an address map change to appear in the middle - // of an instruction or data item. - foreach (AddressMap.AddressMapEntry ent in proj.AddrMap) { - if (ent.Offset < startOffset || ent.Offset > endOffset) { - continue; - } - int index = FindLineByOffset(lines, ent.Offset); - if (index < 0) { - Debug.WriteLine("Couldn't find offset " + ent.Offset + - " in range we just generated"); - Debug.Assert(false); - continue; - } - if (lines[index].LineType == Line.Type.Blank) { - index++; - } - Line topLine = lines[index]; - Line newLine = new Line(topLine.FileOffset, 0, Line.Type.OrgDirective); - string addrStr = formatter.FormatHexValue(ent.Addr, 4); - newLine.Parts = FormattedParts.CreateDirective( - formatter.FormatPseudoOp(opNames.OrgDirective), addrStr); - lines.Insert(index, newLine); - - // Prepend a blank line if the previous line wasn't already blank, and this - // isn't the ORG at the start of the file. (This may temporarily do - // double-spacing if we do a partial update, because we won't be able to - // "see" the previous line. Harmless.) - if (ent.Offset != 0 && index > 0 && lines[index-1].LineType != Line.Type.Blank) { - Line blankLine = new Line(topLine.FileOffset, 0, Line.Type.Blank); - lines.Insert(index, blankLine); - } - } - } - - /// - /// Generates a blank line entry. - /// - private static Line GenerateBlankLine(int offset) { - return new Line(offset, 0, Line.Type.Blank); - } - - /// - /// Takes a list of strings and adds them to the Line list as long comments. - /// - /// - /// - /// - /// - /// - private static void StringListToLines(List list, int offset, Line.Type lineType, - Color color, List lines) { - foreach (string str in list) { - Line line = new Line(offset, 0, lineType); - FormattedParts parts = FormattedParts.CreateLongComment(str); - line.Parts = parts; - line.BackgroundColor = color; - lines.Add(line); - } - } - - private static FormattedParts GenerateInstructionLine(DisasmProject proj, - Formatter formatter, int offset, int instrBytes, bool showCycleCounts) { - Anattrib attr = proj.GetAnattrib(offset); - byte[] data = proj.FileData; - - string offsetStr = formatter.FormatOffset24(offset); - - int addr = attr.Address; - string addrStr = formatter.FormatAddress(addr, !proj.CpuDef.HasAddr16); - string bytesStr = formatter.FormatBytes(data, offset, instrBytes); - string flagsStr = attr.StatusFlags.ToString(proj.CpuDef.HasEmuFlag); - string attrStr = attr.ToAttrString(); - - string labelStr = string.Empty; - if (attr.Symbol != null) { - labelStr = attr.Symbol.Label; - } - - OpDef op = proj.CpuDef.GetOpDef(data[offset]); - int operand = op.GetOperand(data, offset, attr.StatusFlags); - int instrLen = op.GetLength(attr.StatusFlags); - OpDef.WidthDisambiguation wdis = OpDef.WidthDisambiguation.None; - if (op.IsWidthPotentiallyAmbiguous) { - wdis = OpDef.GetWidthDisambiguation(instrLen, operand); - } - - string opcodeStr = formatter.FormatOpcode(op, wdis); - if (attr.Length != instrBytes) { - // An instruction is embedded inside this one. Note that BRK is a two-byte - // instruction, so don't freak out if you see it marked as embedded when a - // $00 is followed by actual code. (But be a little freaked out that your - // code is running into a BRK.) - //opcodeStr = opcodeStr + " \u00bb"; // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - //opcodeStr = opcodeStr + " \u23e9"; // BLACK RIGHT-POINTING DOUBLE TRIANGLE - opcodeStr = opcodeStr + " \u25bc"; // BLACK DOWN-POINTING TRIANGLE - } - - string formattedOperand = null; - int operandLen = instrLen - 1; - PseudoOp.FormatNumericOpFlags opFlags = PseudoOp.FormatNumericOpFlags.None; - - // Tweak branch instructions. We want to show the absolute address rather - // than the relative offset (which happens with the OperandAddress assignment - // below), and 1-byte branches should always appear as a 4-byte hex value. - if (op.AddrMode == OpDef.AddressMode.PCRel) { - Debug.Assert(attr.OperandAddress >= 0); - operandLen = 2; - opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; - } else if (op.AddrMode == OpDef.AddressMode.PCRelLong || - op.AddrMode == OpDef.AddressMode.StackPCRelLong) { - opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; - } else if (op.AddrMode == OpDef.AddressMode.Imm || - op.AddrMode == OpDef.AddressMode.ImmLongA || - op.AddrMode == OpDef.AddressMode.ImmLongXY) { - opFlags = PseudoOp.FormatNumericOpFlags.HasHashPrefix; - } - - // Use the OperandAddress when available. This is important for relative branch - // instructions and PER, where we want to show the target address rather than the - // operand value. - int operandForSymbol = operand; - if (attr.OperandAddress >= 0) { - operandForSymbol = attr.OperandAddress; - } - - // Check Length to watch for bogus descriptors. ApplyFormatDescriptors() should - // have discarded anything appropriate, so we might be able to eliminate this test. - if (attr.DataDescriptor != null && attr.Length == attr.DataDescriptor.Length) { - Debug.Assert(operandLen > 0); - - // Format operand as directed. - if (op.AddrMode == OpDef.AddressMode.BlockMove) { - // Special handling for the double-operand block move. - string opstr1 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, - null, attr.DataDescriptor, operand >> 8, 1, - PseudoOp.FormatNumericOpFlags.None); - string opstr2 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, - null, attr.DataDescriptor, operand & 0xff, 1, - PseudoOp.FormatNumericOpFlags.None); - formattedOperand = opstr1 + "," + opstr2; - } else { - formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, - null, attr.DataDescriptor, operandForSymbol, operandLen, opFlags); - } - } else { - // Show operand value in hex. - if (op.AddrMode == OpDef.AddressMode.BlockMove) { - formattedOperand = formatter.FormatHexValue(operand >> 8, 2) + "," + - formatter.FormatHexValue(operand & 0xff, 2); - } else { - if (operandLen == 2) { - // This is necessary for 16-bit operands, like "LDA abs" and "PEA val", - // when outside bank zero. The bank is included in the operand address, - // but we don't want to show it here. - operandForSymbol &= 0xffff; - } - formattedOperand = formatter.FormatHexValue(operandForSymbol, operandLen * 2); - } - } - string operandStr = formatter.FormatOperand(op, formattedOperand, wdis); - - string eolComment = proj.Comments[offset]; - if (showCycleCounts) { - bool branchCross = (attr.Address & 0xff00) != (operandForSymbol & 0xff00); - int cycles = proj.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, attr.BranchTaken, - branchCross); - if (cycles > 0) { - eolComment = cycles.ToString() + " " + eolComment; - } else { - eolComment = (-cycles).ToString() + "+ " + eolComment; - } - } - string commentStr = formatter.FormatEolComment(eolComment); - - string debugStr = string.Empty; - //debugStr = "opOff=" + - // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); - - FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, - flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); - return parts; - } - - private static FormattedParts GenerateDataLine(DisasmProject proj, Formatter formatter, - PseudoOp.PseudoOpNames opNames, int offset, int subLineIndex) { - Anattrib attr = proj.GetAnattrib(offset); - byte[] data = proj.FileData; - - string offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, - operandStr, commentStr, debugStr; - offsetStr = addrStr = bytesStr = flagsStr = attrStr = labelStr = opcodeStr = - operandStr = commentStr = debugStr = string.Empty; - - PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(formatter, opNames, proj.SymbolTable, - null, attr.DataDescriptor, proj.FileData, offset, subLineIndex); - if (subLineIndex == 0) { - offsetStr = formatter.FormatOffset24(offset); - - addrStr = formatter.FormatAddress(attr.Address, !proj.CpuDef.HasAddr16); - if (attr.Symbol != null) { - labelStr = attr.Symbol.Label; - } - - bytesStr = formatter.FormatBytes(data, offset, attr.Length); - attrStr = attr.ToAttrString(); - - opcodeStr = formatter.FormatPseudoOp(pout.Opcode); - } else { - opcodeStr = " +"; - } - - operandStr = pout.Operand; - - if (subLineIndex == 0) { - commentStr = formatter.FormatEolComment(proj.Comments[offset]); - - //debugStr = "opOff=" + - // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); - } - - FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, - flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); - return parts; } } } diff --git a/SourceGenWPF/DisplayListGen.cs b/SourceGenWPF/DisplayListGen.cs new file mode 100644 index 0000000..ee46d02 --- /dev/null +++ b/SourceGenWPF/DisplayListGen.cs @@ -0,0 +1,1223 @@ +/* + * Copyright 2019 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Windows.Media; +using System.Text; + +using Asm65; + +namespace SourceGenWPF { + /// + /// Converts file data and Anattrib contents into a series of strings and format metadata. + /// + public class DisplayListGen { + /// + /// List of display lines. + /// + private List mLineList; + + /// + /// Project that contains the data we're formatting, notably the FileData and + /// Anattribs arrays. + /// + private DisasmProject mProject; + + /// + /// Code/data formatter. + /// + private Formatter mFormatter; + + /// + /// If set, prepend cycle counts to EOL comments. + /// + private bool mShowCycleCounts; + + /// + /// Names for pseudo-ops. + /// + private PseudoOp.PseudoOpNames mPseudoOpNames; + + + /// + /// Holds a collection of formatted strings. Instances are immutable. + /// + public class FormattedParts { + public string Offset { get; private set; } + public string Addr { get; private set; } + public string Bytes { get; private set; } + public string Flags { get; private set; } + public string Attr { get; private set; } + public string Label { get; private set; } + public string Opcode { get; private set; } + public string Operand { get; private set; } + public string Comment { get; private set; } + + // Use factory methods. + private FormattedParts() { } + + public static FormattedParts Create(string offset, string addr, string bytes, + string flags, string attr, string label, string opcode, string operand, + string comment, string debug) { + FormattedParts parts = new FormattedParts(); + parts.Offset = offset; + parts.Addr = addr; + parts.Bytes = bytes; + parts.Flags = flags; + parts.Attr = attr; + parts.Label = label; + parts.Opcode = opcode; + parts.Operand = operand; + parts.Comment = comment; + return parts; + } + + public static FormattedParts CreateBlankLine() { + FormattedParts parts = new FormattedParts(); + return parts; + } + + public static FormattedParts CreateLongComment(string comment) { + FormattedParts parts = new FormattedParts(); + parts.Comment = comment; + return parts; + } + + public static FormattedParts CreateDirective(string opstr, string addrStr) { + FormattedParts parts = new FormattedParts(); + parts.Opcode = opstr; + parts.Operand = addrStr; + return parts; + } + + public static FormattedParts CreateEquDirective(string label, string opstr, + string addrStr, string comment) { + FormattedParts parts = new FormattedParts(); + parts.Label = label; + parts.Opcode = opstr; + parts.Operand = addrStr; + parts.Comment = comment; + return parts; + } + } + + /// + /// One of these per line of output in the display. It should be possible to draw + /// all of the output without needing to refer back to the project data. (Currently + /// making an exception for some selection-dependent field highlighting.) + /// + /// Base fields are immutable, but the Parts property is set after creation. + /// + public class Line { + // Extremely-negative offset value ensures it's at the very top. + public const int HEADER_COMMENT_OFFSET = int.MinValue + 1; + + [FlagsAttribute] + public enum Type { + Unclassified = 0, + + // Primary functional items. + Code = 1 << 0, + Data = 1 << 1, // includes inline data + CodeOrData = (Code | Data), + + // Decorative items, added by user or formatter. + LongComment = 1 << 2, + Note = 1 << 3, + Blank = 1 << 4, + + // Assembler directives. + OrgDirective = 1 << 5, + EquDirective = 1 << 6, + RegWidthDirective = 1 << 7, + } + + /// + /// Line type. + /// + public Type LineType { get; private set; } + + /// + /// Numeric offset value. Used to map a line item to the Anattrib. Note this is + /// set for all lines, and is the same for all lines in a multi-line sequence, + /// e.g. every line in a long comment has the file offset with which it is associated. + /// + public int FileOffset { get; private set; } + + /// + /// Number of offsets this line covers. Will be > 0 for code and data, zero for + /// everything else. The same value is used for all lines in a multi-line sequence. + /// + public int OffsetSpan { get; private set; } + + /// + /// For multi-line entries, this indicates which line is represented. For + /// single-line entries, this will be zero. + /// + public int SubLineIndex { get; private set; } + + /// + /// Strings for display. Creation may be deferred. Use the DisplayList + /// GetFormattedParts() method to access this property. + /// + public FormattedParts Parts { get; set; } + + /// + /// Background color, used for notes. + /// + public Color BackgroundColor { get; set; } + + /// + /// String for searching. May be created on demand when the Line is first searched. + /// + public string SearchString { get; set; } + + + public Line(int offset, int span, Type type) : this(offset, span, type, 0) { } + + public Line(int offset, int span, Type type, int subLineIndex) { + FileOffset = offset; + OffsetSpan = span; + LineType = type; + SubLineIndex = subLineIndex; + } + + /// + /// True if this line is code or data. + /// + public bool IsCodeOrData { + get { + return LineType == Type.Code || LineType == Type.Data; + } + } + + /// + /// Returns true if the specified offset is represented by this line. There + /// will be only one code/data line for a given offset, but there may be + /// multiple others (comments, notes, etc.) associated with it. + /// + /// + /// + public bool Contains(int offset) { + // Note OffsetSpan can be zero. + return (offset == FileOffset || + (offset >= FileOffset && offset < FileOffset + OffsetSpan)); + } + + public override string ToString() { + return "Line type=" + LineType + " off=+" + FileOffset.ToString("x6") + + " span=" + OffsetSpan; + } + } + + /// + /// Captures the set of selected lines. Lines are identified by offset and type. + /// + /// The idea is to save the selection, rebuild the list -- potentially moving + /// stuff around -- and then rebuild the selection bitmap by finding matching + /// items. + /// + /// We don't try to identify parts of multi-line things. If you've selected + /// part of a multi-line string, then when we restore the selection you'll have + /// the entire string selected. For the operations that are possible across + /// multiple offsets, this seems like reasonable behavior. + /// + /// We can't precisely restore the selection in terms of which file offsets + /// are selected. If you select one byte and apply a code hint, we'll restore + /// the selection to a line with 1-4 bytes. This gets weird if you hit "undo", + /// as you will then have 1-4 bytes selected rather than the original one. It + /// might be better to just clear the selection on "undo". + /// + public class SavedSelection { + private class Tag { + public int mOffset; + public int mSpan; + public Line.Type mTypes; + + public Tag(int offset, int span, Line.Type lineType) { + //Debug.Assert(offset >= 0); + Debug.Assert(span >= 0); + mOffset = offset; + mSpan = (span == 0) ? 1 : span; + mTypes = lineType; + } + } + + private List mSelectionTags = new List(); + + /// + /// This is a place to save the file offset associated with the ListView's + /// TopItem, so we can position the list appropriately. + /// + private int mTopOffset; + + // Use Generate(). + private SavedSelection() { } + + /// + /// Creates a new SavedSelection object, generating a list of tags from the + /// lines that are currently selected. + /// + /// If nothing is selected, SavedSelection will have no members. + /// + /// Display list, with list of Lines. + /// Bit vector specifying which lines are selected. + /// New SavedSelection object. + public static SavedSelection Generate(DisplayListGen dl, VirtualListViewSelection sel, + int topOffset) { + SavedSelection savedSel = new SavedSelection(); + //Debug.Assert(topOffset >= 0); + savedSel.mTopOffset = topOffset; + + List lineList = dl.mLineList; + Debug.Assert(lineList.Count == sel.Length); + + // Generate tags, which are a combination of the offset, span, and a merge + // of types of all the lines associated with that offset. + // + // We may want to consider some sort of optimization for a "select all" + // operation, although there aren't many changes you can make after selecting + // all lines in a very large file. + Tag tag = null; + int curOffset = -1; + for (int i = 0; i < lineList.Count; i++) { + if (!sel[i]) { + continue; + } + Line line = lineList[i]; + // Code hinting can transform code to data and vice-versa, so we + // want the tag to reflect the fact that both could exist. + Line.Type lineType = line.LineType; + if (lineType == Line.Type.Code || lineType == Line.Type.Data) { + lineType = Line.Type.CodeOrData; + } + if (line.FileOffset != curOffset) { + // advanced to new offset, flush previous + if (tag != null) { + savedSel.mSelectionTags.Add(tag); + } + curOffset = line.FileOffset; + + tag = new Tag(line.FileOffset, line.OffsetSpan, lineType); + } else { + // another item at same offset + tag.mSpan = Math.Max(tag.mSpan, line.OffsetSpan); + tag.mTypes |= lineType; + } + } + if (curOffset == -1) { + // It's hard to cause an action that requires save/restore when you don't + // have anything selected in the ListView. However, this can happen if + // you do a sequence like: + // - Open a file that starts with a JMP followed by data. + // - Click on the blank line below the code, which has the code's offset, + // and select "remove hint". This causes the blank line to vanish, + // so the Restore() won't select anything. + // - Click "undo". + Debug.WriteLine("NOTE: no selection found"); + } else { + // Add the in-progress tag to the list. + savedSel.mSelectionTags.Add(tag); + } + + return savedSel; + } + + /// + /// Creates a selection set by identifying the set of lines in the display list + /// that correspond to items in the SavedSelection tag list. + /// + /// Display list, with list of Lines. + /// Set of selected lines. + public VirtualListViewSelection Restore(DisplayListGen dl, out int topIndex) { + List lineList = dl.mLineList; + VirtualListViewSelection sel = new VirtualListViewSelection(lineList.Count); + + topIndex = -1; + + // Walk through the tag list, which is ordered by ascending offset, and + // through the display list, which is similarly ordered. + int tagIndex = 0; + int lineIndex = 0; + while (tagIndex < mSelectionTags.Count && lineIndex < lineList.Count) { + Tag tag = mSelectionTags[tagIndex]; + int lineOffset = lineList[lineIndex].FileOffset; + + // If a line encompassing this offset was at the top of the ListView + // control before, use this line's index as the top. + if (topIndex < 0 && lineList[lineIndex].Contains(mTopOffset)) { + topIndex = lineIndex; + } + + if (lineOffset >= tag.mOffset && lineOffset < tag.mOffset + tag.mSpan) { + // Intersection. If the line type matches, add it to the set. + if ((tag.mTypes & lineList[lineIndex].LineType) != 0) { + sel[lineIndex] = true; + } + + // Advance to the next line entry. + lineIndex++; + } else if (tag.mOffset < lineOffset) { + // advance tag + tagIndex++; + } else { + Debug.Assert(tag.mOffset > lineOffset); + lineIndex++; + } + } + + // Continue search for topIndex, if necessary. + while (topIndex < 0 && lineIndex < lineList.Count) { + if (lineList[lineIndex].Contains(mTopOffset)) { + topIndex = lineIndex; + break; + } + lineIndex++; + } + Debug.WriteLine("TopOffset +" + mTopOffset.ToString("x6") + + " --> index " + topIndex); + if (topIndex < 0) { + // This can happen if you delete the header comment while scrolled + // to the top of the list. + topIndex = 0; + } + return sel; + } + + public void DebugDump() { + Debug.WriteLine("Selection (" + mSelectionTags.Count + " offsets):"); + foreach (Tag tag in mSelectionTags) { + Debug.WriteLine(" +" + tag.mOffset.ToString("x6") + "/" + + tag.mSpan + ": " + tag.mTypes); + } + } + } + + + + /// + /// Constructor. + /// + /// Project object. + /// Formatter object. + public DisplayListGen(DisasmProject proj, Formatter formatter, + PseudoOp.PseudoOpNames opNames) { + mProject = proj; + mFormatter = formatter; + mPseudoOpNames = opNames; + + mLineList = new List(); + mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, + false); + } + + /// + /// Changes the Formatter object. Clears the display list, instigating a full re-render. + /// + /// Formatter object. + public void SetFormatter(Formatter formatter) { + mFormatter = formatter; + mLineList.Clear(); + + // We probably just changed settings, so update this as well. + mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.SRCGEN_SHOW_CYCLE_COUNTS, + false); + } + + /// + /// Changes the pseudo-op name object. Clears the display list, instigating a + /// full re-render. + /// + /// Pseudo-op names. + public void SetPseudoOpNames(PseudoOp.PseudoOpNames opNames) { + mPseudoOpNames = opNames; + mLineList.Clear(); + } + + /// + /// Number of lines in the list. + /// + public int Count { get { return mLineList.Count; } } + + /// + /// Retrieves the Nth element. + /// + public Line this[int key] { + get { + return mLineList[key]; + } + } + + /// + /// Returns the Line's FormattedParts object, generating it first if necessary. + /// + /// Object with formatted strings. + public FormattedParts GetFormattedParts(int index) { + Line line = mLineList[index]; + if (line.Parts == null) { + FormattedParts parts; + switch (line.LineType) { + case Line.Type.Code: + parts = GenerateInstructionLine(mProject, mFormatter, + line.FileOffset, line.OffsetSpan, mShowCycleCounts); + break; + case Line.Type.Data: + parts = GenerateDataLine(mProject, mFormatter, mPseudoOpNames, + line.FileOffset, line.SubLineIndex); + break; + case Line.Type.Blank: + // Nothing to do. + parts = FormattedParts.CreateBlankLine(); + break; + case Line.Type.OrgDirective: + case Line.Type.RegWidthDirective: + case Line.Type.LongComment: + case Line.Type.Note: + // should have been done already + default: + Debug.Assert(false); + parts = FormattedParts.Create("x", "x", "x", "x", "x", "x", "x", "x", + "x", "x"); + break; + } + line.Parts = parts; + } + return line.Parts; + } + + /// + /// Returns a string with the concatenation of the searchable portions of the line. + /// Different sections are separated with an unlikely unicode character. The goal + /// is to have a single string per line that can be searched quickly, without having + /// adjacent fields spill into each other. + /// + /// Line index. + /// Formatted line contents. + public string GetSearchString(int index) { + Line line = mLineList[index]; + if (line.SearchString == null) { + const char sep = '\u203b'; // REFERENCE MARK + + FormattedParts parts = GetFormattedParts(index); + StringBuilder sb = new StringBuilder(); + // Some parts may be null, e.g. for long comments. Append() can deal. + sb.Append(parts.Label); + sb.Append(sep); + sb.Append(parts.Opcode); + sb.Append(sep); + sb.Append(parts.Operand); + sb.Append(sep); + sb.Append(parts.Comment); + line.SearchString = sb.ToString(); + } + return line.SearchString; + } + + /// + /// Finds the first line entry that encompasses the specified offset. + /// + /// Offset to search for. Negative values are allowed. + /// Line list index, or -1 if not found. + private static int FindLineByOffset(List lineList, int offset) { + if (lineList.Count == 0) { + return -1; + } + + int low = 0; + int high = lineList.Count - 1; + int mid = -1; + bool found = false; + while (low <= high) { + mid = (low + high) / 2; + Line line = lineList[mid]; + + if (line.Contains(offset)) { + // found a match + found = true; + break; + } else if (line.FileOffset > offset) { + // too big, move the high end in + high = mid - 1; + } else if (line.FileOffset < offset) { + // too small, move the low end in + low = mid + 1; + } else { + // WTF + throw new Exception("Bad binary search"); + } + } + + if (!found) { + return -1; + } + + // We found *a* matching line. Seek backward to find the *first* matching line. + while (mid > 0) { + Line upLine = lineList[mid - 1]; + if (upLine.Contains(offset)) { + mid--; + } else { + break; + } + } + + return mid; + } + + /// + /// Finds the first line entry that encompasses the specified offset. + /// + /// Offset to search for. + /// Line list index, or -1 if not found. + public int FindLineIndexByOffset(int offset) { + return FindLineByOffset(mLineList, offset); + } + + /// + /// Finds the code or data line entry that encompasses the specified offset. + /// + /// Offset to search for. + /// Line list index, or -1 if not found. + public int FindCodeDataIndexByOffset(int offset) { + if (offset < 0) { + // Header offset. No code or data here. + return -1; + } + int index = FindLineByOffset(mLineList, offset); + if (index < 0) { + return -1; + } + while (mLineList[index].LineType != Line.Type.Code && + mLineList[index].LineType != Line.Type.Data) { + index++; + } + return index; + } + + /// + /// Generates Lines for the entire project. + /// + public void GenerateAll() { + mLineList.Clear(); + GenerateHeaderLines(mProject, mFormatter, mPseudoOpNames, mLineList); + GenerateLineList(mProject, mFormatter, mPseudoOpNames, + 0, mProject.FileData.Length - 1, mLineList); + + Debug.Assert(ValidateLineList(), "Display list failed validation"); + } + + /// + /// Generates a list of Lines for the specified range of offsets, replacing + /// existing values. + /// + /// First offset. Must be the start of an instruction + /// or data area. + /// End offset (inclusive). + public void GenerateRange(int startOffset, int endOffset) { + if (startOffset < 0) { + ClearHeaderLines(); + GenerateHeaderLines(mProject, mFormatter, mPseudoOpNames, mLineList); + if (endOffset < 0) { + // nothing else to do + return; + } + // do the rest + startOffset = 0; + } + Debug.Assert(startOffset >= 0); + Debug.Assert(endOffset < mProject.FileData.Length); + Debug.Assert(endOffset >= startOffset); + //Debug.WriteLine("DL gen range [" + startOffset + "," + endOffset + "]"); + + // Find the start index. The start offset should always appear at the + // start of a Line because it comes from item selection. + int startIndex = FindLineByOffset(mLineList, startOffset); + if (startIndex < 0) { + Debug.Assert(false, "Unable to find startOffset " + startOffset); + GenerateAll(); + return; + } + // Find the end index. The end offset can be part of a multi-line data item, like + // a long string. Find the first Line that starts at an offset larger than endOffset. + int endIndex; + if (startOffset == endOffset) { + // Simple optimization for single-offset groups. + endIndex = startIndex; + } else { + endIndex = FindLineByOffset(mLineList, endOffset); + } + if (endIndex < 0) { + Debug.Assert(false, "Unable to find endOffset " + endOffset); + GenerateAll(); + return; + } + // There may be more than one line involved, so we need to scan forward. + for (endIndex++; endIndex < mLineList.Count; endIndex++) { + if (mLineList[endIndex].FileOffset > endOffset) { + endIndex--; + break; + } + } + if (endIndex == mLineList.Count) { + // whoops, loop ended before we had a chance to decrement + endIndex = mLineList.Count - 1; + } + Debug.WriteLine("GenerateRange: offset [+" + startOffset.ToString("x6") + ",+" + + endOffset.ToString("x6") + + "] maps to index [" + startIndex + "," + endIndex + "]"); + Debug.Assert(endIndex >= startIndex); + + // Create temporary list to hold new lines. Set the initial capacity to + // the previous size, on the assumption that it won't change much. + List newLines = new List(endIndex - startIndex + 1); + GenerateLineList(mProject, mFormatter, mPseudoOpNames, startOffset, endOffset, newLines); + + // Out with the old, in with the new. + mLineList.RemoveRange(startIndex, endIndex - startIndex + 1); + mLineList.InsertRange(startIndex, newLines); + + Debug.Assert(ValidateLineList(), "Display list failed validation"); + } + + /// + /// Validates the line list, confirming that every offset is represented exactly once. + /// + /// True if all is well. + private bool ValidateLineList() { + int expectedOffset = 0; + int lastOffset = Int32.MinValue; + foreach (Line line in mLineList) { + // Header lines aren't guaranteed to be sequential and don't have a span. + // They are expected to be in sorted order, and to be unique (with the + // notable exception of the header comment, which is multi-line). + if (line.FileOffset < 0) { + if (line.FileOffset < lastOffset || (line.LineType != Line.Type.LongComment && + line.FileOffset == lastOffset)) { + Debug.WriteLine("Header offsets went backward: cur=" + + line.FileOffset + " last=" + lastOffset); + return false; + } + lastOffset = line.FileOffset; + continue; + } + + // Blank lines and comments can appear before or after code/data. They + // must have the offset of the associated line, and a span of zero. + if (line.FileOffset != expectedOffset && line.FileOffset != lastOffset) { + Debug.WriteLine("ValidateLineList: bad offset " + line.FileOffset + + " (last=" + lastOffset + ", expected next=" + expectedOffset + ")"); + return false; + } + + if (line.SubLineIndex != 0) { + // In the middle of a multi-line thing, don't advance last/expected. + Debug.Assert(line.FileOffset == lastOffset); + } else { + lastOffset = expectedOffset; + expectedOffset += line.OffsetSpan; + } + } + + if (expectedOffset != mProject.FileData.Length) { + Debug.WriteLine("ValidateLineList: did not cover entire file: last offset " + + expectedOffset + ", file has " + mProject.FileData.Length); + return false; + } + + return true; + } + + /// + /// Removes all header lines from the display list. + /// + private void ClearHeaderLines() { + // Find the first non-header item. + int endIndex = FindLineByOffset(mLineList, 0); + if (endIndex == 0) { + // no header lines present + Debug.WriteLine("No header lines found"); + return; + } + Debug.WriteLine("Removing " + endIndex + " header lines"); + mLineList.RemoveRange(0, endIndex); + } + + /// + /// Generates a synthetic offset for the FileOffset field from an index value. The + /// index arg is the index of an entry in the DisasmProject.ActiveDefSymbolList. + /// (The exact algorithm isn't too important, as these offsets are not stored in the + /// project file.) + /// + private static int DefSymOffsetFromIndex(int index) { + Debug.Assert(index >= 0 && index < (1 << 24)); + return index - (1 << 24); + } + + /// + /// Returns the DisasmProject.ActiveDefSymbolList index for an EQU line with + /// the specified file offset. + /// + public static int DefSymIndexFromOffset(int offset) { + Debug.Assert(offset < 0); + return offset + (1 << 24); + } + + /// + /// Generates the header lines (header comment, EQU directives), and inserts them at + /// the top of the list. + /// + /// This does not currently do incremental generation. Call ClearHeaderLines() before + /// calling here if you're not starting with an empty list. + /// + /// Project reference. + /// Output formatter. + /// Pseudo-op names. + /// List to add output lines to. + private static void GenerateHeaderLines(DisasmProject proj, Formatter formatter, + PseudoOp.PseudoOpNames opNames, List fullLines) { + List tmpLines = new List(); + Line line; + FormattedParts parts; + + // Check for header comment. + if (proj.LongComments.TryGetValue(Line.HEADER_COMMENT_OFFSET, + out MultiLineComment headerComment)) { + List formatted = headerComment.FormatText(formatter, string.Empty); + StringListToLines(formatted, Line.HEADER_COMMENT_OFFSET, Line.Type.LongComment, + Color.FromArgb(0, 0, 0, 0), tmpLines); + } + + // Format symbols. + int index = 0; + foreach (DefSymbol defSym in proj.ActiveDefSymbolList) { + line = new Line(DefSymOffsetFromIndex(index), 0, Line.Type.EquDirective); + // Use an operand length of 1 so things are shown as concisely as possible. + string valueStr = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + null, defSym.DataDescriptor, defSym.Value, 1, + PseudoOp.FormatNumericOpFlags.None); + string comment = formatter.FormatEolComment(defSym.Comment); + parts = FormattedParts.CreateEquDirective(defSym.Label, + formatter.FormatPseudoOp(opNames.EquDirective), + valueStr, comment); + line.Parts = parts; + tmpLines.Add(line); + index++; + } + + if (proj.ActiveDefSymbolList.Count != 0) { + // We had some EQUs, throw a blank line at the end. + index++; + line = new Line(DefSymOffsetFromIndex(index), 0, Line.Type.Blank); + tmpLines.Add(line); + } + + fullLines.InsertRange(0, tmpLines); + } + + /// + /// Generates lines for the specified range of file offsets. + /// + /// Does not generate formatted parts in most cases; that usually happens on demand. + /// Complicated items, such as word-wrapped long comments, may be generated now + /// and saved off. + /// + /// This still needs a formatter arg even when no text is rendered because some + /// options, like maximum per-line operand length, might affect how many lines + /// are generated. + /// + /// Project reference. + /// Output formatter. + /// Offset of first byte. + /// Offset of last byte. + /// List to add output lines to. + private static void GenerateLineList(DisasmProject proj, Formatter formatter, + PseudoOp.PseudoOpNames opNames, int startOffset, int endOffset, List lines) { + //Debug.WriteLine("GenerateRange [+" + startOffset.ToString("x6") + ",+" + + // endOffset.ToString("x6") + "]"); + + Debug.Assert(startOffset >= 0); + Debug.Assert(endOffset >= startOffset); + + // Find the previous status flags for M/X tracking. + StatusFlags prevFlags = StatusFlags.AllIndeterminate; + if (proj.CpuDef.HasEmuFlag) { + for (int scanoff = startOffset - 1; scanoff >= 0; scanoff--) { + Anattrib attr = proj.GetAnattrib(scanoff); + if (attr.IsInstructionStart) { + prevFlags = attr.StatusFlags; + // Apply the same tweak here that we do to curFlags below. + prevFlags.M = attr.StatusFlags.ShortM ? 1 : 0; + prevFlags.X = attr.StatusFlags.ShortX ? 1 : 0; + Debug.WriteLine("GenerateLineList startOff=+" + + startOffset.ToString("x6") + " using initial flags from +" + + scanoff.ToString("x6") + ": " + prevFlags); + break; + } + } + } + + // Configure the initial value of addBlank. The specific case we're handling is + // a no-continue instruction (e.g. JMP) followed by an instruction with a label. + // When we rename the label, we don't want the blank to disappear during the + // partial-list generation. + bool addBlank = false; + if (startOffset > 0) { + int baseOff = DataAnalysis.GetBaseOperandOffset(proj, startOffset - 1); + if (proj.GetAnattrib(baseOff).DoesNotContinue) { + addBlank = true; + } + } + + int offset = startOffset; + while (offset <= endOffset) { + Anattrib attr = proj.GetAnattrib(offset); + if (attr.IsInstructionStart && offset > 0 && + proj.GetAnattrib(offset - 1).IsData) { + // Transition from data to code. (Don't add blank line for inline data.) + lines.Add(GenerateBlankLine(offset)); + } else if (addBlank) { + // Previous instruction wanted to be followed by a blank line. + lines.Add(GenerateBlankLine(offset)); + } + addBlank = false; + + // Insert long comments and notes. These may span multiple display lines, + // and require word-wrap, so it's easiest just to render them fully here. + if (proj.Notes.TryGetValue(offset, out MultiLineComment noteData)) { + List formatted = noteData.FormatText(formatter, "NOTE: "); + StringListToLines(formatted, offset, Line.Type.Note, + noteData.BackgroundColor, lines); + } + if (proj.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { + List formatted = longComment.FormatText(formatter, string.Empty); + StringListToLines(formatted, offset, Line.Type.LongComment, + longComment.BackgroundColor, lines); + } + + if (attr.IsInstructionStart) { + // Generate reg width directive, if necessary. + if (proj.CpuDef.HasEmuFlag) { + // Changing from "ambiguous but assumed short" to "definitively short" + // merits a directive, notably at the start of the file. The tricky + // part is that E=1 means definitively M=1 X=1. And maybe + // indeterminate E also means that. + // + // We don't want to mess with Anattrib, but we do need to tell the + // assembler something. So we tweak our local copy and propagate it. + string operandStr = string.Empty; + StatusFlags curFlags = attr.StatusFlags; + curFlags.M = attr.StatusFlags.ShortM ? 1 : 0; + curFlags.X = attr.StatusFlags.ShortX ? 1 : 0; + if (curFlags.M != prevFlags.M) { + operandStr = (curFlags.M == 0) ? "longm" : "shortm"; + } + + if (curFlags.X != prevFlags.X) { + if (operandStr.Length > 0) { + operandStr += ","; + } + operandStr += (curFlags.X == 0) ? "longx" : "shortx"; + } + + if (operandStr.Length > 0) { + Line rwLine = new Line(offset, 0, Line.Type.RegWidthDirective); + // FormatPseudoOp isn't quite right for the operand, but there + // isn't anything more suitable, and there are only eight + // possible values. Having the operand capitalization match the + // pseudo-op's feels reasonable. + rwLine.Parts = FormattedParts.CreateDirective( + formatter.FormatPseudoOp(opNames.RegWidthDirective), + formatter.FormatPseudoOp(operandStr)); + lines.Add(rwLine); + } + prevFlags = curFlags; + } + + // Look for embedded instructions. + int len; + for (len = 1; len < attr.Length; len++) { + if (proj.GetAnattrib(offset + len).IsInstructionStart) { + break; + } + } + + // Create Line entry. Offset span only covers the instruction up to + // the point where the embedded instruction starts. + Line line = new Line(offset, len, Line.Type.Code); + lines.Add(line); + + // Insert blank after an instruction that doesn't continue. Provides a + // break in code, and before a data area. + // TODO(maybe): Might also want to do this if the next offset is data, + // to make things look nicer when code runs directly into data. + // + // We don't want to add it with the current line's offset. If we do that, + // the binary search will get confused, because blank lines have a span + // of zero. If the code is at offset 10 with length 3, and we search for + // the byte at offset 11, then a blank line (with span=0) at offset 10 will + // cause the binary search to assume that the target is farther down, when + // it's actually one line up. We deal with this by setting a flag and + // generating the blank line on the next trip through the loop. + if (attr.DoesNotContinue) { + addBlank = true; + } + + offset += len; + } else { + Debug.Assert(attr.DataDescriptor != null); + int numLines = + PseudoOp.ComputeRequiredLineCount(formatter, attr.DataDescriptor); + for (int i = 0; i < numLines; i++) { + Line line = new Line(offset, attr.Length, Line.Type.Data, i); + lines.Add(line); + } + offset += attr.Length; + } + } + + // See if there were any address shifts in this section. If so, insert an ORG + // statement as the first entry for the offset. We're expecting to have very + // few AddressMap entries (usually just one), so it's more efficient to process + // them here and walk through the sub-list than it is to ping the address map + // at every line. + // + // It should not be possible for an address map change to appear in the middle + // of an instruction or data item. + foreach (AddressMap.AddressMapEntry ent in proj.AddrMap) { + if (ent.Offset < startOffset || ent.Offset > endOffset) { + continue; + } + int index = FindLineByOffset(lines, ent.Offset); + if (index < 0) { + Debug.WriteLine("Couldn't find offset " + ent.Offset + + " in range we just generated"); + Debug.Assert(false); + continue; + } + if (lines[index].LineType == Line.Type.Blank) { + index++; + } + Line topLine = lines[index]; + Line newLine = new Line(topLine.FileOffset, 0, Line.Type.OrgDirective); + string addrStr = formatter.FormatHexValue(ent.Addr, 4); + newLine.Parts = FormattedParts.CreateDirective( + formatter.FormatPseudoOp(opNames.OrgDirective), addrStr); + lines.Insert(index, newLine); + + // Prepend a blank line if the previous line wasn't already blank, and this + // isn't the ORG at the start of the file. (This may temporarily do + // double-spacing if we do a partial update, because we won't be able to + // "see" the previous line. Harmless.) + if (ent.Offset != 0 && index > 0 && lines[index-1].LineType != Line.Type.Blank) { + Line blankLine = new Line(topLine.FileOffset, 0, Line.Type.Blank); + lines.Insert(index, blankLine); + } + } + } + + /// + /// Generates a blank line entry. + /// + private static Line GenerateBlankLine(int offset) { + return new Line(offset, 0, Line.Type.Blank); + } + + /// + /// Takes a list of strings and adds them to the Line list as long comments. + /// + /// + /// + /// + /// + /// + private static void StringListToLines(List list, int offset, Line.Type lineType, + Color color, List lines) { + foreach (string str in list) { + Line line = new Line(offset, 0, lineType); + FormattedParts parts = FormattedParts.CreateLongComment(str); + line.Parts = parts; + line.BackgroundColor = color; + lines.Add(line); + } + } + + private static FormattedParts GenerateInstructionLine(DisasmProject proj, + Formatter formatter, int offset, int instrBytes, bool showCycleCounts) { + Anattrib attr = proj.GetAnattrib(offset); + byte[] data = proj.FileData; + + string offsetStr = formatter.FormatOffset24(offset); + + int addr = attr.Address; + string addrStr = formatter.FormatAddress(addr, !proj.CpuDef.HasAddr16); + string bytesStr = formatter.FormatBytes(data, offset, instrBytes); + string flagsStr = attr.StatusFlags.ToString(proj.CpuDef.HasEmuFlag); + string attrStr = attr.ToAttrString(); + + string labelStr = string.Empty; + if (attr.Symbol != null) { + labelStr = attr.Symbol.Label; + } + + OpDef op = proj.CpuDef.GetOpDef(data[offset]); + int operand = op.GetOperand(data, offset, attr.StatusFlags); + int instrLen = op.GetLength(attr.StatusFlags); + OpDef.WidthDisambiguation wdis = OpDef.WidthDisambiguation.None; + if (op.IsWidthPotentiallyAmbiguous) { + wdis = OpDef.GetWidthDisambiguation(instrLen, operand); + } + + string opcodeStr = formatter.FormatOpcode(op, wdis); + if (attr.Length != instrBytes) { + // An instruction is embedded inside this one. Note that BRK is a two-byte + // instruction, so don't freak out if you see it marked as embedded when a + // $00 is followed by actual code. (But be a little freaked out that your + // code is running into a BRK.) + //opcodeStr = opcodeStr + " \u00bb"; // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + //opcodeStr = opcodeStr + " \u23e9"; // BLACK RIGHT-POINTING DOUBLE TRIANGLE + opcodeStr = opcodeStr + " \u25bc"; // BLACK DOWN-POINTING TRIANGLE + } + + string formattedOperand = null; + int operandLen = instrLen - 1; + PseudoOp.FormatNumericOpFlags opFlags = PseudoOp.FormatNumericOpFlags.None; + + // Tweak branch instructions. We want to show the absolute address rather + // than the relative offset (which happens with the OperandAddress assignment + // below), and 1-byte branches should always appear as a 4-byte hex value. + if (op.AddrMode == OpDef.AddressMode.PCRel) { + Debug.Assert(attr.OperandAddress >= 0); + operandLen = 2; + opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; + } else if (op.AddrMode == OpDef.AddressMode.PCRelLong || + op.AddrMode == OpDef.AddressMode.StackPCRelLong) { + opFlags = PseudoOp.FormatNumericOpFlags.IsPcRel; + } else if (op.AddrMode == OpDef.AddressMode.Imm || + op.AddrMode == OpDef.AddressMode.ImmLongA || + op.AddrMode == OpDef.AddressMode.ImmLongXY) { + opFlags = PseudoOp.FormatNumericOpFlags.HasHashPrefix; + } + + // Use the OperandAddress when available. This is important for relative branch + // instructions and PER, where we want to show the target address rather than the + // operand value. + int operandForSymbol = operand; + if (attr.OperandAddress >= 0) { + operandForSymbol = attr.OperandAddress; + } + + // Check Length to watch for bogus descriptors. ApplyFormatDescriptors() should + // have discarded anything appropriate, so we might be able to eliminate this test. + if (attr.DataDescriptor != null && attr.Length == attr.DataDescriptor.Length) { + Debug.Assert(operandLen > 0); + + // Format operand as directed. + if (op.AddrMode == OpDef.AddressMode.BlockMove) { + // Special handling for the double-operand block move. + string opstr1 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + null, attr.DataDescriptor, operand >> 8, 1, + PseudoOp.FormatNumericOpFlags.None); + string opstr2 = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + null, attr.DataDescriptor, operand & 0xff, 1, + PseudoOp.FormatNumericOpFlags.None); + formattedOperand = opstr1 + "," + opstr2; + } else { + formattedOperand = PseudoOp.FormatNumericOperand(formatter, proj.SymbolTable, + null, attr.DataDescriptor, operandForSymbol, operandLen, opFlags); + } + } else { + // Show operand value in hex. + if (op.AddrMode == OpDef.AddressMode.BlockMove) { + formattedOperand = formatter.FormatHexValue(operand >> 8, 2) + "," + + formatter.FormatHexValue(operand & 0xff, 2); + } else { + if (operandLen == 2) { + // This is necessary for 16-bit operands, like "LDA abs" and "PEA val", + // when outside bank zero. The bank is included in the operand address, + // but we don't want to show it here. + operandForSymbol &= 0xffff; + } + formattedOperand = formatter.FormatHexValue(operandForSymbol, operandLen * 2); + } + } + string operandStr = formatter.FormatOperand(op, formattedOperand, wdis); + + string eolComment = proj.Comments[offset]; + if (showCycleCounts) { + bool branchCross = (attr.Address & 0xff00) != (operandForSymbol & 0xff00); + int cycles = proj.CpuDef.GetCycles(op.Opcode, attr.StatusFlags, attr.BranchTaken, + branchCross); + if (cycles > 0) { + eolComment = cycles.ToString() + " " + eolComment; + } else { + eolComment = (-cycles).ToString() + "+ " + eolComment; + } + } + string commentStr = formatter.FormatEolComment(eolComment); + + string debugStr = string.Empty; + //debugStr = "opOff=" + + // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); + + FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, + flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); + return parts; + } + + private static FormattedParts GenerateDataLine(DisasmProject proj, Formatter formatter, + PseudoOp.PseudoOpNames opNames, int offset, int subLineIndex) { + Anattrib attr = proj.GetAnattrib(offset); + byte[] data = proj.FileData; + + string offsetStr, addrStr, bytesStr, flagsStr, attrStr, labelStr, opcodeStr, + operandStr, commentStr, debugStr; + offsetStr = addrStr = bytesStr = flagsStr = attrStr = labelStr = opcodeStr = + operandStr = commentStr = debugStr = string.Empty; + + PseudoOp.PseudoOut pout = PseudoOp.FormatDataOp(formatter, opNames, proj.SymbolTable, + null, attr.DataDescriptor, proj.FileData, offset, subLineIndex); + if (subLineIndex == 0) { + offsetStr = formatter.FormatOffset24(offset); + + addrStr = formatter.FormatAddress(attr.Address, !proj.CpuDef.HasAddr16); + if (attr.Symbol != null) { + labelStr = attr.Symbol.Label; + } + + bytesStr = formatter.FormatBytes(data, offset, attr.Length); + attrStr = attr.ToAttrString(); + + opcodeStr = formatter.FormatPseudoOp(pout.Opcode); + } else { + opcodeStr = " +"; + } + + operandStr = pout.Operand; + + if (subLineIndex == 0) { + commentStr = formatter.FormatEolComment(proj.Comments[offset]); + + //debugStr = "opOff=" + + // (attr.OperandOffset < 0 ? "-" : "+" + attr.OperandOffset.ToString("x6")); + } + + FormattedParts parts = FormattedParts.Create(offsetStr, addrStr, bytesStr, + flagsStr, attrStr, labelStr, opcodeStr, operandStr, commentStr, debugStr); + return parts; + } + } +} diff --git a/SourceGenWPF/MainController.cs b/SourceGenWPF/MainController.cs index e50b90c..b57edd4 100644 --- a/SourceGenWPF/MainController.cs +++ b/SourceGenWPF/MainController.cs @@ -65,7 +65,7 @@ namespace SourceGenWPF { /// /// Data backing the codeListView. /// - private DisplayList mDisplayList; + private DisplayListGen mDisplayList; #endregion Project state @@ -519,7 +519,7 @@ namespace SourceGenWPF { proj.Initialize(fileData.Length); proj.PrepForNew(fileData, Path.GetFileName(dataPathName)); - proj.LongComments.Add(DisplayList.Line.HEADER_COMMENT_OFFSET, + proj.LongComments.Add(DisplayListGen.Line.HEADER_COMMENT_OFFSET, new MultiLineComment("6502bench SourceGen v" + App.ProgramVersion)); // The system definition provides a set of defaults that can be overridden. @@ -540,7 +540,7 @@ namespace SourceGenWPF { dlg.ShowDialog(); } - mDisplayList = new DisplayList(mProject, mOutputFormatter, mPseudoOpNames); + mDisplayList = new DisplayListGen(mProject, mOutputFormatter, mPseudoOpNames); // Prep the symbol table subset object. Replace the old one with a new one. //mSymbolSubset = new SymbolTableSubset(mProject.SymbolTable); @@ -625,7 +625,7 @@ namespace SourceGenWPF { int topItem = 0; #endif int topOffset = mDisplayList[topItem].FileOffset; - DisplayList.SavedSelection savedSel = DisplayList.SavedSelection.Generate( + DisplayListGen.SavedSelection savedSel = DisplayListGen.SavedSelection.Generate( mDisplayList, mCodeViewSelection, topOffset); //savedSel.DebugDump(); mReanalysisTimer.EndTask("Save selection"); diff --git a/SourceGenWPF/ProjWin/MainWindow.xaml b/SourceGenWPF/ProjWin/MainWindow.xaml index 13cc2f8..f192b4a 100644 --- a/SourceGenWPF/ProjWin/MainWindow.xaml +++ b/SourceGenWPF/ProjWin/MainWindow.xaml @@ -220,15 +220,15 @@ limitations under the License. Visibility="{Binding Path=CodeListVisibility}"> - - - - - - - - - + + + + + + + + + diff --git a/SourceGenWPF/ProjWin/MainWindow.xaml.cs b/SourceGenWPF/ProjWin/MainWindow.xaml.cs index 9994d84..81ec6df 100644 --- a/SourceGenWPF/ProjWin/MainWindow.xaml.cs +++ b/SourceGenWPF/ProjWin/MainWindow.xaml.cs @@ -45,6 +45,8 @@ namespace SourceGenWPF.ProjWin { this.DataContext = this; mUI = new MainController(this); + + codeListView.ItemsSource = new DisplayList(500); } diff --git a/SourceGenWPF/ProjectFile.cs b/SourceGenWPF/ProjectFile.cs index 5574063..2f2ecf5 100644 --- a/SourceGenWPF/ProjectFile.cs +++ b/SourceGenWPF/ProjectFile.cs @@ -721,7 +721,7 @@ namespace SourceGenWPF { // Shouldn't allow DisplayList.Line.HEADER_COMMENT_OFFSET on anything but // LongComment. Maybe "bool allowNegativeKeys"? if (intKey < fileLen && - (intKey >= 0 || intKey == DisplayList.Line.HEADER_COMMENT_OFFSET)) { + (intKey >= 0 || intKey == DisplayListGen.Line.HEADER_COMMENT_OFFSET)) { return true; } else { report.Add(FileLoadItem.Type.Warning, diff --git a/SourceGenWPF/SourceGenWPF.csproj b/SourceGenWPF/SourceGenWPF.csproj index e9bc7a4..bc70778 100644 --- a/SourceGenWPF/SourceGenWPF.csproj +++ b/SourceGenWPF/SourceGenWPF.csproj @@ -62,6 +62,7 @@ App.xaml Code + DataFileLoadIssue.xaml @@ -96,7 +97,7 @@ - +