1450 lines
45 KiB
C#
1450 lines
45 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
public class ProfilerAutoInstrumentWindow : EditorWindow
|
|
{
|
|
private const string AssetRoot = "Assets";
|
|
private const string PersistKey = "TH1.ProfilerAutoInstrument.Selection.v2";
|
|
private const string MarkerBegin = "// TH1_AUTO_PROFILE_BEGIN";
|
|
private const string MarkerEnd = "// TH1_AUTO_PROFILE_END";
|
|
private const string IndentUnit = " ";
|
|
private const int MaxRenderFoldersWithoutSearch = 400;
|
|
private const int MaxRenderFilesWithoutSearch = 500;
|
|
private const int MaxRenderFunctionsWithoutSearch = 300;
|
|
private const int MaxRenderItemsWithSearch = 1200;
|
|
|
|
private static readonly HashSet<string> NonMethodKeywords = new HashSet<string>(StringComparer.Ordinal)
|
|
{
|
|
"if", "for", "foreach", "while", "switch", "catch", "lock", "using", "nameof", "typeof", "sizeof", "default"
|
|
};
|
|
|
|
[Serializable]
|
|
private class PersistSelection
|
|
{
|
|
public List<string> folders = new List<string>();
|
|
public List<string> files = new List<string>();
|
|
public List<string> functions = new List<string>();
|
|
}
|
|
|
|
private class FunctionItem
|
|
{
|
|
public string Key;
|
|
public string AssetPath;
|
|
public string DeclaringTypeName;
|
|
public string MethodName;
|
|
public int Line;
|
|
public string DisplayText;
|
|
}
|
|
|
|
private class FileSelectionTarget
|
|
{
|
|
public bool InstrumentAll;
|
|
public HashSet<string> FunctionKeys;
|
|
}
|
|
|
|
private Vector2 _folderScrollPos;
|
|
private Vector2 _fileScrollPos;
|
|
private Vector2 _functionScrollPos;
|
|
private Vector2 _reportScrollPos;
|
|
|
|
private string _folderSearch = string.Empty;
|
|
private string _fileSearch = string.Empty;
|
|
private string _functionSearch = string.Empty;
|
|
private string _lastReport = string.Empty;
|
|
|
|
private bool _indexBuilt;
|
|
private readonly List<string> _folderCandidates = new List<string>();
|
|
private readonly List<string> _fileCandidates = new List<string>();
|
|
private readonly List<FunctionItem> _functionCandidates = new List<FunctionItem>();
|
|
private readonly Dictionary<string, FunctionItem> _functionMap = new Dictionary<string, FunctionItem>(StringComparer.Ordinal);
|
|
|
|
private readonly HashSet<string> _selectedFolders = new HashSet<string>(StringComparer.Ordinal);
|
|
private readonly HashSet<string> _selectedFiles = new HashSet<string>(StringComparer.Ordinal);
|
|
private readonly HashSet<string> _selectedFunctionKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
private readonly List<string> _filteredFolders = new List<string>();
|
|
private readonly List<string> _filteredFiles = new List<string>();
|
|
private readonly List<FunctionItem> _filteredFunctions = new List<FunctionItem>();
|
|
private string _folderSearchCache = string.Empty;
|
|
private string _fileSearchCache = string.Empty;
|
|
private string _functionSearchCache = string.Empty;
|
|
|
|
private enum BlockKind
|
|
{
|
|
Other,
|
|
Type,
|
|
Method
|
|
}
|
|
|
|
private struct BlockInfo
|
|
{
|
|
public BlockKind Kind;
|
|
public int MethodIndex;
|
|
}
|
|
|
|
private struct MethodBlock
|
|
{
|
|
public int OpenBraceIndex;
|
|
public int CloseBraceIndex;
|
|
public string DeclaringTypeName;
|
|
public string MethodName;
|
|
}
|
|
|
|
private enum InstrumentResult
|
|
{
|
|
Changed,
|
|
AlreadyInstrumented,
|
|
NoMethod,
|
|
Failed
|
|
}
|
|
|
|
[MenuItem("Tools/TH1/Profiler Auto Instrument")]
|
|
public static void ShowWindow()
|
|
{
|
|
var window = GetWindow<ProfilerAutoInstrumentWindow>("Profiler Auto Instrument");
|
|
window.minSize = new Vector2(1080, 560);
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
LoadSelection();
|
|
}
|
|
|
|
private void OnGUI()
|
|
{
|
|
EditorGUILayout.LabelField("自动打点(按文件夹 / 文件 / 函数)", EditorStyles.boldLabel);
|
|
EditorGUILayout.HelpBox(
|
|
"支持按文件夹、文件、函数进行精准勾选打点;可搜索并保存本地勾选记录。\n" +
|
|
$"已插入的代码会带标记:{MarkerBegin}",
|
|
MessageType.Info);
|
|
|
|
EditorGUILayout.Space(4);
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("刷新索引", GUILayout.Width(100)))
|
|
{
|
|
BuildIndex();
|
|
}
|
|
|
|
GUILayout.FlexibleSpace();
|
|
EditorGUILayout.LabelField(
|
|
$"已选:文件夹 {_selectedFolders.Count} / 文件 {_selectedFiles.Count} / 函数 {_selectedFunctionKeys.Count}",
|
|
EditorStyles.miniLabel,
|
|
GUILayout.Width(360));
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
if (!_indexBuilt)
|
|
{
|
|
EditorGUILayout.HelpBox("索引尚未构建,请点击“刷新索引”。", MessageType.Warning);
|
|
return;
|
|
}
|
|
|
|
EditorGUILayout.Space(6);
|
|
EditorGUILayout.BeginHorizontal();
|
|
DrawFolderPanel();
|
|
DrawFilePanel();
|
|
DrawFunctionPanel();
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
EditorGUILayout.Space(8);
|
|
if (GUILayout.Button("开始自动打点", GUILayout.Height(34)))
|
|
{
|
|
RunAutoInstrument();
|
|
}
|
|
|
|
EditorGUILayout.Space(10);
|
|
EditorGUILayout.LabelField("执行结果", EditorStyles.boldLabel);
|
|
_reportScrollPos = EditorGUILayout.BeginScrollView(_reportScrollPos, GUILayout.Height(150));
|
|
EditorGUILayout.TextArea(string.IsNullOrEmpty(_lastReport) ? "尚未执行。" : _lastReport, GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.EndScrollView();
|
|
}
|
|
|
|
private void DrawFolderPanel()
|
|
{
|
|
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField($"文件夹 ({_folderCandidates.Count})", EditorStyles.boldLabel);
|
|
_folderSearch = EditorGUILayout.TextField("搜索", _folderSearch);
|
|
RebuildFilteredFoldersIfNeeded();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("全选筛选项"))
|
|
{
|
|
bool changed = false;
|
|
foreach (var folder in _filteredFolders)
|
|
{
|
|
changed |= _selectedFolders.Add(folder);
|
|
}
|
|
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
if (GUILayout.Button("取消筛选项"))
|
|
{
|
|
bool changed = false;
|
|
var toRemove = new List<string>();
|
|
foreach (var folder in _selectedFolders)
|
|
{
|
|
if (_filteredFolders.Contains(folder)) toRemove.Add(folder);
|
|
}
|
|
|
|
foreach (var folder in toRemove) changed |= _selectedFolders.Remove(folder);
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
_folderScrollPos = EditorGUILayout.BeginScrollView(_folderScrollPos);
|
|
int renderLimit = string.IsNullOrWhiteSpace(_folderSearch) ? MaxRenderFoldersWithoutSearch : MaxRenderItemsWithSearch;
|
|
int renderCount = Math.Min(renderLimit, _filteredFolders.Count);
|
|
if (_filteredFolders.Count > renderCount)
|
|
{
|
|
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
|
|
}
|
|
|
|
bool anyChanged = false;
|
|
for (int i = 0; i < renderCount; i++)
|
|
{
|
|
var folder = _filteredFolders[i];
|
|
bool selected = _selectedFolders.Contains(folder);
|
|
bool newSelected = EditorGUILayout.ToggleLeft(folder, selected);
|
|
if (newSelected == selected) continue;
|
|
anyChanged = true;
|
|
if (newSelected) _selectedFolders.Add(folder);
|
|
else _selectedFolders.Remove(folder);
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
if (anyChanged) SaveSelection();
|
|
}
|
|
|
|
private void DrawFilePanel()
|
|
{
|
|
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField($"文件 ({_fileCandidates.Count})", EditorStyles.boldLabel);
|
|
_fileSearch = EditorGUILayout.TextField("搜索", _fileSearch);
|
|
RebuildFilteredFilesIfNeeded();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("全选筛选项"))
|
|
{
|
|
bool changed = false;
|
|
foreach (var file in _filteredFiles)
|
|
{
|
|
changed |= _selectedFiles.Add(file);
|
|
}
|
|
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
if (GUILayout.Button("取消筛选项"))
|
|
{
|
|
bool changed = false;
|
|
var toRemove = new List<string>();
|
|
foreach (var file in _selectedFiles)
|
|
{
|
|
if (_filteredFiles.Contains(file)) toRemove.Add(file);
|
|
}
|
|
|
|
foreach (var file in toRemove) changed |= _selectedFiles.Remove(file);
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
_fileScrollPos = EditorGUILayout.BeginScrollView(_fileScrollPos);
|
|
int renderLimit = string.IsNullOrWhiteSpace(_fileSearch) ? MaxRenderFilesWithoutSearch : MaxRenderItemsWithSearch;
|
|
int renderCount = Math.Min(renderLimit, _filteredFiles.Count);
|
|
if (_filteredFiles.Count > renderCount)
|
|
{
|
|
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
|
|
}
|
|
|
|
bool anyChanged = false;
|
|
for (int i = 0; i < renderCount; i++)
|
|
{
|
|
var file = _filteredFiles[i];
|
|
bool selected = _selectedFiles.Contains(file);
|
|
bool newSelected = EditorGUILayout.ToggleLeft(file, selected);
|
|
if (newSelected == selected) continue;
|
|
anyChanged = true;
|
|
if (newSelected) _selectedFiles.Add(file);
|
|
else _selectedFiles.Remove(file);
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
if (anyChanged) SaveSelection();
|
|
}
|
|
|
|
private void DrawFunctionPanel()
|
|
{
|
|
EditorGUILayout.BeginVertical("box", GUILayout.Width(position.width / 3f - 8), GUILayout.ExpandHeight(true));
|
|
EditorGUILayout.LabelField($"函数 ({_functionCandidates.Count})", EditorStyles.boldLabel);
|
|
_functionSearch = EditorGUILayout.TextField("搜索", _functionSearch);
|
|
RebuildFilteredFunctionsIfNeeded();
|
|
|
|
EditorGUILayout.BeginHorizontal();
|
|
if (GUILayout.Button("全选筛选项"))
|
|
{
|
|
bool changed = false;
|
|
foreach (var func in _filteredFunctions)
|
|
{
|
|
changed |= _selectedFunctionKeys.Add(func.Key);
|
|
}
|
|
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
if (GUILayout.Button("取消筛选项"))
|
|
{
|
|
bool changed = false;
|
|
var toRemove = new List<string>();
|
|
foreach (var funcKey in _selectedFunctionKeys)
|
|
{
|
|
if (!_functionMap.TryGetValue(funcKey, out var func)) continue;
|
|
if (_filteredFunctions.Contains(func)) toRemove.Add(funcKey);
|
|
}
|
|
|
|
foreach (var key in toRemove) changed |= _selectedFunctionKeys.Remove(key);
|
|
if (changed) SaveSelection();
|
|
}
|
|
|
|
EditorGUILayout.EndHorizontal();
|
|
|
|
_functionScrollPos = EditorGUILayout.BeginScrollView(_functionScrollPos);
|
|
int renderLimit = string.IsNullOrWhiteSpace(_functionSearch) ? MaxRenderFunctionsWithoutSearch : MaxRenderItemsWithSearch;
|
|
int renderCount = Math.Min(renderLimit, _filteredFunctions.Count);
|
|
if (_filteredFunctions.Count > renderCount)
|
|
{
|
|
EditorGUILayout.HelpBox($"结果过多,仅渲染前 {renderCount} 条,请继续搜索缩小范围。", MessageType.None);
|
|
}
|
|
|
|
bool anyChanged = false;
|
|
for (int i = 0; i < renderCount; i++)
|
|
{
|
|
var func = _filteredFunctions[i];
|
|
bool selected = _selectedFunctionKeys.Contains(func.Key);
|
|
bool newSelected = EditorGUILayout.ToggleLeft(func.DisplayText, selected);
|
|
if (newSelected == selected) continue;
|
|
anyChanged = true;
|
|
if (newSelected) _selectedFunctionKeys.Add(func.Key);
|
|
else _selectedFunctionKeys.Remove(func.Key);
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
EditorGUILayout.EndVertical();
|
|
if (anyChanged) SaveSelection();
|
|
}
|
|
|
|
private void BuildIndex()
|
|
{
|
|
_folderCandidates.Clear();
|
|
_fileCandidates.Clear();
|
|
_functionCandidates.Clear();
|
|
_functionMap.Clear();
|
|
|
|
var folders = new HashSet<string>(StringComparer.Ordinal);
|
|
var csFiles = Directory.GetFiles(Application.dataPath, "*.cs", SearchOption.AllDirectories);
|
|
Array.Sort(csFiles, StringComparer.Ordinal);
|
|
|
|
foreach (var fullPath in csFiles)
|
|
{
|
|
var assetPath = AbsolutePathToAssetPath(fullPath);
|
|
if (!assetPath.StartsWith(AssetRoot, StringComparison.Ordinal)) continue;
|
|
|
|
_fileCandidates.Add(assetPath);
|
|
CollectParentFolders(assetPath, folders);
|
|
|
|
string content;
|
|
try
|
|
{
|
|
content = File.ReadAllText(fullPath);
|
|
}
|
|
catch
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var methods = FindMethodBlocks(content);
|
|
if (methods.Count == 0) continue;
|
|
|
|
var occurrenceMap = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
for (int i = 0; i < methods.Count; i++)
|
|
{
|
|
var method = methods[i];
|
|
occurrenceMap.TryGetValue(method.MethodName, out int occurrence);
|
|
occurrence++;
|
|
occurrenceMap[method.MethodName] = occurrence;
|
|
var line = GetLineNumber(content, method.OpenBraceIndex);
|
|
|
|
var item = new FunctionItem
|
|
{
|
|
AssetPath = assetPath,
|
|
DeclaringTypeName = method.DeclaringTypeName,
|
|
MethodName = method.MethodName,
|
|
Line = line,
|
|
Key = BuildFunctionKey(assetPath, method.MethodName, occurrence),
|
|
DisplayText = $"{method.DeclaringTypeName}.{method.MethodName} ({assetPath}:{line})"
|
|
};
|
|
|
|
_functionCandidates.Add(item);
|
|
_functionMap[item.Key] = item;
|
|
}
|
|
}
|
|
|
|
_folderCandidates.AddRange(folders);
|
|
_folderCandidates.Sort(StringComparer.Ordinal);
|
|
_fileCandidates.Sort(StringComparer.Ordinal);
|
|
_functionCandidates.Sort((a, b) =>
|
|
{
|
|
int pathCmp = string.CompareOrdinal(a.AssetPath, b.AssetPath);
|
|
if (pathCmp != 0) return pathCmp;
|
|
int nameCmp = string.CompareOrdinal(a.MethodName, b.MethodName);
|
|
if (nameCmp != 0) return nameCmp;
|
|
return a.Line.CompareTo(b.Line);
|
|
});
|
|
|
|
PruneSelections();
|
|
InvalidateFilteredCaches();
|
|
_indexBuilt = true;
|
|
SaveSelection();
|
|
}
|
|
|
|
private void InvalidateFilteredCaches()
|
|
{
|
|
_folderSearchCache = null;
|
|
_fileSearchCache = null;
|
|
_functionSearchCache = null;
|
|
_filteredFolders.Clear();
|
|
_filteredFiles.Clear();
|
|
_filteredFunctions.Clear();
|
|
}
|
|
|
|
private void RebuildFilteredFoldersIfNeeded()
|
|
{
|
|
if (string.Equals(_folderSearchCache, _folderSearch, StringComparison.Ordinal)) return;
|
|
_folderSearchCache = _folderSearch;
|
|
_filteredFolders.Clear();
|
|
if (string.IsNullOrWhiteSpace(_folderSearch))
|
|
{
|
|
foreach (var folder in _folderCandidates)
|
|
{
|
|
if (!_selectedFolders.Contains(folder)) continue;
|
|
_filteredFolders.Add(folder);
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (var folder in _folderCandidates)
|
|
{
|
|
if (!IsMatch(folder, _folderSearch)) continue;
|
|
_filteredFolders.Add(folder);
|
|
}
|
|
}
|
|
|
|
private void RebuildFilteredFilesIfNeeded()
|
|
{
|
|
if (string.Equals(_fileSearchCache, _fileSearch, StringComparison.Ordinal)) return;
|
|
_fileSearchCache = _fileSearch;
|
|
_filteredFiles.Clear();
|
|
if (string.IsNullOrWhiteSpace(_fileSearch))
|
|
{
|
|
foreach (var file in _fileCandidates)
|
|
{
|
|
if (!_selectedFiles.Contains(file)) continue;
|
|
_filteredFiles.Add(file);
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (var file in _fileCandidates)
|
|
{
|
|
if (!IsMatch(file, _fileSearch)) continue;
|
|
_filteredFiles.Add(file);
|
|
}
|
|
}
|
|
|
|
private void RebuildFilteredFunctionsIfNeeded()
|
|
{
|
|
if (string.Equals(_functionSearchCache, _functionSearch, StringComparison.Ordinal)) return;
|
|
_functionSearchCache = _functionSearch;
|
|
_filteredFunctions.Clear();
|
|
if (string.IsNullOrWhiteSpace(_functionSearch))
|
|
{
|
|
foreach (var func in _functionCandidates)
|
|
{
|
|
if (!_selectedFunctionKeys.Contains(func.Key)) continue;
|
|
_filteredFunctions.Add(func);
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (var func in _functionCandidates)
|
|
{
|
|
if (!IsFunctionMatch(func, _functionSearch)) continue;
|
|
_filteredFunctions.Add(func);
|
|
}
|
|
}
|
|
|
|
private void CollectParentFolders(string assetPath, HashSet<string> folders)
|
|
{
|
|
var folder = Path.GetDirectoryName(assetPath)?.Replace("\\", "/");
|
|
while (!string.IsNullOrEmpty(folder) && folder.StartsWith(AssetRoot, StringComparison.Ordinal))
|
|
{
|
|
folders.Add(folder);
|
|
int slash = folder.LastIndexOf('/');
|
|
if (slash <= 0) break;
|
|
folder = folder.Substring(0, slash);
|
|
}
|
|
}
|
|
|
|
private void PruneSelections()
|
|
{
|
|
_selectedFolders.RemoveWhere(path => !_folderCandidates.Contains(path));
|
|
_selectedFiles.RemoveWhere(path => !_fileCandidates.Contains(path));
|
|
_selectedFunctionKeys.RemoveWhere(key => !_functionMap.ContainsKey(key));
|
|
}
|
|
|
|
private void LoadSelection()
|
|
{
|
|
_selectedFolders.Clear();
|
|
_selectedFiles.Clear();
|
|
_selectedFunctionKeys.Clear();
|
|
|
|
if (!EditorPrefs.HasKey(PersistKey)) return;
|
|
|
|
var json = EditorPrefs.GetString(PersistKey, string.Empty);
|
|
if (string.IsNullOrEmpty(json)) return;
|
|
|
|
PersistSelection state;
|
|
try
|
|
{
|
|
state = JsonUtility.FromJson<PersistSelection>(json);
|
|
}
|
|
catch
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (state == null) return;
|
|
if (state.folders != null) foreach (var item in state.folders) _selectedFolders.Add(item);
|
|
if (state.files != null) foreach (var item in state.files) _selectedFiles.Add(item);
|
|
if (state.functions != null) foreach (var item in state.functions) _selectedFunctionKeys.Add(item);
|
|
}
|
|
|
|
private void SaveSelection()
|
|
{
|
|
var state = new PersistSelection
|
|
{
|
|
folders = new List<string>(_selectedFolders),
|
|
files = new List<string>(_selectedFiles),
|
|
functions = new List<string>(_selectedFunctionKeys)
|
|
};
|
|
EditorPrefs.SetString(PersistKey, JsonUtility.ToJson(state));
|
|
InvalidateFilteredCaches();
|
|
}
|
|
|
|
private static bool IsMatch(string text, string query)
|
|
{
|
|
return string.IsNullOrWhiteSpace(query) ||
|
|
text.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private static bool IsFunctionMatch(FunctionItem item, string query)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(query)) return true;
|
|
return item.MethodName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0 ||
|
|
item.AssetPath.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
}
|
|
|
|
private static string BuildFunctionKey(string assetPath, string methodName, int occurrence)
|
|
{
|
|
return $"{assetPath}|{methodName}|{occurrence}";
|
|
}
|
|
|
|
private static int GetLineNumber(string content, int index)
|
|
{
|
|
int line = 1;
|
|
int max = Mathf.Clamp(index, 0, Math.Max(0, content.Length));
|
|
for (int i = 0; i < max; i++)
|
|
{
|
|
if (content[i] == '\n') line++;
|
|
}
|
|
|
|
return line;
|
|
}
|
|
|
|
private Dictionary<string, FileSelectionTarget> BuildInstrumentTargets()
|
|
{
|
|
var targets = new Dictionary<string, FileSelectionTarget>(StringComparer.Ordinal);
|
|
|
|
foreach (var folder in _selectedFolders)
|
|
{
|
|
string prefix = folder.EndsWith("/", StringComparison.Ordinal) ? folder : folder + "/";
|
|
foreach (var assetPath in _fileCandidates)
|
|
{
|
|
if (!assetPath.StartsWith(prefix, StringComparison.Ordinal)) continue;
|
|
if (!targets.TryGetValue(assetPath, out var target))
|
|
{
|
|
target = new FileSelectionTarget();
|
|
targets[assetPath] = target;
|
|
}
|
|
|
|
target.InstrumentAll = true;
|
|
target.FunctionKeys = null;
|
|
}
|
|
}
|
|
|
|
foreach (var file in _selectedFiles)
|
|
{
|
|
if (!targets.TryGetValue(file, out var target))
|
|
{
|
|
target = new FileSelectionTarget();
|
|
targets[file] = target;
|
|
}
|
|
|
|
target.InstrumentAll = true;
|
|
target.FunctionKeys = null;
|
|
}
|
|
|
|
foreach (var key in _selectedFunctionKeys)
|
|
{
|
|
if (!_functionMap.TryGetValue(key, out var function)) continue;
|
|
if (!targets.TryGetValue(function.AssetPath, out var target))
|
|
{
|
|
target = new FileSelectionTarget();
|
|
targets[function.AssetPath] = target;
|
|
}
|
|
|
|
if (target.InstrumentAll) continue;
|
|
target.FunctionKeys ??= new HashSet<string>(StringComparer.Ordinal);
|
|
target.FunctionKeys.Add(key);
|
|
}
|
|
|
|
return targets;
|
|
}
|
|
|
|
private void RunAutoInstrument()
|
|
{
|
|
var targets = BuildInstrumentTargets();
|
|
if (targets.Count == 0)
|
|
{
|
|
EditorUtility.DisplayDialog("未选择内容", "请至少勾选一个文件夹、文件或函数。", "确定");
|
|
return;
|
|
}
|
|
|
|
var assetPaths = new List<string>(targets.Keys);
|
|
assetPaths.Sort(StringComparer.Ordinal);
|
|
|
|
int changed = 0;
|
|
int already = 0;
|
|
int noMethod = 0;
|
|
int failed = 0;
|
|
var details = new StringBuilder();
|
|
|
|
for (int i = 0; i < assetPaths.Count; i++)
|
|
{
|
|
var assetPath = assetPaths[i];
|
|
var fullPath = AssetPathToAbsolutePath(assetPath);
|
|
var target = targets[assetPath];
|
|
var result = InstrumentSingleFile(
|
|
fullPath,
|
|
assetPath,
|
|
target.InstrumentAll ? null : target.FunctionKeys,
|
|
out var detail);
|
|
|
|
switch (result)
|
|
{
|
|
case InstrumentResult.Changed:
|
|
changed++;
|
|
details.AppendLine($"[Changed] {assetPath}");
|
|
break;
|
|
case InstrumentResult.AlreadyInstrumented:
|
|
already++;
|
|
break;
|
|
case InstrumentResult.NoMethod:
|
|
noMethod++;
|
|
break;
|
|
case InstrumentResult.Failed:
|
|
failed++;
|
|
details.AppendLine($"[Failed] {assetPath} -> {detail}");
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
AssetDatabase.Refresh();
|
|
|
|
_lastReport =
|
|
$"目标文件:{assetPaths.Count}\n" +
|
|
$"已打点:{changed}\n" +
|
|
$"已存在打点:{already}\n" +
|
|
$"无可打点方法:{noMethod}\n" +
|
|
$"失败:{failed}\n\n" +
|
|
details;
|
|
|
|
Debug.Log($"[ProfilerAutoInstrument] 完成。Changed={changed}, Already={already}, NoMethod={noMethod}, Failed={failed}");
|
|
}
|
|
|
|
private static InstrumentResult InstrumentSingleFile(
|
|
string fullPath,
|
|
string assetPath,
|
|
HashSet<string> selectedFunctionKeys,
|
|
out string detail)
|
|
{
|
|
detail = string.Empty;
|
|
try
|
|
{
|
|
var rawBytes = File.ReadAllBytes(fullPath);
|
|
bool hasUtf8Bom = rawBytes.Length >= 3 &&
|
|
rawBytes[0] == 0xEF &&
|
|
rawBytes[1] == 0xBB &&
|
|
rawBytes[2] == 0xBF;
|
|
|
|
var original = File.ReadAllText(fullPath);
|
|
var methods = FindMethodBlocks(original);
|
|
if (methods.Count == 0)
|
|
{
|
|
return InstrumentResult.NoMethod;
|
|
}
|
|
|
|
var targetMethods = ResolveTargetMethods(original, methods, assetPath, selectedFunctionKeys);
|
|
if (targetMethods.Count == 0)
|
|
{
|
|
return InstrumentResult.NoMethod;
|
|
}
|
|
|
|
bool allAlreadyInstrumented = true;
|
|
for (int i = 0; i < targetMethods.Count; i++)
|
|
{
|
|
if (IsMethodAlreadyInstrumented(original, targetMethods[i])) continue;
|
|
allAlreadyInstrumented = false;
|
|
break;
|
|
}
|
|
|
|
if (allAlreadyInstrumented)
|
|
{
|
|
return InstrumentResult.AlreadyInstrumented;
|
|
}
|
|
|
|
var instrumented = BuildInstrumentedContent(original, targetMethods);
|
|
if (instrumented == original)
|
|
{
|
|
return InstrumentResult.AlreadyInstrumented;
|
|
}
|
|
|
|
File.WriteAllText(fullPath, instrumented, new UTF8Encoding(hasUtf8Bom));
|
|
return InstrumentResult.Changed;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
detail = ex.Message;
|
|
return InstrumentResult.Failed;
|
|
}
|
|
}
|
|
|
|
private static List<MethodBlock> ResolveTargetMethods(
|
|
string content,
|
|
List<MethodBlock> methods,
|
|
string assetPath,
|
|
HashSet<string> selectedFunctionKeys)
|
|
{
|
|
if (selectedFunctionKeys == null || selectedFunctionKeys.Count == 0)
|
|
{
|
|
return methods;
|
|
}
|
|
|
|
var result = new List<MethodBlock>();
|
|
var occurrenceMap = new Dictionary<string, int>(StringComparer.Ordinal);
|
|
for (int i = 0; i < methods.Count; i++)
|
|
{
|
|
var method = methods[i];
|
|
occurrenceMap.TryGetValue(method.MethodName, out int occurrence);
|
|
occurrence++;
|
|
occurrenceMap[method.MethodName] = occurrence;
|
|
|
|
var key = BuildFunctionKey(assetPath, method.MethodName, occurrence);
|
|
if (!selectedFunctionKeys.Contains(key)) continue;
|
|
result.Add(method);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static List<MethodBlock> FindMethodBlocks(string content)
|
|
{
|
|
var codeOnly = BuildCodeOnlyBuffer(content);
|
|
var methods = new List<MethodBlock>();
|
|
var stack = new Stack<BlockInfo>();
|
|
var typeNameStack = new List<string>();
|
|
int typeDepth = 0;
|
|
int methodDepth = 0;
|
|
|
|
for (int i = 0; i < codeOnly.Length; i++)
|
|
{
|
|
char c = codeOnly[i];
|
|
if (c == '{')
|
|
{
|
|
var block = new BlockInfo { Kind = BlockKind.Other, MethodIndex = -1 };
|
|
|
|
if (LooksLikeTypeBlock(codeOnly, i))
|
|
{
|
|
block.Kind = BlockKind.Type;
|
|
if (!TryResolveTypeName(codeOnly, i, out var typeName))
|
|
{
|
|
typeName = "UnknownType";
|
|
}
|
|
typeNameStack.Add(typeName);
|
|
typeDepth++;
|
|
}
|
|
else if (typeDepth > 0 && methodDepth == 0 && TryResolveMethodName(codeOnly, i, out var methodName))
|
|
{
|
|
var method = new MethodBlock
|
|
{
|
|
OpenBraceIndex = i,
|
|
CloseBraceIndex = -1,
|
|
DeclaringTypeName = BuildDeclaringTypeName(typeNameStack),
|
|
MethodName = methodName
|
|
};
|
|
methods.Add(method);
|
|
block.Kind = BlockKind.Method;
|
|
block.MethodIndex = methods.Count - 1;
|
|
methodDepth++;
|
|
}
|
|
|
|
stack.Push(block);
|
|
continue;
|
|
}
|
|
|
|
if (c != '}' || stack.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var closedBlock = stack.Pop();
|
|
if (closedBlock.Kind == BlockKind.Type)
|
|
{
|
|
typeDepth = Math.Max(0, typeDepth - 1);
|
|
if (typeNameStack.Count > 0)
|
|
{
|
|
typeNameStack.RemoveAt(typeNameStack.Count - 1);
|
|
}
|
|
}
|
|
else if (closedBlock.Kind == BlockKind.Method)
|
|
{
|
|
methodDepth = Math.Max(0, methodDepth - 1);
|
|
if (closedBlock.MethodIndex >= 0 && closedBlock.MethodIndex < methods.Count)
|
|
{
|
|
var method = methods[closedBlock.MethodIndex];
|
|
method.CloseBraceIndex = i;
|
|
methods[closedBlock.MethodIndex] = method;
|
|
}
|
|
}
|
|
}
|
|
|
|
methods.RemoveAll(m => m.CloseBraceIndex <= m.OpenBraceIndex);
|
|
return methods;
|
|
}
|
|
|
|
private static string BuildInstrumentedContent(string content, List<MethodBlock> methods)
|
|
{
|
|
string newline = DetectNewLine(content);
|
|
var output = new StringBuilder(content.Length + methods.Count * 220);
|
|
int cursor = 0;
|
|
bool changed = false;
|
|
|
|
for (int i = 0; i < methods.Count; i++)
|
|
{
|
|
var method = methods[i];
|
|
if (method.OpenBraceIndex < cursor || method.CloseBraceIndex <= method.OpenBraceIndex)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsMethodAlreadyInstrumented(content, method))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
output.Append(content, cursor, method.OpenBraceIndex + 1 - cursor);
|
|
|
|
string braceIndent = GetLineIndent(content, method.OpenBraceIndex);
|
|
string innerIndent = braceIndent + IndentUnit;
|
|
string sampleName = BuildSampleName(method.DeclaringTypeName, method.MethodName);
|
|
|
|
output.Append(newline);
|
|
output.Append(innerIndent).Append(MarkerBegin).Append(newline);
|
|
output.Append(innerIndent).Append("UnityEngine.Profiling.Profiler.BeginSample(\"").Append(sampleName).Append("\");").Append(newline);
|
|
output.Append(innerIndent).Append("try").Append(newline);
|
|
output.Append(innerIndent).Append("{").Append(newline);
|
|
|
|
int bodyStart = method.OpenBraceIndex + 1;
|
|
output.Append(content, bodyStart, method.CloseBraceIndex - bodyStart);
|
|
|
|
output.Append(newline);
|
|
output.Append(innerIndent).Append("}").Append(newline);
|
|
output.Append(innerIndent).Append("finally").Append(newline);
|
|
output.Append(innerIndent).Append("{").Append(newline);
|
|
output.Append(innerIndent).Append(IndentUnit).Append("UnityEngine.Profiling.Profiler.EndSample();").Append(newline);
|
|
output.Append(innerIndent).Append("}").Append(newline);
|
|
output.Append(innerIndent).Append(MarkerEnd).Append(newline);
|
|
output.Append(braceIndent);
|
|
|
|
cursor = method.CloseBraceIndex;
|
|
changed = true;
|
|
}
|
|
|
|
if (!changed)
|
|
{
|
|
return content;
|
|
}
|
|
|
|
if (cursor < content.Length)
|
|
{
|
|
output.Append(content, cursor, content.Length - cursor);
|
|
}
|
|
|
|
return output.ToString();
|
|
}
|
|
|
|
private static string BuildSampleName(string declaringTypeName, string methodName)
|
|
{
|
|
string safeTypeName = string.IsNullOrEmpty(declaringTypeName) ? "UnknownType" : declaringTypeName;
|
|
var sample = $"TH1Auto/{safeTypeName}.{methodName}";
|
|
return sample.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
|
}
|
|
|
|
private static bool TryResolveTypeName(string codeOnly, int braceIndex, out string typeName)
|
|
{
|
|
typeName = null;
|
|
int headerStart = FindHeaderStart(codeOnly, braceIndex);
|
|
int length = braceIndex - headerStart;
|
|
if (length <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string header = codeOnly.Substring(headerStart, length);
|
|
var match = Regex.Match(
|
|
header,
|
|
@"\b(?:class|struct|interface|enum)\s+(@?[A-Za-z_][A-Za-z0-9_]*)|\brecord(?:\s+(?:class|struct))?\s+(@?[A-Za-z_][A-Za-z0-9_]*)");
|
|
if (!match.Success)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
typeName = match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
|
|
if (!string.IsNullOrEmpty(typeName) && typeName[0] == '@')
|
|
{
|
|
typeName = typeName.Substring(1);
|
|
}
|
|
|
|
return !string.IsNullOrEmpty(typeName);
|
|
}
|
|
|
|
private static string BuildDeclaringTypeName(List<string> typeNameStack)
|
|
{
|
|
if (typeNameStack == null || typeNameStack.Count == 0)
|
|
{
|
|
return "UnknownType";
|
|
}
|
|
|
|
if (typeNameStack.Count == 1)
|
|
{
|
|
return typeNameStack[0];
|
|
}
|
|
|
|
var builder = new StringBuilder(typeNameStack[0]);
|
|
for (int i = 1; i < typeNameStack.Count; i++)
|
|
{
|
|
builder.Append('.').Append(typeNameStack[i]);
|
|
}
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
private static bool LooksLikeTypeBlock(string codeOnly, int braceIndex)
|
|
{
|
|
int headerStart = FindHeaderStart(codeOnly, braceIndex);
|
|
int length = braceIndex - headerStart;
|
|
if (length <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string header = codeOnly.Substring(headerStart, length);
|
|
if (Regex.IsMatch(header, @"\bnamespace\b"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Regex.IsMatch(header, @"\b(class|struct|interface|enum|record)\b");
|
|
}
|
|
|
|
private static bool TryResolveMethodName(string codeOnly, int braceIndex, out string methodName)
|
|
{
|
|
methodName = null;
|
|
|
|
int segmentStart = FindHeaderStart(codeOnly, braceIndex);
|
|
int closeParen = FindPreviousChar(codeOnly, ')', braceIndex - 1);
|
|
while (closeParen >= 0 && closeParen >= segmentStart)
|
|
{
|
|
int openParen = FindMatchingOpenParen(codeOnly, closeParen);
|
|
if (openParen < 0 || openParen < segmentStart)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int nameEnd = PreviousNonWhitespace(codeOnly, openParen - 1);
|
|
if (nameEnd < 0)
|
|
{
|
|
return false;
|
|
}
|
|
nameEnd = SkipGenericTypeParameterListBackward(codeOnly, nameEnd);
|
|
if (nameEnd < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int nameStart = nameEnd;
|
|
while (nameStart >= 0 && IsIdentifierChar(codeOnly[nameStart]))
|
|
{
|
|
nameStart--;
|
|
}
|
|
|
|
nameStart++;
|
|
if (nameStart > nameEnd)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
methodName = codeOnly.Substring(nameStart, nameEnd - nameStart + 1);
|
|
if (string.IsNullOrEmpty(methodName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (methodName[0] == '@')
|
|
{
|
|
methodName = methodName.Substring(1);
|
|
}
|
|
|
|
if (methodName == "base" || methodName == "this")
|
|
{
|
|
closeParen = FindPreviousChar(codeOnly, ')', openParen - 1);
|
|
continue;
|
|
}
|
|
|
|
var previousIdentifier = GetPreviousIdentifier(codeOnly, nameStart - 1);
|
|
if (previousIdentifier == "new")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (NonMethodKeywords.Contains(methodName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int checkLength = braceIndex - segmentStart;
|
|
if (checkLength <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string header = codeOnly.Substring(segmentStart, checkLength);
|
|
if (header.Contains("=>") || header.Contains(";"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasAssignmentOutsideParameterList(header))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Regex.IsMatch(header, @"\b(return|throw)\b"))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool IsMethodAlreadyInstrumented(string content, MethodBlock method)
|
|
{
|
|
int index = method.OpenBraceIndex + 1;
|
|
while (index < method.CloseBraceIndex && char.IsWhiteSpace(content[index]))
|
|
{
|
|
index++;
|
|
}
|
|
|
|
if (index + MarkerBegin.Length > method.CloseBraceIndex)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return string.Compare(content, index, MarkerBegin, 0, MarkerBegin.Length, StringComparison.Ordinal) == 0;
|
|
}
|
|
|
|
// Support generic method declarations like Foo<T>(...) when resolving method names.
|
|
private static int SkipGenericTypeParameterListBackward(string text, int endIndex)
|
|
{
|
|
if (endIndex < 0)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
if (text[endIndex] != '>')
|
|
{
|
|
return endIndex;
|
|
}
|
|
|
|
int depth = 0;
|
|
for (int i = endIndex; i >= 0; i--)
|
|
{
|
|
char c = text[i];
|
|
if (c == '>')
|
|
{
|
|
depth++;
|
|
continue;
|
|
}
|
|
|
|
if (c != '<')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
return PreviousNonWhitespace(text, i - 1);
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static string GetPreviousIdentifier(string text, int index)
|
|
{
|
|
int i = index;
|
|
while (i >= 0 && char.IsWhiteSpace(text[i]))
|
|
{
|
|
i--;
|
|
}
|
|
|
|
if (i < 0 || !IsIdentifierChar(text[i]))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int end = i;
|
|
while (i >= 0 && IsIdentifierChar(text[i]))
|
|
{
|
|
i--;
|
|
}
|
|
|
|
int start = i + 1;
|
|
var token = text.Substring(start, end - start + 1);
|
|
return token.StartsWith("@", StringComparison.Ordinal) ? token.Substring(1) : token;
|
|
}
|
|
|
|
private static bool HasAssignmentOutsideParameterList(string header)
|
|
{
|
|
int parenDepth = 0;
|
|
for (int i = 0; i < header.Length; i++)
|
|
{
|
|
char c = header[i];
|
|
if (c == '(')
|
|
{
|
|
parenDepth++;
|
|
continue;
|
|
}
|
|
|
|
if (c == ')')
|
|
{
|
|
parenDepth = Math.Max(0, parenDepth - 1);
|
|
continue;
|
|
}
|
|
|
|
if (c != '=' || parenDepth != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
char prev = i > 0 ? header[i - 1] : '\0';
|
|
char next = i + 1 < header.Length ? header[i + 1] : '\0';
|
|
if (prev == '=' || prev == '!' || prev == '<' || prev == '>' || next == '=')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string BuildCodeOnlyBuffer(string content)
|
|
{
|
|
var chars = content.ToCharArray();
|
|
bool inSingleLineComment = false;
|
|
bool inMultiLineComment = false;
|
|
bool inString = false;
|
|
bool inChar = false;
|
|
bool verbatimString = false;
|
|
|
|
for (int i = 0; i < chars.Length; i++)
|
|
{
|
|
char c = chars[i];
|
|
char next = i + 1 < chars.Length ? chars[i + 1] : '\0';
|
|
|
|
if (inSingleLineComment)
|
|
{
|
|
if (c != '\r' && c != '\n')
|
|
{
|
|
chars[i] = ' ';
|
|
}
|
|
else
|
|
{
|
|
inSingleLineComment = false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (inMultiLineComment)
|
|
{
|
|
chars[i] = ' ';
|
|
if (c == '*' && next == '/')
|
|
{
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
inMultiLineComment = false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (inString)
|
|
{
|
|
chars[i] = ' ';
|
|
if (verbatimString)
|
|
{
|
|
if (c == '"' && next == '"')
|
|
{
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
}
|
|
else if (c == '"')
|
|
{
|
|
inString = false;
|
|
verbatimString = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (c == '\\' && i + 1 < chars.Length)
|
|
{
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
}
|
|
else if (c == '"')
|
|
{
|
|
inString = false;
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (inChar)
|
|
{
|
|
chars[i] = ' ';
|
|
if (c == '\\' && i + 1 < chars.Length)
|
|
{
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
}
|
|
else if (c == '\'')
|
|
{
|
|
inChar = false;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (c == '/' && next == '/')
|
|
{
|
|
chars[i] = ' ';
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
inSingleLineComment = true;
|
|
continue;
|
|
}
|
|
|
|
if (c == '/' && next == '*')
|
|
{
|
|
chars[i] = ' ';
|
|
chars[i + 1] = ' ';
|
|
i++;
|
|
inMultiLineComment = true;
|
|
continue;
|
|
}
|
|
|
|
if (c == '"')
|
|
{
|
|
chars[i] = ' ';
|
|
inString = true;
|
|
verbatimString =
|
|
(i > 0 && chars[i - 1] == '@') ||
|
|
(i > 1 && chars[i - 1] == '$' && chars[i - 2] == '@') ||
|
|
(i > 1 && chars[i - 1] == '@' && chars[i - 2] == '$');
|
|
continue;
|
|
}
|
|
|
|
if (c == '\'')
|
|
{
|
|
chars[i] = ' ';
|
|
inChar = true;
|
|
}
|
|
}
|
|
|
|
return new string(chars);
|
|
}
|
|
|
|
private static int FindMatchingOpenParen(string codeOnly, int closeParenIndex)
|
|
{
|
|
int depth = 0;
|
|
for (int i = closeParenIndex; i >= 0; i--)
|
|
{
|
|
char c = codeOnly[i];
|
|
if (c == ')')
|
|
{
|
|
depth++;
|
|
continue;
|
|
}
|
|
|
|
if (c != '(')
|
|
{
|
|
continue;
|
|
}
|
|
|
|
depth--;
|
|
if (depth == 0)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static int FindHeaderStart(string codeOnly, int index)
|
|
{
|
|
for (int i = index - 1; i >= 0; i--)
|
|
{
|
|
char c = codeOnly[i];
|
|
if (c == ';' || c == '{' || c == '}')
|
|
{
|
|
return i + 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static int PreviousNonWhitespace(string text, int index)
|
|
{
|
|
for (int i = index; i >= 0; i--)
|
|
{
|
|
if (!char.IsWhiteSpace(text[i]))
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static int FindPreviousChar(string text, char target, int startIndex)
|
|
{
|
|
for (int i = startIndex; i >= 0; i--)
|
|
{
|
|
char c = text[i];
|
|
if (c == target)
|
|
{
|
|
return i;
|
|
}
|
|
|
|
if (c == ';' || c == '{' || c == '}')
|
|
{
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
private static bool IsIdentifierChar(char c)
|
|
{
|
|
return char.IsLetterOrDigit(c) || c == '_' || c == '@';
|
|
}
|
|
|
|
private static string DetectNewLine(string content)
|
|
{
|
|
return content.Contains("\r\n") ? "\r\n" : "\n";
|
|
}
|
|
|
|
private static string GetLineIndent(string content, int index)
|
|
{
|
|
int lineStart = content.LastIndexOf('\n', Mathf.Clamp(index, 0, Math.Max(0, content.Length - 1)));
|
|
lineStart = lineStart < 0 ? 0 : lineStart + 1;
|
|
int i = lineStart;
|
|
while (i < content.Length && (content[i] == ' ' || content[i] == '\t'))
|
|
{
|
|
i++;
|
|
}
|
|
|
|
return content.Substring(lineStart, i - lineStart);
|
|
}
|
|
|
|
private static string NormalizeFolderPath(string folder)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(folder))
|
|
{
|
|
return AssetRoot;
|
|
}
|
|
|
|
var normalized = folder.Replace("\\", "/").Trim();
|
|
if (!normalized.StartsWith("Assets", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
normalized = "Assets/" + normalized.TrimStart('/');
|
|
}
|
|
|
|
return normalized.TrimEnd('/');
|
|
}
|
|
|
|
private static string AssetPathToAbsolutePath(string assetPath)
|
|
{
|
|
return Path.GetFullPath(Path.Combine(Application.dataPath, "..", assetPath));
|
|
}
|
|
|
|
private static string AbsolutePathToAssetPath(string absolutePath)
|
|
{
|
|
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
|
var full = Path.GetFullPath(absolutePath);
|
|
var relative = full.Substring(projectRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
|
return relative.Replace("\\", "/");
|
|
}
|
|
}
|