mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-03-22 01:44:16 -05:00
598 lines
28 KiB
C#
598 lines
28 KiB
C#
using HavenSoft.HexManiac.Core.Models;
|
|
using HavenSoft.HexManiac.Core.Models.Code;
|
|
using HavenSoft.HexManiac.Core.Models.Runs;
|
|
using HavenSoft.HexManiac.Core.ViewModels.Map;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Windows.Input;
|
|
|
|
namespace HavenSoft.HexManiac.Core.ViewModels.Tools {
|
|
public enum CodeMode { Thumb, Script, BattleScript, AnimationScript, TrainerAiScript, Raw }
|
|
|
|
public class CodeTool : ViewModelCore, IToolViewModel {
|
|
public string Name => "Code Tool";
|
|
|
|
private CodeMode mode;
|
|
private readonly Singletons singletons;
|
|
private readonly ThumbParser thumb;
|
|
private readonly ScriptParser script, battleScript, animationScript, battleAIScript;
|
|
private readonly ViewPort viewPort;
|
|
private readonly IDataModel model;
|
|
private readonly Selection selection;
|
|
private readonly ChangeHistory<ModelDelta> history;
|
|
private readonly IRaiseMessageTab messageTab;
|
|
private readonly IDelayWorkTimer recompileTimer;
|
|
|
|
public event EventHandler<ErrorInfo> ModelDataChanged;
|
|
public event EventHandler AttentionNewContent;
|
|
|
|
public bool IsReadOnly => Mode == CodeMode.Raw;
|
|
public bool UseSingleContent => !UseMultiContent;
|
|
public bool UseMultiContent => Mode.IsAny(CodeMode.Script, CodeMode.BattleScript, CodeMode.AnimationScript, CodeMode.TrainerAiScript);
|
|
|
|
public IDataInvestigator Investigator { get; set; }
|
|
|
|
private bool isSelected;
|
|
public bool IsSelected { get => isSelected; set => Set(ref isSelected, value, old => UpdateContent()); }
|
|
|
|
private bool insertAutoActive = true;
|
|
public bool InsertAutoActive { get => insertAutoActive; set => Set(ref insertAutoActive, value); }
|
|
|
|
private bool showErrorText;
|
|
public bool ShowErrorText { get => showErrorText; private set => TryUpdate(ref showErrorText, value); }
|
|
|
|
private string errorText;
|
|
public string ErrorText { get => errorText; private set => TryUpdate(ref errorText, value); }
|
|
|
|
private int fontSize = 12;
|
|
public int FontSize { get => fontSize; set => TryUpdate(ref fontSize, value); }
|
|
|
|
public CodeMode Mode {
|
|
get => mode;
|
|
set {
|
|
if (!TryUpdateEnum(ref mode, value)) return;
|
|
UpdateContent();
|
|
NotifyPropertyChanged(nameof(IsReadOnly));
|
|
NotifyPropertyChanged(nameof(UseSingleContent));
|
|
NotifyPropertyChanged(nameof(UseMultiContent));
|
|
}
|
|
}
|
|
|
|
public TextEditorViewModel Editor { get; } = new();
|
|
|
|
public string Content {
|
|
get => Editor.Content;
|
|
set {
|
|
if (ignoreContentUpdates) return;
|
|
if (Editor.Content != value) Editor.Content = value;
|
|
else CompileChanges();
|
|
}
|
|
}
|
|
|
|
public ObservableCollection<CodeBody> Contents { get; } = new ObservableCollection<CodeBody>();
|
|
|
|
public ThumbParser Parser => thumb;
|
|
|
|
public ScriptParser ScriptParser => script;
|
|
|
|
public ScriptParser BattleScriptParser => battleScript;
|
|
|
|
public ScriptParser AnimationScriptParser => animationScript;
|
|
|
|
public ScriptParser BattleAIScriptParser => battleAIScript;
|
|
|
|
public event EventHandler<(int originalLocation, int newLocation)> ModelDataMoved;
|
|
|
|
private StubCommand isEventScript;
|
|
public ICommand IsEventScript => StubCommand(ref isEventScript, ExecuteIsEventScript);
|
|
private void ExecuteIsEventScript() {
|
|
var searchPoint = selection.Scroll.ViewPointToDataIndex(selection.SelectionStart);
|
|
ScriptParser.FormatScript<XSERun>(history.CurrentChange, model, searchPoint);
|
|
}
|
|
|
|
// properties that exist solely so the UI can remember things when the tab switches
|
|
public double SingleBoxVerticalOffset { get; set; }
|
|
public double MultiBoxVerticalOffset { get; set; }
|
|
|
|
public CodeTool(Singletons singletons, ViewPort viewPort, Selection selection, ChangeHistory<ModelDelta> history, IRaiseMessageTab messageTab) {
|
|
this.singletons = singletons;
|
|
recompileTimer = singletons.WorkDispatcher.CreateDelayTimer();
|
|
var gameHash = viewPort.Model.GetShortGameCode();
|
|
thumb = new ThumbParser(singletons);
|
|
script = new ScriptParser(gameHash, singletons.ScriptLines, 0x02);
|
|
battleScript = new ScriptParser(gameHash, singletons.BattleScriptLines, 0x3D);
|
|
animationScript = new ScriptParser(gameHash, singletons.AnimationScriptLines, 0x08);
|
|
battleAIScript = new ScriptParser(gameHash, singletons.BattleAIScriptLines, 0x5A);
|
|
this.viewPort = viewPort;
|
|
this.model = viewPort.Model;
|
|
this.selection = selection;
|
|
this.history = history;
|
|
this.messageTab = messageTab;
|
|
selection.PropertyChanged += (sender, e) => {
|
|
if (e.PropertyName == nameof(selection.SelectionStart)) {
|
|
if (selection.SelectionStart == selection.SelectionEnd) UpdateContent();
|
|
} else if (e.PropertyName == nameof(selection.SelectionEnd)) {
|
|
UpdateContent();
|
|
}
|
|
};
|
|
|
|
SetupThumbKeywords(singletons);
|
|
Editor.Bind(nameof(Editor.Content), (sender, e) => {
|
|
if (ignoreContentUpdates) return;
|
|
NotifyPropertyChanged(nameof(Content));
|
|
CompileChanges();
|
|
});
|
|
}
|
|
|
|
private void SetupThumbKeywords(Singletons singletons) {
|
|
Editor.LineCommentHeader = "@";
|
|
Editor.MultiLineCommentHeader = "/*";
|
|
Editor.MultiLineCommentFooter = "*/";
|
|
|
|
Editor.Keywords.Clear();
|
|
var set = new HashSet<string>();
|
|
foreach (var template in singletons.ThumbInstructionTemplates) {
|
|
if (template is Instruction instr) {
|
|
set.Add(instr.Operator);
|
|
}
|
|
}
|
|
set.AddRange(new[] { ".word", ".byte", ".hword", ".align" });
|
|
set.AddRange("beq bne bhs blo bcs bcc bmi bpl bvs bvc bhi bls bge blt bgt ble bal bnv".Split(' '));
|
|
Editor.Keywords.AddRange(set);
|
|
|
|
Editor.Constants.Clear();
|
|
for (int i = 0; i <= 15; i++) Editor.Constants.Add($"r{i}");
|
|
Editor.Constants.AddRange(new[] { "lr", "sp", "pc" });
|
|
}
|
|
|
|
public void DataForCurrentRunChanged() => UpdateContent();
|
|
|
|
public void UpdateContent() {
|
|
if (ignoreContentUpdates || !isSelected) return;
|
|
var start = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionStart));
|
|
var end = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd));
|
|
|
|
if (start > end) (start, end) = (end, start);
|
|
int length = end - start + 1;
|
|
|
|
using (Scope(ref ignoreContentUpdates, true, old => ignoreContentUpdates = old)) {
|
|
if (length > 0x1000) {
|
|
Editor.Content = "Too many bytes selected.";
|
|
NotifyPropertyChanged(nameof(Content));
|
|
} else if (mode == CodeMode.Raw) {
|
|
Editor.Content = RawParse(model, start, end - start + 1);
|
|
NotifyPropertyChanged(nameof(Content));
|
|
} else if (length < 2 && mode == CodeMode.Thumb) {
|
|
Editor.Content = string.Empty;
|
|
NotifyPropertyChanged(nameof(Content));
|
|
UpdateContents(-1, null);
|
|
CanRepointThumb = CalculateCanRepointThumb();
|
|
} else if (mode == CodeMode.Script) {
|
|
UpdateContents(start, script);
|
|
} else if (mode == CodeMode.BattleScript) {
|
|
UpdateContents(start, battleScript);
|
|
} else if (mode == CodeMode.AnimationScript) {
|
|
UpdateContents(start, animationScript);
|
|
} else if (mode == CodeMode.TrainerAiScript) {
|
|
UpdateContents(start, battleAIScript);
|
|
} else if (mode == CodeMode.Thumb) {
|
|
Editor.Content = thumb.Parse(model, start, end - start + 1);
|
|
NotifyPropertyChanged(nameof(Content));
|
|
CanRepointThumb = CalculateCanRepointThumb();
|
|
} else {
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearConstantCache() {
|
|
script.ClearConstantCache();
|
|
battleScript.ClearConstantCache();
|
|
animationScript.ClearConstantCache();
|
|
battleAIScript.ClearConstantCache();
|
|
}
|
|
|
|
#region RepointThumb
|
|
|
|
private bool CalculateCanRepointThumb() {
|
|
int left = selection.Scroll.ViewPointToDataIndex(selection.SelectionStart);
|
|
int right = selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd);
|
|
if (left > right) (left, right) = (right, left);
|
|
var length = right - left + 1;
|
|
return Parser.CanRepoint(model, left, length) != -1;
|
|
}
|
|
|
|
private bool canRepointThumb;
|
|
public bool CanRepointThumb { get => canRepointThumb; private set => Set(ref canRepointThumb, value); }
|
|
|
|
public void RepointThumb() {
|
|
int left = selection.Scroll.ViewPointToDataIndex(selection.SelectionStart);
|
|
int right = selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd);
|
|
if (left > right) (left, right) = (right, left);
|
|
int register = Parser.CanRepoint(model, left, right - left + 1);
|
|
if (register != -1) {
|
|
var newAddress = Parser.Repoint(history.CurrentChange, model, left, register);
|
|
messageTab.RaiseMessage($"Thumb code repointed to {newAddress:X6}");
|
|
selection.Goto.Execute(newAddress);
|
|
selection.SelectionEnd = selection.Scroll.DataIndexToViewPoint(newAddress + 0x13);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// Update all the content objects.
|
|
/// If one of the content objects is the one being changed, don't update that one.
|
|
/// </summary>
|
|
/// <param name="start"></param>
|
|
/// <param name="currentScriptStart"></param>
|
|
private void UpdateContents(int start, ScriptParser parser, int currentScriptStart = -1, int currentScriptLength = -1) {
|
|
if (currentScriptStart == -1) {
|
|
ShowErrorText = false;
|
|
ErrorText = string.Empty;
|
|
}
|
|
|
|
var scripts = parser?.CollectScripts(model, start) ?? new List<int>();
|
|
int skippedScripts = 0;
|
|
int existingSectionCount = 0;
|
|
for (int i = 0; i < scripts.Count; i++) {
|
|
var scriptStart = scripts[i];
|
|
if (scriptStart == currentScriptStart && Contents.Count > i && Contents[i].Address == scriptStart) {
|
|
model.CurrentCacheScope.GetScriptInfo(parser, scriptStart, null, ref existingSectionCount); // mostly to update existingSectionCount
|
|
continue;
|
|
}
|
|
if (currentScriptStart < scriptStart && scriptStart < currentScriptStart + currentScriptLength) {
|
|
// this script is included inside the current under-edit script
|
|
// it doesn't need its own content
|
|
skippedScripts += 1;
|
|
continue;
|
|
}
|
|
|
|
var label = scriptStart.ToString("X6");
|
|
CodeBody body;
|
|
if (Contents.Count > i && Contents[i].Parser == parser) {
|
|
body = Contents[i];
|
|
} else {
|
|
body = new CodeBody(model, parser, Investigator) { Address = scriptStart, Label = label };
|
|
parser.AddKeywords(model, body);
|
|
}
|
|
|
|
var info = model.CurrentCacheScope.GetScriptInfo(parser, scriptStart, body, ref existingSectionCount);
|
|
bool needsAnimation = false;
|
|
|
|
if (Contents.Count > i) {
|
|
body.ContentChanged -= ScriptChanged;
|
|
body.HelpSourceChanged -= UpdateScriptHelpFromLine;
|
|
body.Content = string.Empty;
|
|
body.Content = info.Content;
|
|
body.Address = scriptStart;
|
|
body.CompiledLength = info.Length;
|
|
body.Label = label;
|
|
body.HelpSourceChanged += UpdateScriptHelpFromLine;
|
|
body.ContentChanged += ScriptChanged;
|
|
Contents[i] = body;
|
|
} else {
|
|
body.CompiledLength = info.Length;
|
|
body.Content = info.Content;
|
|
body.ContentChanged += ScriptChanged;
|
|
body.HelpSourceChanged += UpdateScriptHelpFromLine;
|
|
body.RequestShowSearchResult += ShowSearchResults;
|
|
Contents.Add(body);
|
|
needsAnimation = currentScriptLength != -1;
|
|
}
|
|
|
|
if (needsAnimation) AttentionNewContent.Raise(this);
|
|
}
|
|
|
|
while (Contents.Count > scripts.Count - skippedScripts) {
|
|
Contents[Contents.Count - 1].ContentChanged -= ScriptChanged;
|
|
Contents.RemoveAt(Contents.Count - 1);
|
|
}
|
|
}
|
|
|
|
private void ScriptChanged(object viewModel, ExtendedPropertyChangedEventArgs<string> e) {
|
|
var parser = mode switch {
|
|
CodeMode.Script => script,
|
|
CodeMode.BattleScript => battleScript,
|
|
CodeMode.AnimationScript => animationScript,
|
|
CodeMode.TrainerAiScript => battleAIScript,
|
|
_ => null,
|
|
};
|
|
var body = (CodeBody)viewModel;
|
|
body.TryCompleteCommandToken();
|
|
if (InsertAutoActive && body.TryInsertAuto()) {
|
|
// update the caret later, or weird stuff happens
|
|
singletons.WorkDispatcher.DispatchWork(() =>
|
|
singletons.WorkDispatcher.BlockOnUIWork(() => body.Editor.CaretIndex += 6)
|
|
);
|
|
}
|
|
if (InsertAutoActive) body.TryInsertAuto();
|
|
var delta = body.Content.Length - e.OldValue.Length;
|
|
var deltaSize = Math.Abs(delta);
|
|
if (body.CaretPosition >= deltaSize && body.CaretPosition < body.Content.Length - deltaSize) {
|
|
var start = body.Content[0..(body.CaretPosition + delta)];
|
|
if (start.EndsWith("<auto>")) InsertAutoActive = true;
|
|
start = body.Content[0..(body.CaretPosition + delta)];
|
|
if (start.EndsWith("<auto")) InsertAutoActive = false;
|
|
}
|
|
|
|
var codeContent = body.Content;
|
|
|
|
var run = model.GetNextRun(body.Address);
|
|
if (run != null && run.Start != body.Address) run = null;
|
|
|
|
int length = parser.FindLength(model, body.Address);
|
|
Action action = () => {
|
|
var initialStart = selection.Scroll.ViewPointToDataIndex(selection.SelectionStart);
|
|
var initialEnd = selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd);
|
|
if (initialStart > initialEnd) (initialStart, initialEnd) = (initialEnd, initialStart);
|
|
|
|
if (mode == CodeMode.Script) {
|
|
CompileScriptChanges<XSERun>(body, run, ref codeContent, e.OldValue, parser, body == Contents[0]);
|
|
} else if (mode == CodeMode.AnimationScript) {
|
|
CompileScriptChanges<ASERun>(body, run, ref codeContent, e.OldValue, parser, body == Contents[0]);
|
|
} else if (mode == CodeMode.BattleScript) {
|
|
CompileScriptChanges<BSERun>(body, run, ref codeContent, e.OldValue, parser, body == Contents[0]);
|
|
} else if (mode == CodeMode.TrainerAiScript) {
|
|
CompileScriptChanges<TSERun>(body, run, ref codeContent, e.OldValue, parser, body == Contents[0]);
|
|
}
|
|
|
|
body.ContentChanged -= ScriptChanged;
|
|
body.HelpSourceChanged -= UpdateScriptHelpFromLine;
|
|
body.Content = codeContent;
|
|
body.HelpSourceChanged += UpdateScriptHelpFromLine;
|
|
body.ContentChanged += ScriptChanged;
|
|
|
|
// reload
|
|
var start = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionStart));
|
|
var end = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd));
|
|
if (start > end) (start, end) = (end, start);
|
|
if (initialStart == body.Address) {
|
|
length = end - start + 1;
|
|
body.Address = start; // in case of the code getting repointed
|
|
}
|
|
UpdateContents(start, parser, body.Address, length);
|
|
};
|
|
|
|
// don't need to run this if they're still typing
|
|
/*
|
|
action();
|
|
/*/
|
|
body.CaretPosition += ScriptParser.InsertMissingClosers(ref codeContent, body.CaretPosition);
|
|
body.Content = codeContent;
|
|
recompileTimer.DelayCall(TimeSpan.FromSeconds(.5), action);
|
|
//*/
|
|
}
|
|
|
|
private void UpdateScriptHelpFromLine(object sender, HelpContext context) {
|
|
var codeBody = (CodeBody)sender;
|
|
string help;
|
|
if (mode == CodeMode.Script) help = ScriptParser.GetHelp(model, codeBody, context);
|
|
else if (mode == CodeMode.BattleScript) help = BattleScriptParser.GetHelp(model, codeBody, context);
|
|
else if (mode == CodeMode.AnimationScript) help = AnimationScriptParser.GetHelp(model, codeBody, context);
|
|
else if (mode == CodeMode.TrainerAiScript) help = BattleAIScriptParser.GetHelp(model, codeBody, context);
|
|
else throw new NotImplementedException();
|
|
codeBody.HelpContent = help;
|
|
}
|
|
|
|
private void ShowSearchResults(object sender, ISet<(int, int)> results) {
|
|
viewPort.OpenSearchResultsTab("Script Search Results", results.ToList());
|
|
}
|
|
|
|
private void CompileChanges() {
|
|
using (ModelCacheScope.CreateScope(model)) {
|
|
if (mode == CodeMode.Thumb) CompileThumbChanges();
|
|
}
|
|
}
|
|
|
|
private void CompileThumbChanges() {
|
|
var start = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionStart));
|
|
var end = Math.Min(model.Count - 1, selection.Scroll.ViewPointToDataIndex(selection.SelectionEnd));
|
|
if (start > end) (start, end) = (end, start);
|
|
int length = end - start + 1;
|
|
int originalLength = length;
|
|
var code = thumb.Compile(model, start, out var newRuns, Content.Split(new[] { Environment.NewLine, "\n" }, StringSplitOptions.None));
|
|
|
|
// if more length is needed and the next available bytes are free, allow it.
|
|
while (code.Count > length && model.Count > start + length + 1 && model[start + length] == 0xFF && model[start + length + 1] == 0xFF) length += 2;
|
|
|
|
if (code.Count > length) {
|
|
ErrorText = $"Thumb compiled to {code.Count} bytes, but only {length} bytes are available.";
|
|
ShowErrorText = true;
|
|
return;
|
|
}
|
|
ShowErrorText = false;
|
|
|
|
model.ClearFormat(history.CurrentChange, start + 1, length - 1);
|
|
for (int i = 0; i < code.Count; i++) history.CurrentChange.ChangeData(model, start + i, code[i]);
|
|
for (int i = code.Count; i < length; i++) history.CurrentChange.ChangeData(model, start + i, 0xFF);
|
|
foreach (var run in newRuns) model.ObserveRunWritten(history.CurrentChange, run);
|
|
|
|
ModelDataChanged?.Invoke(this, ErrorInfo.NoError);
|
|
|
|
if (length > originalLength) {
|
|
using (CreateRecursionGuard()) {
|
|
selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(start);
|
|
selection.SelectionEnd = selection.Scroll.DataIndexToViewPoint(start + length - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
private SERun Construct<SERun>(int start, SortedSpan<int> sources) where SERun : IScriptStartRun {
|
|
if (typeof(SERun) == typeof(XSERun)) return (SERun)(IScriptStartRun)new XSERun(start, sources);
|
|
if (typeof(SERun) == typeof(ASERun)) return (SERun)(IScriptStartRun)new ASERun(start, sources);
|
|
if (typeof(SERun) == typeof(BSERun)) return (SERun)(IScriptStartRun)new BSERun(start, sources);
|
|
if (typeof(SERun) == typeof(TSERun)) return (SERun)(IScriptStartRun)new TSERun(start, sources);
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
bool ignoreContentUpdates;
|
|
private IDisposable CreateRecursionGuard() {
|
|
if (ignoreContentUpdates) return new StubDisposable();
|
|
ignoreContentUpdates = true;
|
|
return new StubDisposable { Dispose = () => ignoreContentUpdates = false };
|
|
}
|
|
|
|
private void CompileScriptChanges<SERun>(CodeBody body, IFormattedRun run, ref string codeContent, string previousText, ScriptParser parser, bool updateSelection) where SERun : IScriptStartRun {
|
|
ShowErrorText = false;
|
|
ErrorText = string.Empty;
|
|
var sources = run?.PointerSources ?? null;
|
|
int start = body.Address;
|
|
int length = body.CompiledLength;
|
|
|
|
using (CreateRecursionGuard()) {
|
|
var oldScripts = parser.CollectScripts(model, start);
|
|
var originalCodeContent = codeContent;
|
|
int caret = body.CaretPosition;
|
|
body.ClearErrors();
|
|
body.EvaluateTextLength();
|
|
parser.CompileError += body.WatchForCompileErrors;
|
|
var code = parser.Compile(history.CurrentChange, model, start, ref codeContent, ref caret, body, out var movedData, out int ignoreCharacterCount);
|
|
parser.CompileError -= body.WatchForCompileErrors;
|
|
if (originalCodeContent != codeContent) body.Editor.CaretIndex = caret + codeContent.Length - previousText.Length - ignoreCharacterCount;
|
|
if (code == null) {
|
|
return;
|
|
}
|
|
|
|
if (code.Length > length) {
|
|
model.ExpandData(history.CurrentChange, start + code.Length - 1);
|
|
selection.Scroll.DataLength = model.RawData.Length;
|
|
if (run == null) {
|
|
var availableLength = length;
|
|
for (int i = start + length; i < start + code.Length; i++) {
|
|
// if it's freespace, then it's available
|
|
if (model[i] == 0xFF) {
|
|
availableLength++;
|
|
continue;
|
|
}
|
|
if (model.GetNextRun(i) is IScriptStartRun scriptRun && scriptRun.Start == i) {
|
|
// the next byte is a script... maybe it's ok to overwrite it
|
|
// we can overwrite it if it passes 2 checks
|
|
// (1) the only pointers to that script are contained within the script we're currently compiling
|
|
// (2) the script is contained completely within the compiled code (meaning it's actually part of the script as written)
|
|
if (scriptRun.PointerSources.All(source => start < source && source < start + length)) {
|
|
var scriptLength = parser.FindLength(model, scriptRun.Start, model.CurrentCacheScope.ScriptDestinations(start));
|
|
if (i + scriptLength <= start + code.Length) {
|
|
i += scriptLength - 1;
|
|
availableLength += scriptLength;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
ErrorText = $"Script is {code.Length} bytes long, but only {availableLength} bytes are available.";
|
|
ShowErrorText = true;
|
|
return;
|
|
}
|
|
} else {
|
|
if (run is NoInfoRun) run = Construct<SERun>(run.Start, run.PointerSources);
|
|
run = model.RelocateForExpansion(history.CurrentChange, run, body.CompiledLength, code.Length);
|
|
if (start != run.Start) {
|
|
ModelDataMoved?.Invoke(this, (start, run.Start));
|
|
start = run.Start;
|
|
int changedCaret = body.CaretPosition;
|
|
code = parser.Compile(history.CurrentChange, model, start, ref codeContent, ref changedCaret, body, out movedData, out var _); // recompile for the new location. Could update pointers.
|
|
// assume that changedCaret == body.CaretPosition? But it's probably not important
|
|
sources = run.PointerSources;
|
|
}
|
|
}
|
|
}
|
|
|
|
// pre-clear the format before we change the data
|
|
// waiting beyond this point won't clear anchors correctly, since pointer values will be changed
|
|
var changeStart = code.Length;
|
|
for (int i = 0; i < code.Length; i++) {
|
|
if (model[start + i] == code[i]) continue;
|
|
changeStart = Math.Max(1, i); // changeStart should never be zero: we don't want to clear the script anchor
|
|
break;
|
|
}
|
|
if (changeStart < code.Length) {
|
|
// use a nodatachange token here: we want to keep bytes, not anchor names
|
|
var change = history.InsertCustomChange(new NoDataChangeDeltaModel());
|
|
model.ClearFormat(change, start + changeStart, code.Length - changeStart);
|
|
}
|
|
|
|
var anyChanges = history.CurrentChange.ChangeData(model, start, code);
|
|
if (anyChanges || body.CompiledLength != code.Length) {
|
|
body.CompiledLength = code.Length;
|
|
model.ClearFormatAndData(history.CurrentChange, start + code.Length, length - code.Length);
|
|
}
|
|
var formatted = parser.FormatScript<SERun>(history.CurrentChange, model, start, code.Length);
|
|
if (sources != null) {
|
|
foreach (var source in sources) {
|
|
// skip the source if it's within one of the added scripts: it may have moved, and we've already added it.
|
|
if (formatted.Any(kvp => source.InRange(kvp.Key, kvp.Key + kvp.Value))) continue;
|
|
var existingRun = model.GetNextRun(source);
|
|
if (existingRun.Start > source || !(existingRun is ITableRun)) {
|
|
model.ObserveRunWritten(history.CurrentChange, new PointerRun(source));
|
|
}
|
|
}
|
|
}
|
|
|
|
// this change may have orphaned some existing scripts. Don't lose them!
|
|
var newScripts = parser.CollectScripts(model, start);
|
|
var orphans = oldScripts.Except(newScripts).ToList();
|
|
foreach (var orphan in orphans) {
|
|
var orphanRun = model.GetNextRun(orphan);
|
|
if (orphanRun.Start == orphan && orphanRun.PointerSources.IsNullOrEmpty() && string.IsNullOrEmpty(model.GetAnchorFromAddress(-1, orphan))) {
|
|
parser.FormatScript<SERun>(history.CurrentChange, model, orphan);
|
|
if (typeof(SERun) == typeof(XSERun)) {
|
|
model.ObserveAnchorWritten(history.CurrentChange, $"orphans.xse{orphan:X6}", new XSERun(orphan));
|
|
} else if (typeof(SERun) == typeof(BSERun)) {
|
|
model.ObserveAnchorWritten(history.CurrentChange, $"orphans.bse{orphan:X6}", new BSERun(orphan));
|
|
} else if (typeof(SERun) == typeof(ASERun)) {
|
|
model.ObserveAnchorWritten(history.CurrentChange, $"orphans.ase{orphan:X6}", new ASERun(orphan));
|
|
} else if (typeof(SERun) == typeof(TSERun)) {
|
|
model.ObserveAnchorWritten(history.CurrentChange, $"orphans.tse{orphan:X6}", new TSERun(orphan));
|
|
} else {
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (updateSelection) {
|
|
selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(start);
|
|
selection.SelectionEnd = selection.Scroll.DataIndexToViewPoint(start + code.Length - 1);
|
|
}
|
|
|
|
foreach (var movedResource in movedData) ModelDataMoved?.Invoke(this, movedResource);
|
|
}
|
|
|
|
ModelDataChanged?.Invoke(this, ErrorInfo.NoError);
|
|
}
|
|
|
|
private string RawParse(IDataModel model, int start, int length) {
|
|
var builder = new StringBuilder();
|
|
while (length > 0) {
|
|
builder.Append(model[start].ToHexString());
|
|
builder.Append(" ");
|
|
length--;
|
|
start++;
|
|
if (start % 16 == 0) builder.AppendLine();
|
|
}
|
|
return builder.ToString();
|
|
}
|
|
}
|
|
|
|
public class CodeTextFormatter : ITextPreProcessor {
|
|
public TextFormatting[] Format(string content) {
|
|
var result = new TextFormatting[content.Length];
|
|
bool inText = false, inComment = false;
|
|
for (int i = 0; i < content.Length; i++) {
|
|
if (inComment && content[i] == '\n') inComment = false;
|
|
if (content[i] == '#') inComment = true;
|
|
if (inComment) continue;
|
|
if (content[i] == '{') inText = true;
|
|
else if (content[i] == '}') inText = false;
|
|
else if (inText) result[i] = TextFormatting.Text;
|
|
}
|
|
return result;
|
|
}
|
|
public IEnumerable<TextSegment> FindErrors(string content) { yield break; }
|
|
}
|
|
|
|
public record HelpContext(string Line, int Index, int ContentBoundaryCount = 0, int ContentBoundaryIndex = -1, bool IsSelection = false);
|
|
}
|