Forge
csharpdeeb25a2
1using Avalonia;
2using Avalonia.Controls;
3using Avalonia.Layout;
4using Avalonia.Media;
5using Avalonia.Platform.Storage;
6using Avalonia.Threading;
7using CascadeIDE.Services;
8using CommunityToolkit.Mvvm.Input;
9using System.Threading;
10
11namespace CascadeIDE.Views;
12
13public partial class MainWindow
14{
15 private async Task<string?> ShowOpenThemeFileDialogAsync()
16 {
17 var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
18 {
19 Title = "Открыть файл темы",
20 AllowMultiple = false,
21 FileTypeFilter = [new FilePickerFileType("JSON") { Patterns = ["*.json"] }]
22 });
23 if (files.Count == 0)
24 return null;
25 return files[0].TryGetLocalPath() ?? files[0].Path.LocalPath;
26 }
27
28 private async Task<string?> ShowPickDebugTargetAsync()
29 {
30 var options = new FilePickerOpenOptions
31 {
32 Title = "Отладка: выбери .dll или .exe (цель не подставляется автоматически)",
33 AllowMultiple = false,
34 FileTypeFilter =
35 [
36 new FilePickerFileType("Сборка .NET") { Patterns = ["*.dll", "*.exe"] }
37 ]
38 };
39 if (DataContext is ViewModels.MainWindowViewModel vm && !string.IsNullOrEmpty(vm.Workspace.SolutionPath))
40 {
41 var bundledSampleDll = BreakpointsFileService.GetBundledSampleDebugTargetDllPath(vm.Workspace.SolutionPath);
42 var binDir = Path.GetDirectoryName(bundledSampleDll);
43 if (!string.IsNullOrEmpty(binDir) && Directory.Exists(binDir))
44 {
45 var folder = await StorageProvider.TryGetFolderFromPathAsync(binDir).ConfigureAwait(true);
46 if (folder != null)
47 options.SuggestedStartLocation = folder;
48 }
49 }
50
51 var files = await StorageProvider.OpenFilePickerAsync(options);
52 if (files.Count == 0)
53 return null;
54 return files[0].TryGetLocalPath() ?? files[0].Path.LocalPath;
55 }
56
57 private Task<int?> ShowAttachProcessIdAsync() => AttachProcessPidDialog.ShowAsync(this);
58
59 private async Task ShowInfoDialogAsync(string title, string message)
60 {
61 var dialog = new Window
62 {
63 Title = string.IsNullOrWhiteSpace(title) ? "Сообщение" : title,
64 Width = 480,
65 MinHeight = 140,
66 CanResize = true,
67 WindowStartupLocation = WindowStartupLocation.CenterOwner
68 };
69
70 var ok = new Button { Content = "OK", MinWidth = 90, HorizontalAlignment = HorizontalAlignment.Right };
71 ok.Click += (_, _) => dialog.Close();
72
73 dialog.Content = new StackPanel
74 {
75 Margin = new Thickness(16),
76 Spacing = 14,
77 Children =
78 {
79 new TextBlock
80 {
81 Text = string.IsNullOrWhiteSpace(message) ? "" : message,
82 TextWrapping = TextWrapping.Wrap
83 },
84 ok
85 }
86 };
87
88 await dialog.ShowDialog(this);
89 }
90
91 private async Task ShowCreateNewSolutionDialogAsync()
92 {
93 if (DataContext is not ViewModels.MainWindowViewModel vm)
94 return;
95
96 var options = new FilePickerSaveOptions
97 {
98 Title = "Создать новое решение",
99 DefaultExtension = "sln",
100 FileTypeChoices =
101 [
102 new FilePickerFileType("Файл решения (.sln)") { Patterns = ["*.sln"] }
103 ],
104 SuggestedFileName = "MySolution.sln"
105 };
106
107 if (!string.IsNullOrEmpty(vm.Workspace.SolutionPath))
108 {
109 var start = DialogStartDirectoryFromWorkspacePath(vm.Workspace.SolutionPath);
110 if (!string.IsNullOrEmpty(start))
111 {
112 var folder = await StorageProvider.TryGetFolderFromPathAsync(start).ConfigureAwait(true);
113 if (folder is not null)
114 options.SuggestedStartLocation = folder;
115 }
116 }
117
118 var fileOut = await StorageProvider.SaveFilePickerAsync(options).ConfigureAwait(true);
119 if (fileOut is null)
120 return;
121
122 var raw = fileOut.TryGetLocalPath() ?? fileOut.Path.LocalPath;
123 if (string.IsNullOrWhiteSpace(raw))
124 return;
125
126 var fullPath = CanonicalFilePath.Normalize(raw.Trim());
127 if (!fullPath.EndsWith(".sln", StringComparison.OrdinalIgnoreCase))
128 fullPath += ".sln";
129
130 var result = await vm.TryCreateBlankSolutionAtPathAsync(fullPath, CancellationToken.None).ConfigureAwait(true);
131 if (!result.Ok)
132 {
133 if (vm.RequestShowInfoAsync is not null)
134 await vm.RequestShowInfoAsync("Новое решение", result.ErrorMessage ?? "Не удалось создать решение.").ConfigureAwait(true);
135 return;
136 }
137
138 vm.LoadSolution(result.SolutionPath!);
139 }
140
141 private async Task ShowOpenSolutionDialogAsync()
142 {
143 var storageProvider = StorageProvider;
144 var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
145 {
146 Title = "Открыть решение или проект",
147 AllowMultiple = false,
148 FileTypeFilter =
149 [
150 new FilePickerFileType("Решение / проект") { Patterns = ["*.slnx", "*.sln", "*.slnf", "*.csproj", "*.fsproj"] }
151 ]
152 });
153 if (files.Count > 0 && DataContext is ViewModels.MainWindowViewModel vm)
154 {
155 var path = files[0].TryGetLocalPath() ?? files[0].Path.LocalPath;
156 vm.LoadSolution(path);
157 }
158 }
159
160 private async Task ShowOpenFolderDialogAsync()
161 {
162 var options = new FolderPickerOpenOptions
163 {
164 Title = "Открыть папку как workspace",
165 AllowMultiple = false
166 };
167 if (DataContext is ViewModels.MainWindowViewModel vmStart && !string.IsNullOrEmpty(vmStart.Workspace.SolutionPath))
168 {
169 var start = DialogStartDirectoryFromWorkspacePath(vmStart.Workspace.SolutionPath);
170 if (!string.IsNullOrEmpty(start))
171 {
172 var folder = await StorageProvider.TryGetFolderFromPathAsync(start).ConfigureAwait(true);
173 if (folder is not null)
174 options.SuggestedStartLocation = folder;
175 }
176 }
177
178 var folders = await StorageProvider.OpenFolderPickerAsync(options);
179 if (folders.Count == 0 || DataContext is not ViewModels.MainWindowViewModel vm)
180 return;
181 var path = folders[0].TryGetLocalPath() ?? folders[0].Path.LocalPath;
182 if (string.IsNullOrEmpty(path))
183 return;
184 vm.LoadSolution(path);
185 }
186
187 /// <summary>Каталог для старта диалога: рядом с .sln или внутри открытой папки.</summary>
188 private static string? DialogStartDirectoryFromWorkspacePath(string workspacePath)
189 {
190 if (string.IsNullOrWhiteSpace(workspacePath))
191 return null;
192 try
193 {
194 var p = CanonicalFilePath.Normalize(workspacePath.Trim());
195 if (File.Exists(p))
196 return Path.GetDirectoryName(p);
197 if (Directory.Exists(p))
198 return p;
199 }
200 catch
201 {
202 // ignore
203 }
204
205 return null;
206 }
207
208 private async Task ShowOpenFileDialogAsync()
209 {
210 var options = new FilePickerOpenOptions
211 {
212 Title = "Открыть файл",
213 AllowMultiple = false,
214 FileTypeFilter =
215 [
216 new FilePickerFileType("Все файлы") { Patterns = ["*"] },
217 new FilePickerFileType("Текст и код")
218 {
219 Patterns =
220 [
221 "*.cs", "*.xaml", "*.axaml", "*.json", "*.toml", "*.md", "*.xml", "*.txt",
222 "*.csproj", "*.sln", "*.slnx", "*.css", "*.js", "*.ts"
223 ]
224 }
225 ]
226 };
227 if (DataContext is ViewModels.MainWindowViewModel vmStart && !string.IsNullOrEmpty(vmStart.Workspace.SolutionPath))
228 {
229 var dir = DialogStartDirectoryFromWorkspacePath(vmStart.Workspace.SolutionPath);
230 if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
231 {
232 var folder = await StorageProvider.TryGetFolderFromPathAsync(dir).ConfigureAwait(true);
233 if (folder != null)
234 options.SuggestedStartLocation = folder;
235 }
236 }
237
238 var files = await StorageProvider.OpenFilePickerAsync(options);
239 if (files.Count == 0 || DataContext is not ViewModels.MainWindowViewModel vm)
240 return;
241 var path = files[0].TryGetLocalPath() ?? files[0].Path.LocalPath;
242 if (string.IsNullOrEmpty(path) || !File.Exists(path))
243 return;
244 var normalized = CanonicalFilePath.Normalize(path);
245 vm.Documents.OpenOrActivateDocument(normalized);
246 }
247
248 private async Task<string?> ShowSaveExpandedMarkdownDialogAsync(string? currentMarkdownPath)
249 {
250 var options = new FilePickerSaveOptions
251 {
252 Title = "Export expanded Markdown",
253 DefaultExtension = "md",
254 FileTypeChoices =
255 [
256 new FilePickerFileType("Markdown") { Patterns = ["*.md"] },
257 new FilePickerFileType("Все файлы") { Patterns = ["*"] }
258 ]
259 };
260
261 if (!string.IsNullOrWhiteSpace(currentMarkdownPath))
262 {
263 try
264 {
265 var full = CanonicalFilePath.Normalize(currentMarkdownPath);
266 var dir = Path.GetDirectoryName(full);
267 var file = Path.GetFileNameWithoutExtension(full);
268 options.SuggestedFileName = string.IsNullOrWhiteSpace(file) ? "export.expanded.md" : $"{file}.expanded.md";
269 if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir))
270 {
271 var folder = await StorageProvider.TryGetFolderFromPathAsync(dir).ConfigureAwait(true);
272 if (folder is not null)
273 options.SuggestedStartLocation = folder;
274 }
275 }
276 catch
277 {
278 // ignore invalid path
279 }
280 }
281
282 var fileOut = await StorageProvider.SaveFilePickerAsync(options).ConfigureAwait(true);
283 if (fileOut is null)
284 return null;
285 return fileOut.TryGetLocalPath() ?? fileOut.Path.LocalPath;
286 }
287
288 private Task<string> ShowConfirmationDialogAsync(string message, CancellationToken cancellationToken)
289 {
290 var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
291 CancellationTokenRegistration ctr = default;
292 if (cancellationToken.CanBeCanceled)
293 {
294 ctr = cancellationToken.Register(() => tcs.TrySetResult(Services.ConfirmationResponses.Cancel));
295 }
296
297 UiScheduler.Default.Post(async () =>
298 {
299 if (cancellationToken.IsCancellationRequested)
300 {
301 tcs.TrySetResult(Services.ConfirmationResponses.Cancel);
302 return;
303 }
304
305 try
306 {
307 var result = await ShowConfirmationDialogOnUiThreadAsync(message).ConfigureAwait(true);
308 tcs.TrySetResult(result);
309 }
310 catch
311 {
312 tcs.TrySetResult(Services.ConfirmationResponses.Cancel);
313 }
314 finally
315 {
316 ctr.Dispose();
317 }
318 });
319
320 return tcs.Task;
321 }
322
323 private async Task<string> ShowConfirmationDialogOnUiThreadAsync(string message)
324 {
325 var dialog = new Window
326 {
327 Title = "Подтверждение",
328 Width = 460,
329 Height = 190,
330 CanResize = false,
331 WindowStartupLocation = WindowStartupLocation.CenterOwner
332 };
333
334 var text = string.IsNullOrWhiteSpace(message) ? "Подтвердить действие?" : message;
335 var result = Services.ConfirmationResponses.Cancel;
336
337 var okButton = new Button { Content = "OK", MinWidth = 90 };
338 var cancelButton = new Button { Content = "Cancel", MinWidth = 90 };
339
340 okButton.Click += (_, _) =>
341 {
342 result = Services.ConfirmationResponses.Ok;
343 dialog.Close();
344 };
345 cancelButton.Click += (_, _) =>
346 {
347 result = Services.ConfirmationResponses.Cancel;
348 dialog.Close();
349 };
350
351 dialog.Content = new StackPanel
352 {
353 Margin = new Thickness(16),
354 Spacing = 14,
355 Children =
356 {
357 new TextBlock
358 {
359 Text = text,
360 TextWrapping = TextWrapping.Wrap
361 },
362 new StackPanel
363 {
364 Orientation = Orientation.Horizontal,
365 HorizontalAlignment = HorizontalAlignment.Right,
366 Spacing = 10,
367 Children = { okButton, cancelButton }
368 }
369 }
370 };
371
372 await dialog.ShowDialog(this);
373
374 return result;
375 }
376
377 private async void ShowAbout()
378 {
379 var w = new Window { Title = "О программе", Width = 400, Height = 180 };
380 var okCommand = new RelayCommand(() => w.Close());
381 w.Content = new StackPanel
382 {
383 Margin = new Avalonia.Thickness(16),
384 Spacing = 12,
385 Children =
386 {
387 new TextBlock { Text = "CascadeIDE", FontWeight = FontWeight.SemiBold, FontSize = 16 },
388 new TextBlock
389 {
390 Text = "Тонкий клиент для управления агентом (MCP). Файл, вид, терминал, обозреватель решения.",
391 TextWrapping = TextWrapping.Wrap
392 },
393 new Button { Content = "OK", HorizontalAlignment = HorizontalAlignment.Right, Command = okCommand }
394 }
395 };
396 await w.ShowDialog(this);
397 }
398
399 private void ShowSettingsWindow()
400 {
401 if (DataContext is not ViewModels.MainWindowViewModel vm)
402 return;
403 var w = new SettingsWindow { DataContext = vm };
404 w.Show(this);
405 }
406
407 private async Task ShowInstallModelDialogAsync(ViewModels.MainWindowViewModel mainVm)
408 {
409 mainVm.SelectedOllamaModel = mainVm.LastSelectedRealModel ?? mainVm.OllamaModels.FirstOrDefault();
410 var dialog = new InstallModelDialog();
411 dialog.DataContext = new ViewModels.InstallModelDialogViewModel(
412 new Services.OllamaService(),
413 () => dialog.Close());
414 dialog.Closed += (_, _) => _ = mainVm.RefreshOllamaAsync();
415 await dialog.ShowDialog(this);
416 }
417}
418
View only · write via MCP/CIDE