Merge pull request #17 from dbidwell94/ic10editor-mod-patch

Ic10editor mod patch
This commit is contained in:
2025-12-05 15:37:24 -07:00
committed by GitHub
3 changed files with 162 additions and 37 deletions

View File

@@ -42,9 +42,9 @@ public static unsafe class SlangExtensions
* Rust allocation after the List is created, there is no need to Drop this memory. * Rust allocation after the List is created, there is no need to Drop this memory.
* </summary> * </summary>
*/ */
public static Line ToLine(this Vec_FfiToken_t vec, string sourceText) public static List<SemanticToken> ToTokenList(this Vec_FfiToken_t vec)
{ {
var list = new Line(sourceText); var tokens = new List<SemanticToken>();
var currentPtr = vec.ptr; var currentPtr = vec.ptr;
@@ -63,9 +63,8 @@ public static unsafe class SlangExtensions
0, 0,
colIndex, colIndex,
token.length, token.length,
color,
token.token_kind, token.token_kind,
0, color,
token.tooltip.AsString() token.tooltip.AsString()
); );
@@ -76,12 +75,12 @@ public static unsafe class SlangExtensions
semanticToken.Data = errMsg; semanticToken.Data = errMsg;
semanticToken.Color = ICodeFormatter.ColorError; semanticToken.Color = ICodeFormatter.ColorError;
} }
list.AddToken(semanticToken); tokens.Add(semanticToken);
} }
Ffi.free_ffi_token_vec(vec); Ffi.free_ffi_token_vec(vec);
return list; return tokens;
} }
public static unsafe List<Diagnostic> ToList(this Vec_FfiDiagnostic_t vec) public static unsafe List<Diagnostic> ToList(this Vec_FfiDiagnostic_t vec)

View File

