Forge
csharpdeeb25a2
1using DotnetDebug.Core;
2
3namespace CascadeIDE.Services;
4
5/// <summary>
6/// Операции IDE поверх <see cref="BreakpointsStorage"/> (тот же JSON, что у dotnet-debug-mcp).
7/// «Bundled sample» — встроенная <c>samples/DebugTarget</c> для договорённостей MCP/доков; это не output «текущего» стартового проекта
8/// (тот делается через <see cref="MsBuildDebugTargetResolver"/> и F5).
9/// </summary>
10public static class BreakpointsFileService
11{
12 public const string FileName = BreakpointsStorage.FileName;
13
14 /// <summary>Отн. путь от корня monorepo к DLL встроенной sample-цели (после <c>dotnet build</c> в <c>samples/DebugTarget</c>).</summary>
15 public static readonly string BundledSampleDebugTargetDllRelativeToRepoRoot =
16 Path.Combine("samples", "DebugTarget", "bin", "Debug", "net10.0", "DebugTarget.dll");
17
18 /// <summary>Отн. путь к той же DLL, когда workspace — только каталог проекта <c>…/samples/DebugTarget</c>.</summary>
19 private static readonly string BundledSampleDebugTargetDllRelativeToProjectDir = Path.Combine("bin", "Debug", "net10.0", "DebugTarget.dll");
20
21 public static string GetFilePath(string workspacePath) => BreakpointsStorage.GetStorageFilePath(workspacePath);
22
23 /// <summary>Корень workspace (каталог с .sln/.slnx или переданный каталог).</summary>
24 public static string GetWorkspaceRoot(string workspacePath)
25 {
26 var dir = CanonicalFilePath.Normalize(workspacePath.Trim());
27 if (File.Exists(dir))
28 dir = Path.GetDirectoryName(dir) ?? dir;
29 return dir;
30 }
31
32 /// <summary>
33 /// Полный путь к DLL <b>встроенной</b> sample-цели <c>samples/DebugTarget</c>: ключ в JSON для MCP <c>set_breakpoint</c> и старт папки в диалоге выбора;
34 /// не путать с output произвольного стартового проекта. Старт отладки F5/launch — с выбранным <c>target_path</c>.
35 /// </summary>
36 public static string GetBundledSampleDebugTargetDllPath(string workspacePath)
37 {
38 var root = GetWorkspaceRoot(workspacePath);
39 if (IsBundledDebugTargetSampleProjectRoot(root))
40 return CanonicalFilePath.Normalize(Path.Combine(root, BundledSampleDebugTargetDllRelativeToProjectDir));
41 return CanonicalFilePath.Normalize(Path.Combine(root, BundledSampleDebugTargetDllRelativeToRepoRoot));
42 }
43
44 /// <summary>Корень workspace = каталог sample-проекта <c>DebugTarget</c> (не весь monorepo).</summary>
45 private static bool IsBundledDebugTargetSampleProjectRoot(string workspaceRoot)
46 {
47 try
48 {
49 if (!string.Equals(Path.GetFileName(workspaceRoot.TrimEnd(Path.DirectorySeparatorChar)), "DebugTarget", StringComparison.OrdinalIgnoreCase))
50 return false;
51 return File.Exists(Path.Combine(workspaceRoot, "DebugTarget.csproj"));
52 }
53 catch
54 {
55 return false;
56 }
57 }
58
59 private static string? NormalizeEntryPath(string workspacePath, string? entryFile)
60 {
61 if (string.IsNullOrEmpty(entryFile))
62 return null;
63 if (Path.IsPathRooted(entryFile))
64 return CanonicalFilePath.Normalize(entryFile);
65 var wsDir = Path.GetDirectoryName(GetFilePath(workspacePath));
66 return string.IsNullOrEmpty(wsDir) ? null : CanonicalFilePath.Normalize(Path.Combine(wsDir, entryFile));
67 }
68
69 /// <summary>Номера строк с брейкпоинтами из файла для указанного файла (по всем targets).</summary>
70 public static IReadOnlyList<int> GetLinesForFile(string workspacePath, string? filePath)
71 {
72 if (string.IsNullOrEmpty(filePath))
73 return [];
74 var normalized = CanonicalFilePath.Normalize(filePath);
75 var model = BreakpointsStorage.Load(workspacePath);
76 var lines = new HashSet<int>();
77 foreach (var list in model.Targets.Values)
78 {
79 foreach (var entry in list)
80 {
81 var entryPath = NormalizeEntryPath(workspacePath, entry.File);
82 if (entryPath != null && CanonicalFilePath.Equals(entryPath, normalized))
83 lines.Add(entry.Line);
84 }
85 }
86 return lines.OrderBy(static l => l).ToList();
87 }
88
89 /// <summary>Выбрать target: сначала bundled sample (если есть ключ), иначе первый под workspace, иначе первый в списке.</summary>
90 public static string? GetPreferredTargetKey(string workspacePath)
91 {
92 var ws = GetWorkspaceRoot(workspacePath);
93 var model = BreakpointsStorage.Load(workspacePath);
94 var preferred = CanonicalFilePath.Normalize(GetBundledSampleDebugTargetDllPath(workspacePath));
95 if (model.Targets.ContainsKey(preferred))
96 return preferred;
97 var key = model.Targets.Keys.FirstOrDefault(k => k.StartsWith(ws, StringComparison.OrdinalIgnoreCase));
98 return key ?? model.Targets.Keys.FirstOrDefault();
99 }
100
101 /// <summary>Записать брейкпоинт в JSON для bundled sample-цели (<see cref="GetBundledSampleDebugTargetDllPath"/>), в т.ч. из MCP <c>set_breakpoint</c>.</summary>
102 public static void SetBreakpointForBundledSampleTarget(string workspacePath, string filePath, int line, string? condition = null)
103 {
104 if (line < 1 || string.IsNullOrEmpty(filePath))
105 return;
106 var path = CanonicalFilePath.Normalize(filePath);
107 var target = CanonicalFilePath.Normalize(GetBundledSampleDebugTargetDllPath(workspacePath));
108 var list = BreakpointsStorage.GetBreakpoints(workspacePath, target).ToList();
109 list.RemoveAll(e =>
110 {
111 var ep = NormalizeEntryPath(workspacePath, e.File);
112 return ep != null && CanonicalFilePath.Equals(ep, path) && e.Line == line;
113 });
114 list.Add(new BreakpointsStorage.BreakpointEntry(path, line, condition));
115 BreakpointsStorage.SetBreakpoints(workspacePath, target, list);
116 }
117
118 /// <summary>Удалить брейкпоинт из JSON для той же bundled sample-цели.</summary>
119 public static void RemoveBreakpointForBundledSampleTarget(string workspacePath, string filePath, int line)
120 {
121 if (line < 1 || string.IsNullOrEmpty(filePath))
122 return;
123 var path = CanonicalFilePath.Normalize(filePath);
124 var target = CanonicalFilePath.Normalize(GetBundledSampleDebugTargetDllPath(workspacePath));
125 var list = BreakpointsStorage.GetBreakpoints(workspacePath, target).ToList();
126 list.RemoveAll(e =>
127 {
128 var ep = NormalizeEntryPath(workspacePath, e.File);
129 return ep != null && CanonicalFilePath.Equals(ep, path) && e.Line == line;
130 });
131 BreakpointsStorage.SetBreakpoints(workspacePath, target, list);
132 }
133
134 /// <summary>Переключить брейкпоинт в файле: если есть — удалить, иначе добавить в предпочитаемый target.</summary>
135 public static void ToggleBreakpoint(string workspacePath, string filePath, int line)
136 {
137 if (line < 1) return;
138 var path = CanonicalFilePath.Normalize(filePath);
139 var model = BreakpointsStorage.Load(workspacePath);
140 var targetKey = GetPreferredTargetKey(workspacePath);
141 if (string.IsNullOrEmpty(targetKey))
142 {
143 targetKey = CanonicalFilePath.Normalize(GetBundledSampleDebugTargetDllPath(workspacePath));
144 if (!model.Targets.ContainsKey(targetKey))
145 model.Targets[targetKey] = [];
146 }
147 if (!model.Targets.TryGetValue(targetKey, out var list))
148 {
149 list = [];
150 model.Targets[targetKey] = list;
151 }
152 var fileNorm = path;
153 var removed = list.RemoveAll(e =>
154 {
155 var ep = NormalizeEntryPath(workspacePath, e.File);
156 return ep != null && CanonicalFilePath.Equals(ep, fileNorm) && e.Line == line;
157 });
158 if (removed == 0)
159 list.Add(new BreakpointsStorage.BreakpointEntry(path, line));
160 BreakpointsStorage.Save(workspacePath, model);
161 }
162}
163
View only · write via MCP/CIDE