@@ -4,7 +4,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Timers;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using StationeersIC10Editor; using StationeersIC10Editor;
@@ -28,6 +27,7 @@ public class SlangFormatter : ICodeFormatter
public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4"); public static readonly uint ColorOperator = ColorFromHTML("#D4D4D4");
private HashSet<uint> _linesWithErrors = new(); private HashSet<uint> _linesWithErrors = new();
private int _lastLineCount = -1;
public SlangFormatter() public SlangFormatter()
: base() : base()
@@ -70,9 +70,23 @@ public class SlangFormatter : ICodeFormatter
return this.Lines.RawText; return this.Lines.RawText;
} }
public override Line ParseLine(string line) public override StyledLine ParseLine(string line)
{ {
return Marshal.TokenizeLine(line); L.Debug($"Parsing line for syntax highlighting: {line}");
// We create the line first
var styledLine = new StyledLine(line);
// We get the semantic tokens (color + data)
var tokens = Marshal.TokenizeLine(line);
// We call update to create the basic tokens
styledLine.Update(tokens);
// CRITICAL FIX: We must manually re-attach metadata because StyledLine.Update() drops it.
ReattachMetadata(styledLine, tokens);
return styledLine;
} }
private void HandleCodeChanged() private void HandleCodeChanged()
@@ -90,8 +104,6 @@ public class SlangFormatter : ICodeFormatter
HandleLsp(inputSrc, token).Forget(); HandleLsp(inputSrc, token).Forget();
} }
private void OnTimerElapsed(object sender, ElapsedEventArgs e) { }
private async UniTaskVoid HandleLsp(string inputSrc, CancellationToken cancellationToken) private async UniTaskVoid HandleLsp(string inputSrc, CancellationToken cancellationToken)
{ {
try try
@@ -125,8 +137,27 @@ public class SlangFormatter : ICodeFormatter
// This runs on the Main Thread // This runs on the Main Thread
private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict) private void ApplyDiagnostics(Dictionary<uint, IGrouping<uint, Diagnostic>> dict)
{ {
var linesToRefresh = new HashSet<uint>(dict.Keys); HashSet<uint> linesToRefresh;
// CRITICAL FIX FOR LINE SHIFTS:
// If the line count has changed (lines added/deleted), indices have shifted.
// We must refresh ALL lines to ensure any line that shifted into a new position
// gets scrubbed of its old visual state.
if (this.Lines.Count != _lastLineCount)
{
linesToRefresh = new HashSet<uint>();
for (int i = 0; i < this.Lines.Count; i++)
{
linesToRefresh.Add((uint)i);
}
}
else
{
linesToRefresh = new HashSet<uint>(dict.Keys);
linesToRefresh.UnionWith(_linesWithErrors); linesToRefresh.UnionWith(_linesWithErrors);
}
_lastLineCount = this.Lines.Count;
foreach (var lineIndex in linesToRefresh) foreach (var lineIndex in linesToRefresh)
{ {
@@ -139,36 +170,131 @@ public class SlangFormatter : ICodeFormatter
if (line is null) if (line is null)
continue; continue;
line.ClearTokens(); // 1. Get base syntax tokens
var allTokens = Marshal.TokenizeLine(line.Text);
Dictionary<int, SemanticToken> lineDict = Marshal
.TokenizeLine(line.Text)
.Tokens.ToDictionary((t) => t.Column);
// 2. Overlay error tokens if diagnostics exist for this line
if (dict.ContainsKey(lineIndex)) if (dict.ContainsKey(lineIndex))
{ {
foreach (var lineDiagnostic in dict[lineIndex]) foreach (var lineDiagnostic in dict[lineIndex])
{ {
lineDict[(int)lineDiagnostic.Range.StartCol] = new SemanticToken allTokens.Add(
{ new SemanticToken(
Column = Math.Abs((int)lineDiagnostic.Range.StartCol), line: (int)lineIndex,
Length = Math.Abs( column: Math.Abs((int)lineDiagnostic.Range.StartCol),
length: Math.Abs(
(int)(lineDiagnostic.Range.EndCol - lineDiagnostic.Range.StartCol) (int)(lineDiagnostic.Range.EndCol - lineDiagnostic.Range.StartCol)
), ),
Line = (int)lineIndex, type: 0,
IsError = true, style: ICodeFormatter.ColorError,
Data = lineDiagnostic.Message, data: lineDiagnostic.Message,
Color = SlangFormatter.ColorError, isError: true
}; )
);
} }
} }
foreach (var token in lineDict.Values) // 3. Update the line (this clears existing tokens and uses the list we just built)
{ line.Update(allTokens);
line.AddToken(token);
} // 4. CRITICAL FIX: Re-attach metadata that Update() dropped
ReattachMetadata(line, allTokens);
} }
_linesWithErrors = new HashSet<uint>(dict.Keys); _linesWithErrors = new HashSet<uint>(dict.Keys);
} }
// Helper to map SemanticToken data (tooltips/errors) back to the tokens in the line
private void ReattachMetadata(StyledLine line, List<SemanticToken> semanticTokens)
{
foreach (var semToken in semanticTokens)
{
// Skip tokens without data
if (string.IsNullOrEmpty(semToken.Data))
continue;
// Find the corresponding Token in the line
var token = line.GetTokenAt(semToken.Column);
if (token != null)
{
// Wrap text to avoid "wide as monitor" tooltips
var wrappedMessage = WrapText(semToken.Data, 50);
var msgText = CreateStyledTextFromLines(
wrappedMessage,
semToken.IsError ? ICodeFormatter.ColorError : ICodeFormatter.ColorDefault
);
if (semToken.IsError)
{
token.Error = msgText;
}
else
{
token.Tooltip = msgText;
}
}
}
}
// Helper to create a StyledText object from a list of strings
private StyledText CreateStyledTextFromLines(List<string> lines, uint color)
{
var styledText = new StyledText();
foreach (var lineContent in lines)
{
var l = new StyledLine(lineContent);
l.Add(new Token(0, lineContent, new Style(color)));
styledText.Add(l);
}
return styledText;
}
// Text wrapper that preserves paragraph structure but enforces width
private List<string> WrapText(string text, int maxLineLength)
{
var lines = new List<string>();
if (string.IsNullOrEmpty(text))
return lines;
// Normalize newlines and split by paragraph
var paragraphs = text.Replace("\r\n", "\n").Split('\n');
foreach (var paragraph in paragraphs)
{
// Preserve empty lines (paragraph breaks)
if (string.IsNullOrWhiteSpace(paragraph))
{
lines.Add("");
continue;
}
var words = paragraph.Split(' ');
var currentLine = "";
foreach (var word in words)
{
// If adding the next word exceeds max length...
if (currentLine.Length + word.Length + 1 > maxLineLength)
{
// Push current line if it has content
if (currentLine.Length > 0)
{
lines.Add(currentLine.TrimEnd());
currentLine = "";
}
}
if (currentLine.Length > 0)
currentLine += " ";
currentLine += word;
}
// Flush remaining content
if (currentLine.Length > 0)
{
lines.Add(currentLine.TrimEnd());
}
}
return lines;
}
} }

View File

@@ -131,11 +131,11 @@ public static class Marshal
} }
} }
public static unsafe Line TokenizeLine(string inputString) public static unsafe List<SemanticToken> TokenizeLine(string inputString)
{ {
if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded()) if (string.IsNullOrEmpty(inputString) || !EnsureLibLoaded())
{ {
return new Line(inputString); return new List<SemanticToken>();
} }
fixed (char* ptrInputStr = inputString) fixed (char* ptrInputStr = inputString)
@@ -147,8 +147,8 @@ public static class Marshal
}; };
var tokens = Ffi.tokenize_line(strRef); var tokens = Ffi.tokenize_line(strRef);
L.Debug($"Tokenized line '{inputString}' into {tokens.len} tokens.");
return tokens.ToLine(inputString); return tokens.ToTokenList();
} }
} }