diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs index 0949a17..5a9a307 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/BatchRunner.cs @@ -51,9 +51,9 @@ public static async Task RunAsync( CancellationToken cancellationToken = default) { List outcomes = [with(targets.Count)]; - for (var i = 0; i < targets.Count; i++) + for (int i = 0; i < targets.Count; i++) { - (var target, var name) = targets[i]; + (T? target, string? name) = targets[i]; if (cancellationToken.IsCancellationRequested) { outcomes.Add(new(name, false, "已取消,这个目标没有执行。")); @@ -73,7 +73,7 @@ public static async Task RunAsync( { // 连接没了就没有必要继续戳后面的目标 —— 它们只会拿到同一条错误。 outcomes.Add(new(name, false, ex.Message)); - for (var j = i + 1; j < targets.Count; j++) + for (int j = i + 1; j < targets.Count; j++) { outcomes.Add(new(targets[j].Name, false, "连接已断开,这个目标没有执行。")); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs index 5c53036..3493241 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeCli.cs @@ -41,14 +41,14 @@ public sealed record ComposeProject(string Name, string Status, string ConfigFil private int ParseCount(string keyword) { - var at = Status.IndexOf(keyword, StringComparison.OrdinalIgnoreCase); + int at = Status.IndexOf(keyword, StringComparison.OrdinalIgnoreCase); if (at < 0) { return 0; } - var open = Status.IndexOf('(', at); - var close = open > 0 ? Status.IndexOf(')', open) : -1; - return close > open && int.TryParse(Status.AsSpan(open + 1, close - open - 1), out var n) ? n : 0; + int open = Status.IndexOf('(', at); + int close = open > 0 ? Status.IndexOf(')', open) : -1; + return close > open && int.TryParse(Status.AsSpan(open + 1, close - open - 1), out int n) ? n : 0; } } @@ -88,11 +88,11 @@ public string PortsText { return "—"; } - var parts = Publishers + IEnumerable parts = Publishers .Where(p => p.PublishedPort > 0) .Select(p => $"{p.PublishedPort}→{p.TargetPort}") .Distinct(); - var text = string.Join(", ", parts); + string text = string.Join(", ", parts); return text.Length == 0 ? "—" : text; } } @@ -143,7 +143,7 @@ public sealed class ComposeCli(IComposeHost host) /// public async Task ListProjectsAsync(CancellationToken cancellationToken = default) { - var result = await host + ExecResult result = await host .RunAsync(["compose", "ls", "--all", "--format", "json"], TimeSpan.FromSeconds(20), cancellationToken) .ConfigureAwait(false); if (!result.IsSuccess) @@ -152,7 +152,7 @@ public async Task ListProjectsAsync(CancellationToken cancella // 上层的提示,而不是把一条看不懂的错误摔在用户脸上。 return []; } - var entries = DockerJson.TryDeserialize(result.Output.Trim()); + ComposeListEntry[]? entries = DockerJson.TryDeserialize(result.Output.Trim()); return entries is null ? [] : [.. entries.Select(e => new ComposeProject(e.Name ?? "", e.Status ?? "", e.ConfigFiles ?? ""))]; @@ -161,14 +161,14 @@ public async Task ListProjectsAsync(CancellationToken cancella /// 列出项目里的服务。 public async Task ListServicesAsync(ComposeProject project, CancellationToken cancellationToken = default) { - var result = await host + ExecResult result = await host .RunAsync([.. Prefix(project), "ps", "-a", "--format", "json"], TimeSpan.FromSeconds(20), cancellationToken) .ConfigureAwait(false); if (!result.IsSuccess) { return []; } - var text = result.Output.Trim(); + string text = result.Output.Trim(); if (text.Length == 0) { return []; @@ -180,7 +180,7 @@ public async Task ListServicesAsync(ComposeProject project, Ca return DockerJson.TryDeserialize(text) ?? []; } List services = []; - foreach (var line in text.Split('\n', StringSplitOptions.RemoveEmptyEntries)) + foreach (string line in text.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { if (DockerJson.TryDeserialize(line.Trim()) is { } service) { @@ -250,7 +250,7 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken = d { try { - var result = await host + ExecResult result = await host .RunAsync(["compose", "version", "--short"], TimeSpan.FromSeconds(15), cancellationToken) .ConfigureAwait(false); return result.IsSuccess; @@ -275,7 +275,7 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken = d private static List Prefix(ComposeProject project) { List argv = ["compose", "-p", project.Name]; - foreach (var file in project.ConfigFiles.Split(',', StringSplitOptions.RemoveEmptyEntries)) + foreach (string file in project.ConfigFiles.Split(',', StringSplitOptions.RemoveEmptyEntries)) { argv.Add("-f"); argv.Add(file.Trim()); diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs index 92e14e4..204636e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposeHost.cs @@ -57,7 +57,7 @@ public Task RunAsync(IReadOnlyList arguments, TimeSpan timeo public async Task StreamAsync(IReadOnlyList arguments, IProgress output, CancellationToken cancellationToken = default) { - var result = await exec + ExecStreamResult result = await exec .StreamAsync(sessionId, Join(arguments), new ExecStreamOptions { IncludeStandardError = true }, output, cancellationToken) .ConfigureAwait(false); @@ -67,7 +67,7 @@ public async Task StreamAsync(IReadOnlyList arguments, IProgress public async Task ReadFileAsync(string path, CancellationToken cancellationToken = default) { - var bytes = await remoteFs.ReadAllBytesAsync(sessionId, path, 4 << 20, cancellationToken) + byte[] bytes = await remoteFs.ReadAllBytesAsync(sessionId, path, 4 << 20, cancellationToken) .ConfigureAwait(false); return Encoding.UTF8.GetString(bytes); } @@ -168,7 +168,7 @@ private static async Task PumpAsync(IReadOnlyList arguments, Action StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; - foreach (var argument in arguments) + foreach (string argument in arguments) { info.ArgumentList.Add(argument); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs index d45d9c9..af9d59e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/ComposePath.cs @@ -30,16 +30,16 @@ internal static bool IsAbsolute(string path) => /// 去掉最后一段(取所在目录);没有分隔符就原样返回。 internal static string DirectoryOf(string path) { - var trimmed = path.TrimEnd(Separators); - var slash = trimmed.LastIndexOfAny(Separators); + string trimmed = path.TrimEnd(Separators); + int slash = trimmed.LastIndexOfAny(Separators); return slash > 0 ? trimmed[..slash] : trimmed; } /// 取最后一段(目录名 / 文件名)。 internal static string LastSegment(string path) { - var trimmed = path.TrimEnd(Separators); - var slash = trimmed.LastIndexOfAny(Separators); + string trimmed = path.TrimEnd(Separators); + int slash = trimmed.LastIndexOfAny(Separators); return slash >= 0 ? trimmed[(slash + 1)..] : trimmed; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs index ae535b8..0342d9d 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Containers.cs @@ -79,7 +79,7 @@ public Task TopAsync(string id, string psArgs = "-ef", Cance public async Task ChangesAsync(string id, CancellationToken cancellationToken = default) { // 没有任何变更时 daemon 返回 JSON null,不是空数组。 - var body = await GetStringAsync($"/containers/{Uri.EscapeDataString(id)}/changes", cancellationToken).ConfigureAwait(false); + string body = await GetStringAsync($"/containers/{Uri.EscapeDataString(id)}/changes", cancellationToken).ConfigureAwait(false); return DockerJson.TryDeserialize(body) ?? []; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs index 5c61ead..a15b405 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Exec.cs @@ -17,11 +17,11 @@ public string FailureText { get { - foreach (var candidate in new[] { StandardError, StandardOutput }) + foreach (string? candidate in new[] { StandardError, StandardOutput }) { - foreach (var line in candidate.Split('\n')) + foreach (string line in candidate.Split('\n')) { - var trimmed = line.Trim(); + string trimmed = line.Trim(); if (trimmed.Length > 0) { return trimmed; @@ -98,15 +98,15 @@ public sealed partial class DockerClient public async Task StartExecAsync(string containerId, string[] command, bool tty, string? user = null, string? workingDir = null, CancellationToken cancellationToken = default) { - var created = await CreateExecAsync(containerId, command, tty, attachStdin: true, user, workingDir, + ExecCreateResponse created = await CreateExecAsync(containerId, command, tty, attachStdin: true, user, workingDir, cancellationToken).ConfigureAwait(false); // 劫持端点要独占一条流:HttpClient 的连接池不能用,它没法在同一条连接上 // 边写 stdin 边读 stdout。 - var stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + Stream stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); try { - var body = JsonSerializer.Serialize(new { Detach = false, Tty = tty }, DockerJson.Options); - (var status, var reason, var hijacked) = await DockerRawHttp + string body = JsonSerializer.Serialize(new { Detach = false, Tty = tty }, DockerJson.Options); + (int status, string? reason, Stream? hijacked) = await DockerRawHttp .PostAsync(stream, $"/exec/{Uri.EscapeDataString(created.Id)}/start", body, upgrade: true, cancellationToken) .ConfigureAwait(false); return status is not (200 or 101) ? throw new DockerApiException((System.Net.HttpStatusCode)status, $"启动 exec 失败:HTTP {status} {reason}") : new(created.Id, hijacked, this, tty); @@ -129,15 +129,15 @@ public async Task StartExecAsync(string containerId, string[] public async Task ExecCaptureAsync(string containerId, string[] command, string? user = null, string? workingDir = null, CancellationToken cancellationToken = default) { - var created = await CreateExecAsync(containerId, command, tty: false, attachStdin: false, + ExecCreateResponse created = await CreateExecAsync(containerId, command, tty: false, attachStdin: false, user, workingDir, cancellationToken).ConfigureAwait(false); - var stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); + Stream stream = await Transport.ConnectAsync(cancellationToken).ConfigureAwait(false); var stdout = new StringBuilder(); var stderr = new StringBuilder(); await using (stream.ConfigureAwait(false)) { - var body = JsonSerializer.Serialize(new { Detach = false, Tty = false }, DockerJson.Options); - (var status, var reason, var hijacked) = await DockerRawHttp + string body = JsonSerializer.Serialize(new { Detach = false, Tty = false }, DockerJson.Options); + (int status, string? reason, Stream? hijacked) = await DockerRawHttp .PostAsync(stream, $"/exec/{Uri.EscapeDataString(created.Id)}/start", body, upgrade: true, cancellationToken) .ConfigureAwait(false); if (status is not (200 or 101)) @@ -147,11 +147,11 @@ public async Task ExecCaptureAsync(string containerId, string[] com var decoder = new DockerFrameDecoder(tty: false, timestamps: false); await decoder.ReadAsync(hijacked, line => { - var target = line.Kind == DockerStreamKind.StdErr ? stderr : stdout; - target.Append(line.Text).Append('\n'); + StringBuilder target = line.Kind == DockerStreamKind.StdErr ? stderr : stdout; + _ = target.Append(line.Text).Append('\n'); }, cancellationToken).ConfigureAwait(false); } - var inspect = await InspectExecAsync(created.Id, cancellationToken).ConfigureAwait(false); + ExecInspectResponse inspect = await InspectExecAsync(created.Id, cancellationToken).ConfigureAwait(false); return new(stdout.ToString(), stderr.ToString(), inspect.ExitCode); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs index 9289869..04e8f3f 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Files.cs @@ -49,10 +49,10 @@ public sealed partial class DockerClient public async Task ListDirectoryAsync(string containerId, string path, CancellationToken cancellationToken = default) { - var target = string.IsNullOrWhiteSpace(path) ? "/" : path; + string target = string.IsNullOrWhiteSpace(path) ? "/" : path; // -A 排掉 . 与 ..,--time-style=long-iso 把时间列压成固定的两段, // 省得为不同 locale 的 ls 输出各写一套解析。 - var result = await ExecCaptureAsync(containerId, + ExecCapture result = await ExecCaptureAsync(containerId, ["/bin/sh", "-c", $"ls -lAL --time-style=long-iso -- {ShellQuote(target)} 2>/dev/null || ls -lA -- {ShellQuote(target)}"], cancellationToken: cancellationToken).ConfigureAwait(false); if (!result.IsSuccess && result.StandardOutput.Length == 0) @@ -61,7 +61,7 @@ public async Task ListDirectoryAsync(string containerId, s result.StandardError.Length > 0 ? result.FailureText : $"列不出目录 {target}。"); } List entries = []; - foreach (var raw in result.StandardOutput.Split('\n')) + foreach (string raw in result.StandardOutput.Split('\n')) { if (ParseLsLine(raw, target) is { } entry) { @@ -76,32 +76,32 @@ public async Task ListDirectoryAsync(string containerId, s /// internal static ContainerFileEntry? ParseLsLine(string line, string directory) { - var trimmed = line.TrimEnd('\r'); + string trimmed = line.TrimEnd('\r'); if (trimmed.Length < 10 || trimmed.StartsWith("total ", StringComparison.Ordinal)) { return null; } - var mode = trimmed[..10]; + string mode = trimmed[..10]; if (mode[0] is not ('-' or 'd' or 'l' or 'c' or 'b' or 'p' or 's')) { return null; } // 权限 链接数 属主 属组 大小 日期 时间 名字… - var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); + string[] parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 8) { return null; } - var owner = $"{parts[2]} {parts[3]}"; - _ = long.TryParse(parts[4], out var size); - var modified = $"{parts[5]} {parts[6]}"; + string owner = $"{parts[2]} {parts[3]}"; + _ = long.TryParse(parts[4], out long size); + string modified = $"{parts[5]} {parts[6]}"; // 名字里可能有空格,所以按"前七段之后的全部"取,而不是取第八段。 - var nameStart = IndexOfNthToken(trimmed, 7); - var name = nameStart >= 0 ? trimmed[nameStart..] : parts[^1]; + int nameStart = IndexOfNthToken(trimmed, 7); + string name = nameStart >= 0 ? trimmed[nameStart..] : parts[^1]; string? linkTarget = null; if (mode[0] == 'l') { - var arrow = name.IndexOf(" -> ", StringComparison.Ordinal); + int arrow = name.IndexOf(" -> ", StringComparison.Ordinal); if (arrow > 0) { linkTarget = name[(arrow + 4)..]; @@ -112,7 +112,7 @@ public async Task ListDirectoryAsync(string containerId, s { return null; } - var full = directory.TrimEnd('/') + "/" + name; + string full = directory.TrimEnd('/') + "/" + name; return new(name, full, mode[0] == 'd', mode[0] == 'l', size, mode, owner, modified, linkTarget); } @@ -153,10 +153,10 @@ private static int IndexOfNthToken(string text, int n) public async Task ReadFileAsync(string containerId, string path, long maxBytes = MaxEditableFileBytes, CancellationToken cancellationToken = default) { - var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); - using var response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); - await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var entry = await TarUtil.ReadFirstFileAsync(body, maxBytes, cancellationToken) + string url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); + using HttpResponseMessage response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + (string Name, byte[] Content)? entry = await TarUtil.ReadFirstFileAsync(body, maxBytes, cancellationToken) .ConfigureAwait(false); return entry?.Content ?? throw new DockerApiException(System.Net.HttpStatusCode.NotFound, $"{path} 不是一个普通文件。"); @@ -172,31 +172,31 @@ public async Task ReadFileAsync(string containerId, string path, long ma public async Task WriteFileAsync(string containerId, string path, ReadOnlyMemory content, CancellationToken cancellationToken = default) { - var directory = GetDirectory(path); - var name = path[(path.LastIndexOf('/') + 1)..]; - var tar = TarUtil.CreateSingleFile(name, content.Span); - var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + + string directory = GetDirectory(path); + string name = path[(path.LastIndexOf('/') + 1)..]; + byte[] tar = TarUtil.CreateSingleFile(name, content.Span); + string url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", directory), ("noOverwriteDirNonDir", "1")); using var request = new HttpRequestMessage(HttpMethod.Put, url) { Content = new ByteArrayContent(tar) }; request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/x-tar"); - using var response = await SendRawAsync(request, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendRawAsync(request, cancellationToken).ConfigureAwait(false); await EnsureSuccessAsync(response, url, cancellationToken).ConfigureAwait(false); } /// 把容器里的一个路径整包取下来(目录也行),交给调用方落盘。 public async Task DownloadArchiveAsync(string containerId, string path, CancellationToken cancellationToken = default) { - var url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); - var response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); + string url = $"/containers/{Uri.EscapeDataString(containerId)}/archive" + Query(("path", path)); + HttpResponseMessage response = await OpenStreamAsync(HttpMethod.Get, url, cancellationToken).ConfigureAwait(false); return new ResponseOwningStream(response, await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)); } private static string GetDirectory(string path) { - var slash = path.LastIndexOf('/'); + int slash = path.LastIndexOf('/'); return slash <= 0 ? "/" : path[..slash]; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs index 0548d4c..ec66445 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Images.cs @@ -53,7 +53,7 @@ public Task RemoveImageAsync(string id, bool force = false, Canc public async Task PullImageAsync(string fromImage, string? tag, string? platform, bool allTags, string? registryAuth, IProgress progress, CancellationToken cancellationToken = default) { - var path = "/images/create" + Query( + string path = "/images/create" + Query( ("fromImage", fromImage), ("tag", allTags ? null : string.IsNullOrWhiteSpace(tag) ? "latest" : tag), ("platform", string.IsNullOrWhiteSpace(platform) ? null : platform)); @@ -64,7 +64,7 @@ public async Task PullImageAsync(string fromImage, string? tag, string? platform public async Task PushImageAsync(string name, string? tag, string? registryAuth, IProgress progress, CancellationToken cancellationToken = default) { - var path = $"/images/{Uri.EscapeDataString(name)}/push" + Query(("tag", tag)); + string path = $"/images/{Uri.EscapeDataString(name)}/push" + Query(("tag", tag)); await StreamNdjsonAsync(HttpMethod.Post, path, registryAuth, progress, cancellationToken).ConfigureAwait(false); } @@ -90,7 +90,7 @@ public Task PruneImagesAsync(bool danglingOnly, CancellationToken c public async Task SaveImagesAsync(IReadOnlyList names, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); - var path = "/images/get" + Query([.. names.Select(n => ("names", (string?)n))]); + string path = "/images/get" + Query([.. names.Select(n => ("names", (string?)n))]); HttpResponseMessage response; try { @@ -135,7 +135,7 @@ private async Task StreamNdjsonAsync(HttpMethod method, string path, string? reg } if (!string.IsNullOrEmpty(registryAuth)) { - request.Headers.TryAddWithoutValidation("X-Registry-Auth", registryAuth); + _ = request.Headers.TryAddWithoutValidation("X-Registry-Auth", registryAuth); } HttpResponseMessage response; try @@ -150,7 +150,7 @@ private async Task StreamNdjsonAsync(HttpMethod method, string path, string? reg using (response) { await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); - await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(body); string? failure = null; while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs index 3214e91..54c8de6 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.Streams.cs @@ -17,9 +17,9 @@ public sealed partial class DockerClient public async Task StreamEventsAsync(DateTimeOffset? since, Action onEvent, CancellationToken cancellationToken) { - var path = "/events" + Query(("since", since?.ToUnixTimeSeconds().ToString())); - using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); - await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + string path = "/events" + Query(("since", since?.ToUnixTimeSeconds().ToString())); + using HttpResponseMessage response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(body); while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) { @@ -58,15 +58,15 @@ public async Task StreamEventsAsync(DateTimeOffset? since, Action o public async Task StreamLogsAsync(string id, bool tty, bool follow, string tail, bool timestamps, DateTimeOffset? since, Action onLine, CancellationToken cancellationToken) { - var path = $"/containers/{Uri.EscapeDataString(id)}/logs" + Query( + string path = $"/containers/{Uri.EscapeDataString(id)}/logs" + Query( ("stdout", "1"), ("stderr", "1"), ("follow", follow ? "1" : "0"), ("tail", string.IsNullOrWhiteSpace(tail) ? "500" : tail), ("timestamps", timestamps ? "1" : "0"), ("since", since?.ToUnixTimeSeconds().ToString())); - using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); - await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var decoder = new DockerFrameDecoder(tty, timestamps); await decoder.ReadAsync(body, onLine, cancellationToken).ConfigureAwait(false); } @@ -77,9 +77,9 @@ public async Task StreamLogsAsync(string id, bool tty, bool follow, string tail, /// public async Task StreamStatsAsync(string id, Action onSample, CancellationToken cancellationToken) { - var path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "1"), ("one-shot", "0")); - using var response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); - await using var body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + string path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "1"), ("one-shot", "0")); + using HttpResponseMessage response = await OpenStreamAsync(HttpMethod.Get, path, cancellationToken).ConfigureAwait(false); + await using Stream body = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); using var reader = new StreamReader(body); while (await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false) is { } line) { @@ -109,8 +109,8 @@ public async Task StreamStatsAsync(string id, Action onSample, C /// public async Task StatsSnapshotAsync(string id, CancellationToken cancellationToken = default) { - var path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "0"), ("one-shot", "0")); - var body = await GetStringAsync(path, cancellationToken).ConfigureAwait(false); + string path = $"/containers/{Uri.EscapeDataString(id)}/stats" + Query(("stream", "0"), ("one-shot", "0")); + string body = await GetStringAsync(path, cancellationToken).ConfigureAwait(false); return DockerJson.TryDeserialize(body); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs index 11efb71..3ff043a 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.VolumesNetworks.cs @@ -7,7 +7,7 @@ public sealed partial class DockerClient /// 列卷。 public async Task ListVolumesAsync(CancellationToken cancellationToken = default) { - var response = await GetJsonAsync("/volumes", cancellationToken).ConfigureAwait(false); + VolumeListResponse response = await GetJsonAsync("/volumes", cancellationToken).ConfigureAwait(false); return response.Volumes ?? []; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs index 7f5efca..b4c0a46 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerClient.cs @@ -79,13 +79,13 @@ public ValueTask DisposeAsync() internal static string Query(params (string Key, string? Value)[] items) { var sb = new StringBuilder(); - foreach ((var key, var value) in items) + foreach ((string? key, string? value) in items) { if (value is null) { continue; } - sb.Append(sb.Length == 0 ? '?' : '&') + _ = sb.Append(sb.Length == 0 ? '?' : '&') .Append(Uri.EscapeDataString(key)) .Append('=') .Append(Uri.EscapeDataString(value)); @@ -101,7 +101,7 @@ internal static string Query(params (string Key, string? Value)[] items) return null; } var map = new Dictionary(); - foreach (var group in items.GroupBy(i => i.Key)) + foreach (IGrouping group in items.GroupBy(i => i.Key)) { map[group.Key] = [.. group.Select(g => g.Value)]; } @@ -111,42 +111,42 @@ internal static string Query(params (string Key, string? Value)[] items) /// GET 一段 JSON 并反序列化。 internal async Task GetJsonAsync(string path, CancellationToken cancellationToken) { - using var response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); } /// GET 一段原始文本(inspect 的完整 JSON 直接给界面看)。 internal async Task GetStringAsync(string path, CancellationToken cancellationToken) { - using var response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Get, path, null, cancellationToken).ConfigureAwait(false); return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); } /// POST,不关心响应体。 internal async Task PostAsync(string path, object? body = null, CancellationToken cancellationToken = default) { - using var response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); } /// POST 并反序列化响应。 internal async Task PostJsonAsync(string path, object? body = null, CancellationToken cancellationToken = default) { - using var response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Post, path, JsonBody(body), cancellationToken).ConfigureAwait(false); return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); } /// DELETE,不关心响应体。 internal async Task DeleteAsync(string path, CancellationToken cancellationToken = default) { - using var response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); } /// DELETE 并反序列化响应。 internal async Task DeleteJsonAsync(string path, CancellationToken cancellationToken = default) { - using var response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); + using HttpResponseMessage response = await SendAsync(HttpMethod.Delete, path, null, cancellationToken).ConfigureAwait(false); return await ReadJsonAsync(response, path, cancellationToken).ConfigureAwait(false); } @@ -208,7 +208,7 @@ private async Task SendAsync(HttpMethod method, string path private static async Task ReadJsonAsync(HttpResponseMessage response, string path, CancellationToken cancellationToken) { await EnsureSuccessAsync(response, path, cancellationToken).ConfigureAwait(false); - var value = await response.Content.ReadFromJsonAsync(DockerJson.Options, cancellationToken).ConfigureAwait(false); + T? value = await response.Content.ReadFromJsonAsync(DockerJson.Options, cancellationToken).ConfigureAwait(false); return value ?? throw new DockerApiException(response.StatusCode, "daemon 返回了空响应体。", path); } @@ -225,7 +225,7 @@ private static async Task EnsureSuccessAsync(HttpResponseMessage response, strin { return; } - var body = ""; + string body = ""; try { body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); @@ -234,7 +234,7 @@ private static async Task EnsureSuccessAsync(HttpResponseMessage response, strin { // 连错误响应体都读不回来时,至少还有状态码可报。 } - var message = DockerJson.TryDeserialize(body)?.Message + string message = DockerJson.TryDeserialize(body)?.Message ?? (body.Length > 0 ? body.Trim() : $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}"); throw new DockerApiException(response.StatusCode, message, path); } @@ -248,10 +248,10 @@ private static async Task EnsureSuccessAsync(HttpResponseMessage response, strin /// private DockerUnreachableException Unreachable(Exception ex) { - var text = Flatten(ex); + string text = Flatten(ex); // 权限先判:它是明说的,而下面那组"通道开不起来"是笼统的 —— // 一条同时带着两者的消息,按"没权限"处理才对得上用户要做的事。 - var reason = + DockerUnreachableReason reason = text.Contains("permission denied", StringComparison.OrdinalIgnoreCase) || text.Contains("access is denied", StringComparison.OrdinalIgnoreCase) || text.Contains("EACCES", StringComparison.OrdinalIgnoreCase) @@ -272,7 +272,7 @@ private DockerUnreachableException Unreachable(Exception ex) text.Contains("not found", StringComparison.OrdinalIgnoreCase) ? DockerUnreachableReason.SessionUnavailable : DockerUnreachableReason.Unknown; - var message = reason switch + string message = reason switch { DockerUnreachableReason.SocketMissing => $"连不上 {Transport.Description}:打不开到它的通道。远端可能没装 Docker、daemon 没在跑,或者这条路径不对。", @@ -290,13 +290,13 @@ private DockerUnreachableException Unreachable(Exception ex) private static string Flatten(Exception ex) { var sb = new StringBuilder(); - for (var current = ex; current is not null; current = current.InnerException) + for (Exception? current = ex; current is not null; current = current.InnerException) { if (sb.Length > 0) { - sb.Append(" → "); + _ = sb.Append(" → "); } - sb.Append(current.Message); + _ = sb.Append(current.Message); } return sb.ToString(); } @@ -309,7 +309,7 @@ private static string Flatten(Exception ex) /// public async Task PingAsync(CancellationToken cancellationToken = default) { - var version = await GetJsonAsync("/version", cancellationToken).ConfigureAwait(false); + SystemVersion version = await GetJsonAsync("/version", cancellationToken).ConfigureAwait(false); return version; } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs index 21e50b9..f1c6515 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerFrames.cs @@ -51,7 +51,7 @@ public sealed class DockerFrameDecoder(bool tty, bool timestamps) /// 取消令牌。 public async Task ReadAsync(Stream stream, Action onLine, CancellationToken cancellationToken) { - var payload = new byte[64 * 1024]; + byte[] payload = new byte[64 * 1024]; while (!cancellationToken.IsCancellationRequested) { DockerStreamKind kind; @@ -96,30 +96,30 @@ public async Task ReadAsync(Stream stream, Action onLine, Cancell private void Emit(DockerStreamKind kind, ReadOnlySpan bytes, Action onLine) { - var needed = _decoder.GetCharCount(bytes, flush: false); + int needed = _decoder.GetCharCount(bytes, flush: false); if (needed > _chars.Length) { _chars = new char[needed]; } - var produced = _decoder.GetChars(bytes, _chars, flush: false); - var buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; - for (var i = 0; i < produced; i++) + int produced = _decoder.GetChars(bytes, _chars, flush: false); + StringBuilder buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; + for (int i = 0; i < produced; i++) { - var c = _chars[i]; + char c = _chars[i]; if (c == '\n') { PublishLine(kind, buffer, onLine); } else if (c != '\r') { - buffer.Append(c); + _ = buffer.Append(c); } } } private void FlushRemainder(DockerStreamKind kind, Action onLine) { - var buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; + StringBuilder buffer = kind == DockerStreamKind.StdErr ? _stderr : _stdout; if (buffer.Length > 0) { PublishLine(kind, buffer, onLine); @@ -128,10 +128,10 @@ private void FlushRemainder(DockerStreamKind kind, Action onLine) private void PublishLine(DockerStreamKind kind, StringBuilder buffer, Action onLine) { - var text = buffer.ToString(); - buffer.Clear(); + string text = buffer.ToString(); + _ = buffer.Clear(); DateTimeOffset? stamp = null; - if (timestamps && TrySplitTimestamp(text, out var parsed, out var rest)) + if (timestamps && TrySplitTimestamp(text, out DateTimeOffset parsed, out string? rest)) { stamp = parsed; text = rest; @@ -147,7 +147,7 @@ internal static bool TrySplitTimestamp(string line, out DateTimeOffset timestamp { timestamp = default; rest = line; - var space = line.IndexOf(' '); + int space = line.IndexOf(' '); // RFC3339 至少是 "2026-08-21T09:41:22Z" 这么长。 if (space is < 20 or > 40) { @@ -164,10 +164,10 @@ internal static bool TrySplitTimestamp(string line, out DateTimeOffset timestamp private static async ValueTask ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) { - var read = 0; + int read = 0; while (read < buffer.Length) { - var n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); + int n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); if (n <= 0) { return false; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs index 8e01ee2..d1e2e70 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerModels.cs @@ -35,7 +35,7 @@ public static class DockerJson /// public static T? TryDeserialize(string json) where T : class { - var body = json.AsSpan().Trim(); + ReadOnlySpan body = json.AsSpan().Trim(); if (body.IsEmpty || (body[0] != '{' && body[0] != '[')) { return null; @@ -974,13 +974,13 @@ public double CpuPercent { return 0; } - var cpuDelta = (double)CpuStats.CpuUsage.TotalUsage - PreCpuStats.CpuUsage.TotalUsage; - var systemDelta = (double)CpuStats.SystemCpuUsage - PreCpuStats.SystemCpuUsage; + double cpuDelta = (double)CpuStats.CpuUsage.TotalUsage - PreCpuStats.CpuUsage.TotalUsage; + double systemDelta = (double)CpuStats.SystemCpuUsage - PreCpuStats.SystemCpuUsage; if (cpuDelta <= 0 || systemDelta <= 0) { return 0; } - var cpus = CpuStats.OnlineCpus > 0 + int cpus = CpuStats.OnlineCpus > 0 ? CpuStats.OnlineCpus : CpuStats.CpuUsage.PerCpuUsage?.Length ?? 1; return cpuDelta / systemDelta * cpus * 100.0; @@ -1006,11 +1006,11 @@ public ulong MemoryUsed ulong cache = 0; if (MemoryStats.Stats is { } stats) { - if (stats.TryGetValue("inactive_file", out var inactive)) + if (stats.TryGetValue("inactive_file", out ulong inactive)) { cache = inactive; } - else if (stats.TryGetValue("cache", out var c)) + else if (stats.TryGetValue("cache", out ulong c)) { cache = c; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs index 1247f78..6b6e6d1 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerRawHttp.cs @@ -27,8 +27,8 @@ internal static class DockerRawHttp public static async Task<(int StatusCode, string ReasonPhrase, Stream Body)> PostAsync( Stream stream, string path, string? jsonBody, bool upgrade, CancellationToken cancellationToken) { - var body = jsonBody is null ? [] : Encoding.UTF8.GetBytes(jsonBody); - var request = new StringBuilder() + byte[] body = jsonBody is null ? [] : Encoding.UTF8.GetBytes(jsonBody); + StringBuilder request = new StringBuilder() .Append("POST ").Append(path).Append(" HTTP/1.1\r\n") .Append("Host: docker\r\n") .Append("User-Agent: VelaShell-DockerPanel\r\n") @@ -36,9 +36,9 @@ internal static class DockerRawHttp .Append("Content-Length: ").Append(body.Length).Append("\r\n"); if (upgrade) { - request.Append("Connection: Upgrade\r\nUpgrade: tcp\r\n"); + _ = request.Append("Connection: Upgrade\r\nUpgrade: tcp\r\n"); } - request.Append("\r\n"); + _ = request.Append("\r\n"); await stream.WriteAsync(Encoding.ASCII.GetBytes(request.ToString()), cancellationToken).ConfigureAwait(false); if (body.Length > 0) { @@ -58,11 +58,11 @@ internal static class DockerRawHttp private static async Task<(int, string, Stream)> ReadResponseHeadAsync(Stream stream, CancellationToken cancellationToken) { var head = new List(512); - var one = new byte[1]; - var matched = 0; + byte[] one = new byte[1]; + int matched = 0; while (matched < HeaderTerminator.Length) { - var read = await stream.ReadAsync(one.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); + int read = await stream.ReadAsync(one.AsMemory(0, 1), cancellationToken).ConfigureAwait(false); if (read <= 0) { throw new DockerApiException(System.Net.HttpStatusCode.BadGateway, @@ -71,11 +71,11 @@ internal static class DockerRawHttp head.Add(one[0]); matched = one[0] == HeaderTerminator[matched] ? matched + 1 : one[0] == HeaderTerminator[0] ? 1 : 0; } - var text = Encoding.ASCII.GetString([.. head]); - var statusLine = text[..text.IndexOf('\r')]; - var parts = statusLine.Split(' ', 3); - var status = parts.Length > 1 && int.TryParse(parts[1], out var code) ? code : 0; - var reason = parts.Length > 2 ? parts[2] : ""; + string text = Encoding.ASCII.GetString([.. head]); + string statusLine = text[..text.IndexOf('\r')]; + string[] parts = statusLine.Split(' ', 3); + int status = parts.Length > 1 && int.TryParse(parts[1], out int code) ? code : 0; + string reason = parts.Length > 2 ? parts[2] : ""; return (status, reason, stream); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs index 8f1a703..597da4e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/DockerTransport.cs @@ -52,7 +52,7 @@ public async Task ConnectAsync(CancellationToken cancellationToken = def { if (OperatingSystem.IsWindows() && Description.StartsWith(@"\\.\pipe\", StringComparison.Ordinal)) { - var pipeName = Description[@"\\.\pipe\".Length..]; + string pipeName = Description[@"\\.\pipe\".Length..]; var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); try { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs index 5066f0c..511ffcb 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/Humanize.cs @@ -22,7 +22,7 @@ public static string Bytes(long value) return $"{value} B"; } double size = value; - var unit = 0; + int unit = 0; while (size >= 1000 && unit < SizeUnits.Length - 1) { size /= 1000; @@ -63,7 +63,7 @@ public static string Duration(TimeSpan span) /// 把一个时刻说成"多久以前"。 public static string Ago(DateTimeOffset moment) { - var span = DateTimeOffset.UtcNow - moment.ToUniversalTime(); + TimeSpan span = DateTimeOffset.UtcNow - moment.ToUniversalTime(); return span < TimeSpan.FromSeconds(5) ? "刚刚" : $"{Duration(span)} 前"; } @@ -73,7 +73,7 @@ public static string Ago(DateTimeOffset moment) /// 所以这两个入口都得有。 /// public static string AgoFromIso(string? iso) => - DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var moment) + DateTimeOffset.TryParse(iso, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out DateTimeOffset moment) ? Ago(moment) : "—"; @@ -85,7 +85,7 @@ public static string AgoFromUnix(long unixSeconds) => public static string LocalTime(string? iso) { return string.IsNullOrWhiteSpace(iso) ? "—" - : DateTimeOffset.TryParse(iso, null, DateTimeStyles.RoundtripKind, out var parsed) + : DateTimeOffset.TryParse(iso, null, DateTimeStyles.RoundtripKind, out DateTimeOffset parsed) ? parsed.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) : iso; } @@ -101,15 +101,15 @@ public static string Ports(DockerPort[]? ports) } // 同一个容器端口可能同时绑在 0.0.0.0 与 :: 上,daemon 会给两条。 // 界面上它们是同一件事,去重之后才不会显示成 "8080→80, 8080→80"。 - var published = ports + IEnumerable published = ports .Where(p => p.PublicPort > 0) .Select(p => $"{p.PublicPort}→{p.PrivatePort}") .Distinct(); - var exposed = ports + IEnumerable exposed = ports .Where(p => p.PublicPort == 0) .Select(p => $"{p.PrivatePort}/{p.Type ?? "tcp"}") .Distinct(); - var text = string.Join(", ", published.Concat(exposed)); + string text = string.Join(", ", published.Concat(exposed)); return text.Length == 0 ? "—" : text; } @@ -120,7 +120,7 @@ public static string ShortId(string? id) { return ""; } - var text = id.StartsWith("sha256:", StringComparison.Ordinal) ? id[7..] : id; + string text = id.StartsWith("sha256:", StringComparison.Ordinal) ? id[7..] : id; return text.Length <= 12 ? text : text[..12]; } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs index 12d3c15..8510969 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/LineDiff.cs @@ -42,13 +42,13 @@ public static class LineDiff /// 比出差异。 public static IReadOnlyList Compute(string oldText, string newText) { - var a = Split(oldText); - var b = Split(newText); - var lcs = BuildLcs(a, b); + string[] a = Split(oldText); + string[] b = Split(newText); + int[,] lcs = BuildLcs(a, b); List result = []; - var i = 0; - var j = 0; + int i = 0; + int j = 0; while (i < a.Length && j < b.Length) { if (a[i] == b[j]) @@ -95,15 +95,15 @@ public static int CountChanged(IReadOnlyList lines) => private static List Pair(List lines) { List paired = [with(lines.Count)]; - for (var k = 0; k < lines.Count; k++) + for (int k = 0; k < lines.Count; k++) { - var current = lines[k]; + DiffLine current = lines[k]; if (current.Marker == DiffMarker.Removed && k + 1 < lines.Count && lines[k + 1].Marker == DiffMarker.Added && lines.Count <= MaxPairedChange * 4) { - var next = lines[k + 1]; + DiffLine next = lines[k + 1]; paired.Add(new(DiffMarker.Changed, current.OldNumber, next.NewNumber, next.Text)); k++; continue; @@ -119,10 +119,10 @@ private static string[] Split(string text) => private static int[,] BuildLcs(string[] a, string[] b) { // lcs[i, j] = a[i..] 与 b[j..] 的最长公共子序列长度。倒着填,回溯时正着走。 - var lcs = new int[a.Length + 1, b.Length + 1]; - for (var i = a.Length - 1; i >= 0; i--) + int[,] lcs = new int[a.Length + 1, b.Length + 1]; + for (int i = a.Length - 1; i >= 0; i--) { - for (var j = b.Length - 1; j >= 0; j--) + for (int j = b.Length - 1; j >= 0; j--) { lcs[i, j] = a[i] == b[j] ? lcs[i + 1, j + 1] + 1 diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs index 87e9d1c..82c055e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/RegistryAuth.cs @@ -54,14 +54,14 @@ public sealed class RegistryAuthProvider(IRemoteFsApi remoteFs, DockerEndpoint e /// public static string ResolveRegistry(string imageReference) { - var reference = imageReference.Trim(); - var slash = reference.IndexOf('/'); + string reference = imageReference.Trim(); + int slash = reference.IndexOf('/'); if (slash <= 0) { return DockerHub; } - var first = reference[..slash]; - var isHost = first.Contains('.', StringComparison.Ordinal) + string first = reference[..slash]; + bool isHost = first.Contains('.', StringComparison.Ordinal) || first.Contains(':', StringComparison.Ordinal) || first == "localhost"; return isHost ? first : DockerHub; @@ -70,8 +70,8 @@ public static string ResolveRegistry(string imageReference) /// 查这个镜像所属仓库的登录状态(给表单显示用,不返回凭据本身)。 public async Task GetStatusAsync(string imageReference, CancellationToken cancellationToken = default) { - var registry = ResolveRegistry(imageReference); - var config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); + string registry = ResolveRegistry(imageReference); + DockerConfigFile? config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); if (config is null) { return new(registry, registry == DockerHub ? RegistryAuthState.NotRequired : RegistryAuthState.Missing, @@ -84,7 +84,7 @@ public async Task GetStatusAsync(string imageReference, Canc return new(registry, RegistryAuthState.HelperOnly, "凭据由 credential helper 保管,面板读不到。私有镜像请在终端里 docker pull。"); } - if (config.Auths?.TryGetValue(registry, out var entry) == true && !string.IsNullOrEmpty(entry.Auth)) + if (config.Auths?.TryGetValue(registry, out DockerAuthEntry? entry) == true && !string.IsNullOrEmpty(entry.Auth)) { return new(registry, RegistryAuthState.Available, "凭据来自远端 ~/.docker/config.json"); } @@ -98,20 +98,20 @@ public async Task GetStatusAsync(string imageReference, Canc /// public async Task GetAuthHeaderAsync(string imageReference, CancellationToken cancellationToken = default) { - var registry = ResolveRegistry(imageReference); - var config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); - if (config?.Auths is null || !config.Auths.TryGetValue(registry, out var entry)) + string registry = ResolveRegistry(imageReference); + DockerConfigFile? config = await ReadConfigAsync(cancellationToken).ConfigureAwait(false); + if (config?.Auths is null || !config.Auths.TryGetValue(registry, out DockerAuthEntry? entry)) { return null; } - var username = entry.Username; - var password = entry.Password; + string? username = entry.Username; + string? password = entry.Password; if (!string.IsNullOrEmpty(entry.Auth)) { try { - var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(entry.Auth)); - var colon = decoded.IndexOf(':'); + string decoded = Encoding.UTF8.GetString(Convert.FromBase64String(entry.Auth)); + int colon = decoded.IndexOf(':'); if (colon > 0) { username = decoded[..colon]; @@ -127,7 +127,7 @@ public async Task GetStatusAsync(string imageReference, Canc { return null; } - var json = JsonSerializer.Serialize(new + string json = JsonSerializer.Serialize(new { username, password, @@ -146,19 +146,19 @@ public async Task GetStatusAsync(string imageReference, Canc { if (endpoint.Kind == DockerEndpointKind.Local) { - var path = Path.Combine( + string path = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".docker", "config.json"); return File.Exists(path) ? DockerJson.TryDeserialize(await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false)) : null; } - var home = await remoteFs.GetWorkingDirectoryAsync(endpoint.SessionId, cancellationToken).ConfigureAwait(false); - var remotePath = $"{home.TrimEnd('/')}/.docker/config.json"; + string home = await remoteFs.GetWorkingDirectoryAsync(endpoint.SessionId, cancellationToken).ConfigureAwait(false); + string remotePath = $"{home.TrimEnd('/')}/.docker/config.json"; if (!await remoteFs.ExistsAsync(endpoint.SessionId, remotePath, cancellationToken).ConfigureAwait(false)) { return null; } - var bytes = await remoteFs.ReadAllBytesAsync(endpoint.SessionId, remotePath, 1 << 20, cancellationToken) + byte[] bytes = await remoteFs.ReadAllBytesAsync(endpoint.SessionId, remotePath, 1 << 20, cancellationToken) .ConfigureAwait(false); return DockerJson.TryDeserialize(Encoding.UTF8.GetString(bytes)); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs index 7eec0c5..333a683 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/RunCommandBuilder.cs @@ -23,88 +23,88 @@ public static class RunCommandBuilder public static string Build(ContainerInspect inspect, IReadOnlyCollection? imageEnv) { var sb = new StringBuilder("docker run"); - var config = inspect.Config; - var host = inspect.HostConfig; + ContainerConfig? config = inspect.Config; + ContainerHostConfig? host = inspect.HostConfig; // -d 是面板起容器的默认形态,也是绝大多数长期容器的形态。 - sb.Append(" -d"); + _ = sb.Append(" -d"); if (host?.AutoRemove == true) { - sb.Append(" --rm"); + _ = sb.Append(" --rm"); } if (inspect.Name is { Length: > 0 } name) { - sb.Append(" --name ").Append(Sh.Quote(name.TrimStart('/'))); + _ = sb.Append(" --name ").Append(Sh.Quote(name.TrimStart('/'))); } - foreach ((var port, var bindings) in Ordered(host?.PortBindings)) + foreach ((string? port, PortBinding[]? bindings) in Ordered(host?.PortBindings)) { // tcp 是 -p 的默认协议,写出来只是噪音;udp / sctp 必须留着。 - var spec = port.EndsWith("/tcp", StringComparison.Ordinal) ? port[..^4] : port; - foreach (var binding in bindings ?? []) + string spec = port.EndsWith("/tcp", StringComparison.Ordinal) ? port[..^4] : port; + foreach (PortBinding binding in bindings ?? []) { - var hostPart = string.IsNullOrEmpty(binding.HostIp) + string hostPart = string.IsNullOrEmpty(binding.HostIp) ? binding.HostPort ?? "" : $"{binding.HostIp}:{binding.HostPort}"; - sb.Append(" -p ").Append(Sh.Quote(hostPart.Length > 0 ? $"{hostPart}:{spec}" : spec)); + _ = sb.Append(" -p ").Append(Sh.Quote(hostPart.Length > 0 ? $"{hostPart}:{spec}" : spec)); } } - foreach (var bind in host?.Binds ?? []) + foreach (string bind in host?.Binds ?? []) { - sb.Append(" -v ").Append(Sh.Quote(bind)); + _ = sb.Append(" -v ").Append(Sh.Quote(bind)); } // Binds 只覆盖 -v 起的那些;命名卷经 Mounts 出现,别漏掉。 - foreach (var mount in inspect.Mounts ?? []) + foreach (DockerMount mount in inspect.Mounts ?? []) { if (mount.Type != "volume" || mount.Name is not { Length: > 0 } volume) { continue; } - var spec = $"{volume}:{mount.Destination}{(mount.RW ? "" : ":ro")}"; - sb.Append(" -v ").Append(Sh.Quote(spec)); + string spec = $"{volume}:{mount.Destination}{(mount.RW ? "" : ":ro")}"; + _ = sb.Append(" -v ").Append(Sh.Quote(spec)); } - foreach (var entry in UserEnv(config?.Env, imageEnv)) + foreach (string entry in UserEnv(config?.Env, imageEnv)) { - sb.Append(" -e ").Append(Sh.Quote(entry)); + _ = sb.Append(" -e ").Append(Sh.Quote(entry)); } if (host?.NetworkMode is { Length: > 0 } network && network != "default" && !network.StartsWith("container:", StringComparison.Ordinal)) { - sb.Append(" --network ").Append(Sh.Quote(network)); + _ = sb.Append(" --network ").Append(Sh.Quote(network)); } if (host?.RestartPolicy?.Name is { Length: > 0 } policy && policy != "no") { - sb.Append(" --restart ") + _ = sb.Append(" --restart ") .Append(policy == "on-failure" && host.RestartPolicy.MaximumRetryCount > 0 ? $"on-failure:{host.RestartPolicy.MaximumRetryCount}" : policy); } if (config?.User is { Length: > 0 } user) { - sb.Append(" -u ").Append(Sh.Quote(user)); + _ = sb.Append(" -u ").Append(Sh.Quote(user)); } if (config?.WorkingDir is { Length: > 0 } workdir) { - sb.Append(" -w ").Append(Sh.Quote(workdir)); + _ = sb.Append(" -w ").Append(Sh.Quote(workdir)); } if (host?.Privileged == true) { - sb.Append(" --privileged"); + _ = sb.Append(" --privileged"); } if (host?.Memory > 0) { - sb.Append(" -m ").Append(host.Memory); + _ = sb.Append(" -m ").Append(host.Memory); } - sb.Append(' ').Append(Sh.Quote(config?.Image ?? inspect.Image ?? "")); + _ = sb.Append(' ').Append(Sh.Quote(config?.Image ?? inspect.Image ?? "")); // Cmd 只有在覆盖了镜像默认值时才该出现,但面板拿不到镜像的默认 Cmd 来比对, // 所以照原样附上 —— 多写一次等价的命令,好过漏掉一个真正被覆盖过的。 - foreach (var arg in config?.Cmd ?? []) + foreach (string arg in config?.Cmd ?? []) { - sb.Append(' ').Append(Sh.Quote(arg)); + _ = sb.Append(' ').Append(Sh.Quote(arg)); } return sb.ToString(); } @@ -128,7 +128,7 @@ internal static IEnumerable UserEnv(string[]? containerEnv, IReadOnlyCol yield break; } HashSet fromImage = imageEnv is null ? [] : [.. imageEnv]; - foreach (var entry in containerEnv) + foreach (string entry in containerEnv) { if (!fromImage.Contains(entry)) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs index 9ffd537..70b201a 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/SocketProbe.cs @@ -56,8 +56,8 @@ public static async Task RunAsync(IRemoteExecApi exec, Docker { try { - var script = Script(endpoint.SocketPath); - var output = endpoint.Kind == DockerEndpointKind.Remote + string script = Script(endpoint.SocketPath); + string output = endpoint.Kind == DockerEndpointKind.Remote ? (await exec.RunAsync(endpoint.SessionId, script, new ExecOptions { Timeout = TimeSpan.FromSeconds(10) }, cancellationToken) .ConfigureAwait(false)).Output @@ -104,15 +104,15 @@ private static async Task LocalAsync(string script, CancellationToken ca CreateNoWindow = true } }; - process.Start(); - var output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + _ = process.Start(); + string output = await process.StandardOutput.ReadToEndAsync(cancellationToken).ConfigureAwait(false); await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false); return output; } private static SocketProbeResult Parse(string output) { - var line = output.Split('\n').Select(l => l.Trim()) + string line = output.Split('\n').Select(l => l.Trim()) .LastOrDefault(l => l.Length > 0) ?? ""; if (line == "OK") { @@ -127,7 +127,7 @@ private static SocketProbeResult Parse(string output) return Unknown; } // DENIED|属组|账号|账号所在的组 - var parts = line.Split('|'); + string[] parts = line.Split('|'); return new(SocketProbeKind.PermissionDenied, parts.Length > 2 ? parts[2].Trim() : "", parts.Length > 1 ? parts[1].Trim() : "", diff --git a/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs b/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs index cd8786d..9327da0 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Docker/TarUtil.cs @@ -24,7 +24,7 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i DateTimeOffset? modified = null) { ArgumentException.ThrowIfNullOrWhiteSpace(name); - var header = new byte[BlockSize]; + byte[] header = new byte[BlockSize]; WriteString(header, 0, 100, name); WriteOctal(header, 100, 8, (ulong)mode); WriteOctal(header, 108, 8, 0); // uid @@ -36,20 +36,20 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i header[263] = (byte)'0'; header[264] = (byte)'0'; // 校验和的算法要求先把校验和字段当成 8 个空格再求和 —— 顺序反了 tar 会说文件损坏。 - for (var i = 148; i < 156; i++) + for (int i = 148; i < 156; i++) { header[i] = (byte)' '; } uint checksum = 0; - foreach (var b in header) + foreach (byte b in header) { checksum += b; } WriteOctal(header, 148, 7, checksum); header[155] = (byte)' '; - var padded = (content.Length + BlockSize - 1) / BlockSize * BlockSize; - var result = new byte[BlockSize + padded + BlockSize * 2]; + int padded = (content.Length + BlockSize - 1) / BlockSize * BlockSize; + byte[] result = new byte[BlockSize + padded + BlockSize * 2]; header.CopyTo(result, 0); content.CopyTo(result.AsSpan(BlockSize)); return result; @@ -62,7 +62,7 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i public static async Task<(string Name, byte[] Content)?> ReadFirstFileAsync(Stream tar, long maxBytes, CancellationToken cancellationToken) { - var header = new byte[BlockSize]; + byte[] header = new byte[BlockSize]; while (true) { if (!await ReadExactlyAsync(tar, header, cancellationToken).ConfigureAwait(false)) @@ -74,10 +74,10 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i { return null; } - var name = ReadString(header, 0, 100); - var size = (long)ReadOctal(header, 124, 12); - var type = (char)header[156]; - var padded = (int)((size + BlockSize - 1) / BlockSize * BlockSize); + string name = ReadString(header, 0, 100); + long size = (long)ReadOctal(header, 124, 12); + char type = (char)header[156]; + int padded = (int)((size + BlockSize - 1) / BlockSize * BlockSize); if (type is '0' or '\0') { if (size > maxBytes) @@ -85,7 +85,7 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i throw new InvalidOperationException( $"文件 {name} 有 {size:N0} 字节,超过了面板允许在线编辑的上限 {maxBytes:N0} 字节。"); } - var content = new byte[size]; + byte[] content = new byte[size]; if (!await ReadExactlyAsync(tar, content, cancellationToken).ConfigureAwait(false)) { return null; @@ -109,8 +109,8 @@ public static byte[] CreateSingleFile(string name, ReadOnlySpan content, i public static async Task ExtractFirstFileAsync(Stream tar, Stream destination, CancellationToken cancellationToken) { - var header = new byte[BlockSize]; - var buffer = new byte[64 * 1024]; + byte[] header = new byte[BlockSize]; + byte[] buffer = new byte[64 * 1024]; while (true) { if (!await ReadExactlyAsync(tar, header, cancellationToken).ConfigureAwait(false) @@ -118,19 +118,19 @@ public static async Task ExtractFirstFileAsync(Stream tar, Stream destinat { return 0; } - var size = (long)ReadOctal(header, 124, 12); - var type = (char)header[156]; - var padded = (size + BlockSize - 1) / BlockSize * BlockSize; + long size = (long)ReadOctal(header, 124, 12); + char type = (char)header[156]; + long padded = (size + BlockSize - 1) / BlockSize * BlockSize; if (type is not ('0' or '\0')) { await SkipAsync(tar, padded, cancellationToken).ConfigureAwait(false); continue; } - var remaining = size; + long remaining = size; while (remaining > 0) { - var want = (int)Math.Min(buffer.Length, remaining); - var read = await tar.ReadAsync(buffer.AsMemory(0, want), cancellationToken).ConfigureAwait(false); + int want = (int)Math.Min(buffer.Length, remaining); + int read = await tar.ReadAsync(buffer.AsMemory(0, want), cancellationToken).ConfigureAwait(false); if (read <= 0) { // 流在中途断了。已经写出去的那部分是残缺的,必须说出来 —— @@ -148,20 +148,20 @@ public static async Task ExtractFirstFileAsync(Stream tar, Stream destinat private static void WriteString(byte[] buffer, int offset, int length, string value) { - var bytes = Encoding.UTF8.GetBytes(value); - var n = Math.Min(bytes.Length, length - 1); + byte[] bytes = Encoding.UTF8.GetBytes(value); + int n = Math.Min(bytes.Length, length - 1); Array.Copy(bytes, 0, buffer, offset, n); } private static void WriteOctal(byte[] buffer, int offset, int length, ulong value) { - var text = Convert.ToString((long)value, 8).PadLeft(length - 1, '0'); + string text = Convert.ToString((long)value, 8).PadLeft(length - 1, '0'); WriteString(buffer, offset, length, text); } private static string ReadString(byte[] buffer, int offset, int length) { - var end = offset; + int end = offset; while (end < offset + length && buffer[end] != 0) { end++; @@ -172,9 +172,9 @@ private static string ReadString(byte[] buffer, int offset, int length) private static ulong ReadOctal(byte[] buffer, int offset, int length) { ulong value = 0; - for (var i = offset; i < offset + length; i++) + for (int i = offset; i < offset + length; i++) { - var b = buffer[i]; + byte b = buffer[i]; if (b is 0 or (byte)' ') { continue; @@ -190,11 +190,11 @@ private static ulong ReadOctal(byte[] buffer, int offset, int length) private static async Task SkipAsync(Stream stream, long count, CancellationToken cancellationToken) { - var scratch = new byte[Math.Min(count, 64 * 1024) is var n && n > 0 ? n : 1]; - var left = count; + byte[] scratch = new byte[Math.Min(count, 64 * 1024) is var n && n > 0 ? n : 1]; + long left = count; while (left > 0) { - var read = await stream.ReadAsync(scratch.AsMemory(0, (int)Math.Min(left, scratch.Length)), cancellationToken) + int read = await stream.ReadAsync(scratch.AsMemory(0, (int)Math.Min(left, scratch.Length)), cancellationToken) .ConfigureAwait(false); if (read <= 0) { @@ -206,10 +206,10 @@ private static async Task SkipAsync(Stream stream, long count, CancellationToken private static async Task ReadExactlyAsync(Stream stream, Memory buffer, CancellationToken cancellationToken) { - var read = 0; + int read = 0; while (read < buffer.Length) { - var n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); + int n = await stream.ReadAsync(buffer[read..], cancellationToken).ConfigureAwait(false); if (n <= 0) { return false; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs index cae36ef..812ad51 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/AutoScroll.cs @@ -1,8 +1,8 @@ +using System.Collections.Specialized; using Avalonia; using Avalonia.Controls; using Avalonia.Threading; using Avalonia.VisualTree; -using System.Collections.Specialized; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -31,7 +31,10 @@ public static class AutoScroll public static readonly AttachedProperty SourceProperty = AvaloniaProperty.RegisterAttached("Source", typeof(AutoScroll)); - static AutoScroll() => SourceProperty.Changed.AddClassHandler(OnSourceChanged); + static AutoScroll() + { + _ = SourceProperty.Changed.AddClassHandler(OnSourceChanged); + } /// 读 。 public static INotifyCollectionChanged? GetSource(Control control) => control.GetValue(SourceProperty); @@ -42,7 +45,7 @@ public static void SetSource(Control control, INotifyCollectionChanged? value) = private static void OnSourceChanged(Control control, AvaloniaPropertyChangedEventArgs e) { - if (Watchers.Remove(control, out var previous)) + if (Watchers.Remove(control, out Watcher? previous)) { previous.Detach(); } @@ -67,7 +70,7 @@ internal Watcher(Control control, INotifyCollectionChanged source) _source = source; source.CollectionChanged += OnCollectionChanged; control.AttachedToVisualTree += OnAttached; - Bind(); + _ = Bind(); } internal void Detach() diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs index aab1eb5..c9c7ac0 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CodeEditor.cs @@ -1,10 +1,11 @@ +using System.Reflection; +using System.Xml; using Avalonia; using Avalonia.Controls; using Avalonia.Media; using AvaloniaEdit; using AvaloniaEdit.Highlighting; using AvaloniaEdit.Highlighting.Xshd; -using System.Xml; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -154,7 +155,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == TextProperty && !_syncing) { _syncing = true; - var incoming = Text ?? ""; + string incoming = Text ?? ""; // 只在真不一样时才写:同一份文本写回去会把光标顶到开头, // 用户打一个字就跳一次。 if (_editor.Text != incoming) @@ -245,7 +246,7 @@ private void ApplyLanguage() /// private IHighlightingDefinition Recolor(IHighlightingDefinition definition) { - foreach (var color in definition.NamedHighlightingColors) + foreach (HighlightingColor? color in definition.NamedHighlightingColors) { if (Role(color.Name) is { } token && Resource(token) is ISolidColorBrush brush) { @@ -276,7 +277,7 @@ private IHighlightingDefinition Recolor(IHighlightingDefinition definition) }; private object? Resource(string key) => - this.TryFindResource(key, ActualThemeVariant, out var value) ? value : null; + this.TryFindResource(key, ActualThemeVariant, out object? value) ? value : null; /// /// 取一份语法定义。 @@ -307,8 +308,8 @@ private static void EnsureRegistered() return; } _registered = true; - var assembly = typeof(CodeEditor).Assembly; - foreach ((var resource, var extensions) in + Assembly assembly = typeof(CodeEditor).Assembly; + foreach ((string? resource, string[]? extensions) in new[] { ("VelaShell.Plugin.DockerPanel.Syntax.Yaml.xshd", new[] { ".yaml", ".yml" }), @@ -317,13 +318,13 @@ private static void EnsureRegistered() { try { - using var stream = assembly.GetManifestResourceStream(resource); + using Stream? stream = assembly.GetManifestResourceStream(resource); if (stream is null) { continue; } using var reader = XmlReader.Create(stream); - var definition = HighlightingLoader.Load(reader, HighlightingManager.Instance); + IHighlightingDefinition definition = HighlightingLoader.Load(reader, HighlightingManager.Instance); HighlightingManager.Instance.RegisterHighlighting(definition.Name, extensions, definition); } catch (Exception) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs index d813a6a..71d0d1f 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ColumnResizer.cs @@ -1,9 +1,9 @@ +using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; -using System.Globalization; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -39,7 +39,7 @@ public ColumnResizer(Control host, string headerName, double chrome) _header = host.GetControl(headerName); _chrome = chrome; // 轨道在 XAML 里只写 Classes 与 Tag,事件在这里挂 —— 四个页面就不必各抄一遍转发方法。 - foreach (var grip in _header.Children.OfType().Where(b => b.Tag is string)) + foreach (Border? grip in _header.Children.OfType().Where(b => b.Tag is string)) { grip.PointerPressed += OnPressed; grip.PointerReleased += OnReleased; @@ -66,7 +66,7 @@ private void OnPressed(object? sender, PointerPressedEventArgs e) _active = key; _startX = e.GetPosition(_host).X; _startWidths.Clear(); - foreach (var other in columns.Keys) + foreach (string other in columns.Keys) { _startWidths[other] = columns.Get(other); } @@ -77,11 +77,11 @@ private void OnPressed(object? sender, PointerPressedEventArgs e) private void OnMoved(object? sender, PointerEventArgs e) { if (_active is not { } key || Columns is not { } columns || - !_startWidths.TryGetValue(key, out var start)) + !_startWidths.TryGetValue(key, out double start)) { return; } - var delta = e.GetPosition(_host).X - _startX; + double delta = e.GetPosition(_host).X - _startX; columns.Set(key, Math.Clamp(start + delta, columns.Min(key), Max(columns, key))); e.Handled = true; } @@ -103,8 +103,8 @@ private void OnReleased(object? sender, PointerReleasedEventArgs e) /// private double Max(ListColumns columns, string key) { - var others = columns.Keys.Where(k => k != key).Sum(columns.Get); - var available = _header.Bounds.Width > 0 ? _header.Bounds.Width : _host.Bounds.Width; + double others = columns.Keys.Where(k => k != key).Sum(columns.Get); + double available = _header.Bounds.Width > 0 ? _header.Bounds.Width : _host.Bounds.Width; return Math.Max(columns.Min(key), available - others - (columns.Keys.Count * ListColumns.TrackWidth) - _chrome); } @@ -122,12 +122,12 @@ private void AutoFit(ListColumns columns, string key, Border grip) { return; } - var size = Resource(key is "name" or "repo" ? "VelaFontSize12" : "VelaFontSize11") as double? ?? 11; + double size = Resource(key is "name" or "repo" ? "VelaFontSize12" : "VelaFontSize11") as double? ?? 11; Typeface mono = new(Resource("VelaUiMonoFont") as FontFamily ?? FontFamily.Default); Typeface ui = new(FontFamily.Default); // 表头那几个字也要装得下。文字直接从列头里读,免得把同一串标题在代码里再写一遍。 - var widest = Measure(HeaderText(grip), ui, size); - foreach (var text in page.ColumnTexts(key)) + double widest = Measure(HeaderText(grip), ui, size); + foreach (string text in page.ColumnTexts(key)) { widest = Math.Max(widest, Measure(text, key is "name" or "repo" ? ui : mono, size)); } @@ -146,5 +146,5 @@ private static double Measure(string text, Typeface typeface, double size) => typeface, size, Brushes.Black).Width; private object? Resource(string key) => - _host.TryFindResource(key, (_host as StyledElement)?.ActualThemeVariant, out var value) ? value : null; + _host.TryFindResource(key, (_host as StyledElement)?.ActualThemeVariant, out object? value) ? value : null; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs index eb9ec7d..01298c6 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/CommandPalette.cs @@ -144,7 +144,7 @@ public void Move(int delta) return; } // 环绕:到底了按下一次回到第一条,比停在那里不动更符合预期。 - var next = (_selectedIndex + delta + Items.Count) % Items.Count; + int next = (_selectedIndex + delta + Items.Count) % Items.Count; Select(next); } @@ -166,7 +166,7 @@ public void Complete() private void Select(int index) { - for (var i = 0; i < Items.Count; i++) + for (int i = 0; i < Items.Count; i++) { Items[i].Active = i == index; } @@ -193,13 +193,13 @@ private async Task RunAsync(PaletteItem item) private void ApplyFilter() { Items.Clear(); - var needle = Query.Trim(); + string needle = Query.Trim(); List matched = needle.Length == 0 ? [.. _all] : [.. _all.Where(e => Matches(e, needle)).OrderByDescending(e => Score(e, needle))]; string? group = null; - foreach (var entry in matched) + foreach (PaletteEntry entry in matched) { var item = new PaletteItem(entry); if (entry.Group != group) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs index a2a1962..6125bca 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGate.cs @@ -241,7 +241,7 @@ public string RemainingHint { return ""; } - var typed = TypedWord.Trim(); + string typed = TypedWord.Trim(); return typed == request.ConfirmWord ? "可以确认了" : request.ConfirmWord.StartsWith(typed, StringComparison.Ordinal) @@ -272,22 +272,22 @@ public Task AskAsync(ConfirmRequest request) TypedWord = ""; Precaution = request.PrecautionDefault; Commands.Clear(); - foreach (var command in request.Commands) + foreach (string command in request.Commands) { Commands.Add(command); } Targets.Clear(); - foreach (var target in request.Targets) + foreach (ConfirmTarget target in request.Targets) { Targets.Add(target); } Consequences.Clear(); - foreach (var consequence in request.Consequences) + foreach (ConfirmConsequence consequence in request.Consequences) { Consequences.Add(consequence); } DataLossPoints.Clear(); - foreach (var point in request.DataLossPoints) + foreach (string point in request.DataLossPoints) { DataLossPoints.Add(point); } @@ -301,13 +301,13 @@ public Task AskAsync(ConfirmRequest request) private void Complete(bool confirmed) { - var pending = _pending; + TaskCompletionSource? pending = _pending; _pending = null; Ui.Post(() => { Request = null; TypedWord = ""; }); - pending?.TrySetResult(confirmed); + _ = (pending?.TrySetResult(confirmed)); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs index e16a87e..0ccdfc7 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ConfirmGateView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui; public sealed partial class ConfirmGateView : UserControl { /// 建视图。 - public ConfirmGateView() => AvaloniaXamlLoader.Load(this); + public ConfirmGateView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs index b8e99c3..5f682ba 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Converters.cs @@ -1,9 +1,9 @@ +using System.Globalization; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data.Converters; using Avalonia.Media; -using System.Globalization; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -191,17 +191,14 @@ public static class Converters /// ThemeVariant.Default 查,写在 ThemeDictionaries 分支下的那一族一个都查不到。 /// /// - internal static object? Lookup(string key) - { - return Application.Current is not { } app ? null : app.TryFindResource(key, app.ActualThemeVariant, out var value) ? value : null; - } + internal static object? Lookup(string key) => Application.Current is not { } app ? null : app.TryFindResource(key, app.ActualThemeVariant, out object? value) ? value : null; private sealed class ToneBrushConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var dim = (parameter as string) == "dim"; - var key = value switch + bool dim = (parameter as string) == "dim"; + string key = value switch { RowTone.Ok => dim ? "VelaShellGreenDim" : "VelaStatusConnected", RowTone.Warn => dim ? "VelaShellYellowDim" : "VelaWarning", @@ -220,7 +217,7 @@ private sealed class WiderThanConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is double width && !double.IsNaN(width) && - double.TryParse(parameter as string, NumberStyles.Float, CultureInfo.InvariantCulture, out var least) && + double.TryParse(parameter as string, NumberStyles.Float, CultureInfo.InvariantCulture, out double least) && width >= least; public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => @@ -262,7 +259,7 @@ private sealed class SourceBrushConverter : IValueConverter public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var index = value is int i && i >= 0 ? i : 0; + int index = value is int i && i >= 0 ? i : 0; return ThemeBrush(Palette[index % Palette.Length], Brushes.Gray); } @@ -289,8 +286,8 @@ private sealed class FeedbackBrushConverter : IValueConverter { public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var dim = (parameter as string) == "dim"; - var key = value switch + bool dim = (parameter as string) == "dim"; + string key = value switch { FeedbackKind.Success => dim ? "VelaShellGreenDim" : "VelaStatusConnected", FeedbackKind.Warning => dim ? "VelaShellYellowDim" : "VelaWarning", @@ -318,10 +315,7 @@ public sealed class IconLookupConverter : IValueConverter public static readonly IconLookupConverter Instance = new(); /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not string key || key.Length == 0 ? null : Converters.Lookup(key); - } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is not string key || key.Length == 0 ? null : Converters.Lookup(key); /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => @@ -339,7 +333,7 @@ public sealed class DataLossBorderConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaError" : "VelaBorderSecondary"; + string key = value is true ? "VelaError" : "VelaBorderSecondary"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -354,7 +348,7 @@ public sealed class ReadOnlyBackgroundConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaBgSurface" : "VelaBgInput"; + string key = value is true ? "VelaBgSurface" : "VelaBgInput"; return Converters.ThemeBrush(key, Brushes.Transparent); } @@ -369,7 +363,7 @@ public sealed class DangerTextConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaError" : "VelaTextTertiary"; + string key = value is true ? "VelaError" : "VelaTextTertiary"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -382,10 +376,7 @@ public object ConvertBack(object? value, Type targetType, object? parameter, Cul public sealed class FollowBackgroundConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellGreenDim", Brushes.Transparent)); - } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellGreenDim", Brushes.Transparent)); /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => @@ -398,7 +389,7 @@ public sealed class FollowForegroundConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaStatusConnected" : "VelaTextSecondary"; + string key = value is true ? "VelaStatusConnected" : "VelaTextSecondary"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -417,10 +408,7 @@ public object ConvertBack(object? value, Type targetType, object? parameter, Cul public sealed class MatchBackgroundConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellYellowDim", Brushes.Transparent)); - } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is not true ? Brushes.Transparent : (object)(Converters.ThemeBrush("VelaShellYellowDim", Brushes.Transparent)); /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => @@ -433,7 +421,7 @@ public sealed class LogLineForegroundConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaShellRed" : "VelaShellWhite"; + string key = value is true ? "VelaShellRed" : "VelaShellWhite"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -455,9 +443,9 @@ public sealed class LogBodyBrushConverter : IMultiValueConverter /// public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - var isError = values.Count > 0 && values[0] is true; - var level = values.Count > 1 && values[1] is LogLevel l ? l : LogLevel.None; - var key = isError || level == LogLevel.Error ? "VelaShellRed" + bool isError = values.Count > 0 && values[0] is true; + LogLevel level = values.Count > 1 && values[1] is LogLevel l ? l : LogLevel.None; + string key = isError || level == LogLevel.Error ? "VelaShellRed" : level == LogLevel.Warn ? "VelaWarning" : level == LogLevel.Debug ? "VelaTextTertiary" : "VelaShellWhite"; @@ -483,7 +471,7 @@ public sealed class AuthBrushConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaStatusConnected" : "VelaWarning"; + string key = value is true ? "VelaStatusConnected" : "VelaWarning"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -510,12 +498,12 @@ public sealed class OutputLineForegroundConverter : IMultiValueConverter /// public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) { - var isError = values.Count > 0 && values[0] is true; - var isCommand = values.Count > 1 && values[1] is true; - var level = values.Count > 2 && values[2] is LogLevel l ? l : LogLevel.None; + bool isError = values.Count > 0 && values[0] is true; + bool isCommand = values.Count > 1 && values[1] is true; + LogLevel level = values.Count > 2 && values[2] is LogLevel l ? l : LogLevel.None; // 命令本身 > 走 stderr > 正文里认出来的级别。最后这一档平时不会触发 // (compose 自己的输出没有级别),但 up 的时候服务把 ERROR 打到 stdout 是常有的事。 - var key = isCommand ? "VelaStatusConnected" + string key = isCommand ? "VelaStatusConnected" : isError || level == LogLevel.Error ? "VelaShellRed" : level == LogLevel.Warn ? "VelaWarning" : "VelaShellWhite"; @@ -527,10 +515,7 @@ public sealed class OutputLineForegroundConverter : IMultiValueConverter public sealed class ResourceBrushConverter : IValueConverter { /// - public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is not string key ? Brushes.Transparent : (object)(Converters.ThemeBrush(key, Brushes.Transparent)); - } + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value is not string key ? Brushes.Transparent : (object)(Converters.ThemeBrush(key, Brushes.Transparent)); /// public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => @@ -543,7 +528,7 @@ public sealed class PruneBorderConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is RowTone.Danger ? "VelaShellRedDim" : "VelaBorderPrimary"; + string key = value is RowTone.Danger ? "VelaShellRedDim" : "VelaBorderPrimary"; return Converters.ThemeBrush(key, Brushes.Gray); } @@ -558,7 +543,7 @@ public sealed class HotBrushConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - var key = value is true ? "VelaGaugeWarn" : "VelaGaugeCpu"; + string key = value is true ? "VelaGaugeWarn" : "VelaGaugeCpu"; return Converters.ThemeBrush(key, Brushes.Gray); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs index 82a4cbe..ebd2edf 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelView.axaml.cs @@ -57,11 +57,14 @@ private void TakeFocusForOverlay() { return; } - Focus(); + _ = Focus(); } /// 无参构造只为设计器与 XAML 装载器;运行时走带上下文的那个。 - public DockerPanelView() => InitializeComponent(); + public DockerPanelView() + { + InitializeComponent(); + } private void InitializeComponent() { @@ -135,7 +138,7 @@ protected override void OnKeyDown(KeyEventArgs e) base.OnKeyDown(e); return; } - var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); + bool ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); if (ctrl && e.Key == Key.K) { viewModel.OpenPaletteCommand.Execute(null); diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs index 40d0302..66ffe72 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Dialogs.cs @@ -47,7 +47,7 @@ public object? ActiveDialog return; } form.FormError = null; - foreach (var each in form.Fields) + foreach (FormField each in form.Fields) { each.Error = null; } @@ -127,10 +127,10 @@ public Task ShowFormAsync(PanelForm form) private void CompleteForm(bool confirmed) { - var pending = _formPending; + TaskCompletionSource? pending = _formPending; _formPending = null; Ui.Post(() => ActiveForm = null); - pending?.TrySetResult(confirmed); + _ = (pending?.TrySetResult(confirmed)); } /// 打开拉取镜像对话框。 @@ -150,7 +150,7 @@ public async Task ShowRunContainerAsync(string image) string[] networks; try { - var all = await client.ListNetworksAsync(Lifetime).ConfigureAwait(true); + NetworkSummary[] all = await client.ListNetworksAsync(Lifetime).ConfigureAwait(true); networks = [.. all.Select(n => n.Name).OrderBy(n => n, StringComparer.OrdinalIgnoreCase)]; } catch (Exception ex) when (ex is not OperationCanceledException) @@ -159,10 +159,10 @@ public async Task ShowRunContainerAsync(string image) } // 镜像的小字:大小与平台。表单里原来完全看不到自己要跑的是哪个镜像 // (只有最底下那条等效命令里出现过),而这颗按钮是从好几个地方点进来的。 - var detail = ""; + string detail = ""; try { - var images = await client.ListImagesAsync(false, Lifetime).ConfigureAwait(true); + ImageSummary[] images = await client.ListImagesAsync(false, Lifetime).ConfigureAwait(true); if (images.FirstOrDefault(i => i.RepoTags?.Contains(image) == true) is { } match) { detail = Humanize.Bytes(match.Size); @@ -178,21 +178,21 @@ public async Task ShowRunContainerAsync(string image) { return; } - var task = Tasks.Start("Icon.play", $"运行 {image}", indeterminate: true); + PanelTask task = Tasks.Start("Icon.play", $"运行 {image}", indeterminate: true); try { - var created = await client + CreateContainerResponse created = await client .CreateContainerAsync(form.ContainerName, form.ToRequest(), task.Token).ConfigureAwait(true); - foreach (var warning in created.Warnings ?? []) + foreach (string warning in created.Warnings ?? []) { - Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); + _ = Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); } if (form.Detach) { await client.StartContainerAsync(created.Id, task.Token).ConfigureAwait(true); } task.Finish(PanelTaskState.Succeeded, "完成", Humanize.ShortId(created.Id)); - Feedback.Notify(FeedbackKind.Success, + _ = Feedback.Notify(FeedbackKind.Success, form.Detach ? "容器已启动" : "容器已创建", $"{(form.ContainerName.Length > 0 ? form.ContainerName : Humanize.ShortId(created.Id))} · {image}"); await GoToAsync(PanelPage.Containers).ConfigureAwait(true); diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs index 2c4b373..0c19d9c 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.Events.cs @@ -57,7 +57,7 @@ private void StartEventStream() { _eventCts = CancellationTokenSource.CreateLinkedTokenSource(Lifetime); _eventsSince = DateTimeOffset.UtcNow; - var token = _eventCts.Token; + CancellationToken token = _eventCts.Token; _eventTask = Task.Run(() => EventLoopAsync(token), token); } @@ -94,7 +94,7 @@ private async Task EventLoopAsync(CancellationToken token) var backoff = TimeSpan.FromSeconds(1); while (!token.IsCancellationRequested) { - var client = Client; + DockerClient? client = Client; if (client is null) { return; @@ -146,7 +146,7 @@ private void OnDockerEvent(DockerEvent dockerEvent) Ui.Post(() => { Overview.AcceptEvent(dockerEvent); - var wanted = ActivePage?.WantsRefresh(dockerEvent) ?? false; + bool wanted = ActivePage?.WantsRefresh(dockerEvent) ?? false; // 总览页永远关心计数,即使它不在前台 —— 用户切回去时不该看到一份旧数字。 if (!wanted && !Overview.WantsRefresh(dockerEvent)) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs index 0362ac1..897023e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DockerPanelViewModel.cs @@ -446,9 +446,9 @@ private List CollectPaletteEntries() } // ── 动作:对具体容器的高频操作。放最前面,因为它们是"要做一件事"而不是"要看一眼"。 - foreach (var row in Containers.View.Where(r => r.IsRunning).Take(20)) + foreach (ContainerRow? row in Containers.View.Where(r => r.IsRunning).Take(20)) { - var target = row; + ContainerRow target = row; entries.Add(new("动作", $"重启 {target.Name}", DescribeContainer(target), "Docker.rotate-cw", RowTone.Ok, false, () => { Containers.RestartCommand.Execute(target); return Task.CompletedTask; })); entries.Add(new("动作", $"停止 {target.Name}", DescribeContainer(target), "Icon.square", @@ -456,24 +456,24 @@ private List CollectPaletteEntries() } // ── 容器 / 镜像 / 卷:导航到某个对象。 - foreach (var row in Containers.View.Take(40)) + foreach (ContainerRow? row in Containers.View.Take(40)) { - var target = row; + ContainerRow target = row; entries.Add(new("容器", target.Name, DescribeContainer(target), "Docker.box", target.Tone, false, () => Containers.OpenDetailCommand is { } open ? Task.Run(() => Ui.Post(() => open.Execute(target))) : Task.CompletedTask)); } - foreach (var row in Images.View.Take(40)) + foreach (ImageRow? row in Images.View.Take(40)) { - var target = row; + ImageRow target = row; entries.Add(new("镜像 / 卷", $"{target.Repository}:{target.Tag}", $"镜像 · {target.SizeText}", "Icon.layers", RowTone.Idle, false, () => { Images.OpenDetailCommand.Execute(target); return Task.CompletedTask; })); } - foreach (var row in Volumes.View.Take(40)) + foreach (VolumeRow? row in Volumes.View.Take(40)) { - var target = row; + VolumeRow target = row; entries.Add(new("镜像 / 卷", target.Name, $"卷 · {target.SizeText}", "Icon.hard-drive", RowTone.Idle, false, () => { Volumes.SelectCommand.Execute(target); return Task.CompletedTask; })); } @@ -487,7 +487,7 @@ private List CollectPaletteEntries() RowTone.Idle, true, () => { Images.PruneDanglingCommand.Execute(null); return Task.CompletedTask; })); entries.Add(new("面板命令", "打开设置", "连接、显示、行为", "Icon.settings", RowTone.Idle, false, () => { SettingsOpen = true; return Task.CompletedTask; })); - foreach ((var page, var title, var icon) in ((PanelPage, string, string)[]) + foreach ((PanelPage page, string? title, string? icon) in ((PanelPage, string, string)[]) [ (PanelPage.Overview, "总览", "Docker.layout-dashboard"), (PanelPage.Containers, "容器", "Docker.box"), @@ -497,7 +497,7 @@ private List CollectPaletteEntries() (PanelPage.System, "系统", "Icon.gauge") ]) { - var target = page; + PanelPage target = page; entries.Add(new("面板命令", $"转到{title}", "切换页面", icon, RowTone.Idle, false, () => GoToAsync(target))); } @@ -574,9 +574,9 @@ public async Task ReloadEndpointsAsync() [ new(DockerEndpoint.Local("本机 Docker"), true, "可用", FeedbackKind.Info) ]; - foreach (var session in sessions.OrderBy(s => s.Host, StringComparer.OrdinalIgnoreCase)) + foreach (SessionInfo? session in sessions.OrderBy(s => s.Host, StringComparer.OrdinalIgnoreCase)) { - var connected = session.State == SessionState.Connected; + bool connected = session.State == SessionState.Connected; items.Add(new( DockerEndpoint.Remote(session.SessionId, session.Host, $"{session.Username}@{session.Host}:{session.Port}"), connected, @@ -586,7 +586,7 @@ public async Task ReloadEndpointsAsync() Ui.Post(() => { Endpoints.Clear(); - foreach (var item in items) + foreach (EndpointItem item in items) { Endpoints.Add(item); } @@ -618,7 +618,7 @@ public async Task ConnectAsync(EndpointItem? item) // 点进去却是上一台机器的项目。 Compose = null; OnPropertyChanged(nameof(ComposeAvailable)); - foreach (var page in AllPages) + foreach (PageViewModel page in AllPages) { page.Reset(); } @@ -626,8 +626,8 @@ public async Task ConnectAsync(EndpointItem? item) State = PanelConnectionState.Connecting; RecoveryActions.Clear(); - var endpoint = item.Endpoint; - var remembered = await Settings.GetSocketPathAsync(endpoint.DisplayName, _lifetime.Token).ConfigureAwait(true); + DockerEndpoint endpoint = item.Endpoint; + string? remembered = await Settings.GetSocketPathAsync(endpoint.DisplayName, _lifetime.Token).ConfigureAwait(true); if (!string.IsNullOrWhiteSpace(remembered) && remembered != endpoint.SocketPath) { endpoint = endpoint with { SocketPath = remembered }; @@ -640,7 +640,7 @@ public async Task ConnectAsync(EndpointItem? item) var client = new DockerClient(endpoint, transport); try { - var version = await client.PingAsync(_lifetime.Token).ConfigureAwait(true); + SystemVersion version = await client.PingAsync(_lifetime.Token).ConfigureAwait(true); Client = client; RegistryAuth = new(Context.RemoteFs, endpoint); // compose 只有 CLI,所以要一条"跑命令"的通道:远端是 SSH,本机是本地进程。 @@ -679,7 +679,7 @@ private async Task ApplySocketPathAsync() { return; } - var path = SocketPathInput.Trim(); + string path = SocketPathInput.Trim(); if (path.Length == 0) { Feedback.Status(FeedbackKind.Warning, "socket 路径不能为空。"); @@ -706,18 +706,18 @@ public async Task SendToHostTerminalAsync(string command) { if (SelectedEndpoint?.Endpoint is not { Kind: DockerEndpointKind.Remote } endpoint) { - Feedback.Notify(FeedbackKind.Info, "本机端点没有终端可送", $"自己执行:{command}"); + _ = Feedback.Notify(FeedbackKind.Info, "本机端点没有终端可送", $"自己执行:{command}"); return; } try { await Context.Terminal.WriteAsync(endpoint.SessionId, command + "\n", _lifetime.Token) .ConfigureAwait(true); - Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); + _ = Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); } catch (PluginPermissionDeniedException) { - Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", $"可以自己执行:{command}"); + _ = Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", $"可以自己执行:{command}"); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -843,7 +843,7 @@ or DockerUnreachableReason.PermissionDenied or DockerUnreachableReason.Unknown /// private async Task RefineFailureAsync(EndpointItem item) { - var probe = await SocketProbe + SocketProbeResult probe = await SocketProbe .RunAsync(Context.RemoteExec, item.Endpoint, _lifetime.Token).ConfigureAwait(true); // 探测期间用户可能已经换了端点、或者又连上了 —— 那就别再动这一屏。 if (State != PanelConnectionState.Failed || !ReferenceEquals(SelectedEndpoint, item)) @@ -882,7 +882,7 @@ private async Task RefineFailureAsync(EndpointItem item) /// private void ShowTunnelBlocked(EndpointItem item) { - var path = item.Endpoint.SocketPath; + string path = item.Endpoint.SocketPath; RecoveryActions.Clear(); // 本机端点压根没有 SSH 这一层,上面那套话术套不上去。 if (item.Endpoint.Kind == DockerEndpointKind.Local) @@ -927,8 +927,8 @@ private void ShowTunnelBlocked(EndpointItem item) /// private void ShowPermissionDenied(EndpointItem item, SocketProbeResult probe) { - var account = probe.Account is { Length: > 0 } a ? a : "当前账号"; - var group = probe.Group is { Length: > 0 } g ? g : "docker"; + string account = probe.Account is { Length: > 0 } a ? a : "当前账号"; + string group = probe.Group is { Length: > 0 } g ? g : "docker"; ErrorIcon = "Docker.lock"; ErrorTitle = $"账号「{account}」还不被允许使用这台机器上的 Docker"; ErrorDetail = $"Docker 装着、也在跑,只是它只对 {group} 组开放,而 {account} 不在这个组里" + @@ -1063,7 +1063,7 @@ public async ValueTask DisposeAsync() Tasks.CancelAll(); await _lifetime.CancelAsync().ConfigureAwait(false); await StopEventStreamAsync().ConfigureAwait(false); - foreach (var page in AllPages) + foreach (PageViewModel page in AllPages) { if (page is IAsyncDisposable disposable) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs index 02f3aeb..4ee8a3c 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/DrawerLayout.cs @@ -1,5 +1,5 @@ -using Avalonia.Controls; using System.ComponentModel; +using Avalonia.Controls; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -75,9 +75,9 @@ private void Apply() { return; } - var list = _root.ColumnDefinitions[0]; - var splitter = _root.ColumnDefinitions[1]; - var panel = _root.ColumnDefinitions[2]; + ColumnDefinition list = _root.ColumnDefinitions[0]; + ColumnDefinition splitter = _root.ColumnDefinitions[1]; + ColumnDefinition panel = _root.ColumnDefinitions[2]; if (!drawer.IsOpen) { @@ -96,7 +96,7 @@ private void Apply() panel.Width = Fill; return; } - var max = Math.Max(DrawerState.MinWidth, _hostWidth - _listReserve); + double max = Math.Max(DrawerState.MinWidth, _hostWidth - _listReserve); list.Width = Fill; splitter.Width = Track; panel.MinWidth = DrawerState.MinWidth; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs index 26df2b8..4708815 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Feedback.cs @@ -1,6 +1,6 @@ -using Avalonia.Threading; using System.Collections.ObjectModel; using System.Net; +using Avalonia.Threading; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -111,7 +111,7 @@ public void Status(FeedbackKind kind, string text) => Ui.Post(() => public Toast Notify(FeedbackKind kind, string title, string detail, params ToastAction[] actions) { var toast = new Toast(kind, title, detail); - foreach (var action in actions) + foreach (ToastAction action in actions) { toast.Actions.Add(action); } @@ -126,7 +126,7 @@ public Toast Notify(FeedbackKind kind, string title, string detail, params Toast } if (toast.AutoDismiss) { - DispatcherTimer.RunOnce(() => Toasts.Remove(toast), TimeSpan.FromSeconds(4)); + _ = DispatcherTimer.RunOnce(() => Toasts.Remove(toast), TimeSpan.FromSeconds(4)); } }); return toast; @@ -147,19 +147,19 @@ public void ReportBatch(string verb, BatchResult result, bool inView, Action? on { if (result.AllSucceeded) { - var text = $"已{verb} {result.SucceededCount} 个"; + string text = $"已{verb} {result.SucceededCount} 个"; if (inView) { Status(FeedbackKind.Success, text); } else { - Notify(FeedbackKind.Success, text, ""); + _ = Notify(FeedbackKind.Success, text, ""); } return; } - var summary = $"已{verb} {result.SucceededCount} 个,{result.FailedCount} 个失败"; - var detail = string.Join('\n', result.Failures.Take(3).Select(f => $"{f.Target}:{f.Failure}")); + string summary = $"已{verb} {result.SucceededCount} 个,{result.FailedCount} 个失败"; + string detail = string.Join('\n', result.Failures.Take(3).Select(f => $"{f.Target}:{f.Failure}")); if (result.FailedCount > 3) { detail += $"\n…还有 {result.FailedCount - 3} 个"; @@ -170,7 +170,7 @@ public void ReportBatch(string verb, BatchResult result, bool inView, Action? on { actions.Add(new("查看详情", onShowDetail)); } - Notify(FeedbackKind.Warning, summary, detail, [.. actions]); + _ = Notify(FeedbackKind.Warning, summary, detail, [.. actions]); } /// @@ -192,13 +192,13 @@ public void ReportError(string what, Exception ex, params ToastAction[] actions) Status(FeedbackKind.Info, $"{what} 已取消"); return; } - var detail = ex switch + string detail = ex switch { DockerApiException api => Explain(api) is { Length: > 0 } why ? $"{why}\n{api.Message}" : api.Message, DockerUnreachableException unreachable => unreachable.Message, _ => ex.Message }; - Notify(FeedbackKind.Error, $"{what} 失败", detail, actions); + _ = Notify(FeedbackKind.Error, $"{what} 失败", detail, actions); } /// diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs index e61bc5c..8a1e7be 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/FilePicker.cs @@ -32,7 +32,7 @@ public static class FilePicker { return null; } - var picked = await storage.OpenFilePickerAsync(new() + IReadOnlyList picked = await storage.OpenFilePickerAsync(new() { Title = title, AllowMultiple = false diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs index bfd790e..2a11222 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Forms.cs @@ -51,7 +51,7 @@ public RenameContainerForm(string currentName, string composeProject) /// public override bool Validate() { - var value = _next.Value.Trim(); + string value = _next.Value.Trim(); if (value.Length == 0) { _next.Error = "名字不能为空。"; @@ -117,7 +117,7 @@ public RestartPolicyForm(string current, int maxRetries) public string Policy => _policy.Value; /// 最大重试次数。 - public int MaxRetries => int.TryParse(_retries.Value, out var n) ? Math.Max(0, n) : 0; + public int MaxRetries => int.TryParse(_retries.Value, out int n) ? Math.Max(0, n) : 0; /// public override bool Validate() @@ -376,28 +376,28 @@ public override bool Validate() /// protected override void UpdatePreview() { - var sb = new StringBuilder("docker network create -d ").Append(Driver); + StringBuilder sb = new StringBuilder("docker network create -d ").Append(Driver); if (Subnet.Length > 0) { - sb.Append(" --subnet ").Append(Subnet); + _ = sb.Append(" --subnet ").Append(Subnet); } if (Gateway.Length > 0) { - sb.Append(" --gateway ").Append(Gateway); + _ = sb.Append(" --gateway ").Append(Gateway); } if (Internal) { - sb.Append(" --internal"); + _ = sb.Append(" --internal"); } if (Attachable) { - sb.Append(" --attachable"); + _ = sb.Append(" --attachable"); } if (EnableIPv6) { - sb.Append(" --ipv6"); + _ = sb.Append(" --ipv6"); } - sb.Append(' ').Append(Name.Length > 0 ? Name : "<名称>"); + _ = sb.Append(' ').Append(Name.Length > 0 ? Name : "<名称>"); CommandPreview = "POST /networks/create"; CommandNote = $"等价于 {sb}"; } @@ -416,7 +416,7 @@ public ConnectNetworkForm(string network, { Fields.Add(new TextField("目标网络") { Value = network, ReadOnly = true }); Containers = new("选择容器") { Placeholder = "过滤未接入的容器…" }; - foreach ((var id, var name, var meta, var enabled, var reason) in candidates) + foreach ((string? id, string? name, string? meta, bool enabled, string? reason) in candidates) { Containers.Items.Add(new(id, name, meta, enabled, reason)); } @@ -624,7 +624,7 @@ public ExecUserForm(string currentUser, string currentShell) /// protected override void UpdatePreview() { - var user = User.Length > 0 ? $"&User={User}" : ""; + string user = User.Length > 0 ? $"&User={User}" : ""; CommandPreview = $"POST /containers/{{id}}/exec Cmd=[{Shell}]{user}"; CommandNote = "等价于 docker exec -it " + (User.Length > 0 ? $"-u {User} " : "") + $"<容器> {Shell}"; } @@ -680,7 +680,7 @@ public override bool Validate() /// protected override void UpdatePreview() { - var dir = WorkingDir.Length > 0 ? $"&WorkingDir={WorkingDir}" : ""; + string dir = WorkingDir.Length > 0 ? $"&WorkingDir={WorkingDir}" : ""; CommandPreview = $"POST /containers/{{id}}/exec Cmd=[shell]{dir}"; CommandNote = "等价于 docker exec -it " + (WorkingDir.Length > 0 ? $"-w {WorkingDir} " : "") + "<容器> "; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs index 142e13e..3780c62 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Glyph.cs @@ -41,7 +41,7 @@ static Glyph() { AffectsRender(DataProperty, BrushProperty, SizeProperty); AffectsMeasure(SizeProperty); - KeyProperty.Changed.AddClassHandler((glyph, _) => glyph.ResolveKey()); + _ = KeyProperty.Changed.AddClassHandler((glyph, _) => glyph.ResolveKey()); // 图标是定尺寸的,默认的 Stretch 对它没有意义:排版会把它拉成整行高, // 而 Render 从原点开画 —— 于是图标贴在行顶,旁边居中的文字与它错开一截。 // 这是"图标和文字不在一个水平中轴上"的根因,逐处补 VerticalAlignment 补不干净。 @@ -94,7 +94,7 @@ private void ResolveKey() { return; } - if (this.TryFindResource(key, out var resource) && resource is Geometry geometry) + if (this.TryFindResource(key, out object? resource) && resource is Geometry geometry) { Data = geometry; } @@ -107,11 +107,11 @@ public override void Render(DrawingContext context) { return; } - var scale = Size / ViewBox; + double scale = Size / ViewBox; // 在实际分到的框里居中再画。默认对齐下这一步是零位移(框正好是 Size×Size); // 但调用方显式写了 Stretch、或父容器给了更大的框时,图标仍然待在正中, // 而不是缩在左上角。 - var transform = Matrix.CreateScale(scale, scale) * + Matrix transform = Matrix.CreateScale(scale, scale) * Matrix.CreateTranslation((Bounds.Width - Size) / 2, (Bounds.Height - Size) / 2); using (context.PushTransform(transform)) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs index 83335fd..f536a14 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/LineBuffer.cs @@ -1,5 +1,5 @@ -using Avalonia.Threading; using System.Collections.ObjectModel; +using Avalonia.Threading; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -79,7 +79,7 @@ private void OnTick(object? sender, EventArgs e) batch = [.. _pending]; _pending.Clear(); } - foreach (var item in batch) + foreach (T? item in batch) { target.Add(item); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs index 16f9792..04530c0 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ListColumns.cs @@ -40,7 +40,7 @@ public abstract class ListColumns : ObservableObject /// 用户拖出来的列宽只可能是像素值 —— 星形和 Auto 在这里没有意义。 protected GridLength Clamp(GridLength value, string key) { - var min = Min(key); + double min = Min(key); return new(Math.Max(min, value.IsAbsolute ? value.Value : min)); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs index 96ed4ca..3e9c6a0 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/LogLevel.cs @@ -51,7 +51,7 @@ public static LogLevel Detect(string text) { return fromJson; } - var head = text.AsSpan(0, Math.Min(text.Length, HeadWindow)); + ReadOnlySpan head = text.AsSpan(0, Math.Min(text.Length, HeadWindow)); return HasWord(head, "ERROR") || HasWord(head, "FATAL") || HasWord(head, "PANIC") ? LogLevel.Error : HasWord(head, "WARN") || HasWord(head, "WARNING") ? LogLevel.Warn : HasWord(head, "DEBUG") || HasWord(head, "TRACE") ? LogLevel.Debug @@ -69,18 +69,18 @@ public static LogLevel Detect(string text) /// private static bool HasWord(ReadOnlySpan haystack, ReadOnlySpan word) { - var from = 0; + int from = 0; while (from < haystack.Length) { - var at = haystack[from..].IndexOf(word, StringComparison.OrdinalIgnoreCase); + int at = haystack[from..].IndexOf(word, StringComparison.OrdinalIgnoreCase); if (at < 0) { return false; } at += from; - var after = at + word.Length; - var leftOk = at == 0 || !char.IsLetterOrDigit(haystack[at - 1]); - var rightOk = after >= haystack.Length || !char.IsLetterOrDigit(haystack[after]); + int after = at + word.Length; + bool leftOk = at == 0 || !char.IsLetterOrDigit(haystack[at - 1]); + bool rightOk = after >= haystack.Length || !char.IsLetterOrDigit(haystack[after]); if (leftOk && rightOk) { return true; @@ -92,16 +92,16 @@ private static bool HasWord(ReadOnlySpan haystack, ReadOnlySpan word private static LogLevel? TryJsonLevel(string text) { - foreach (var key in (string[])["\"level\"", "\"lvl\"", "\"severity\""]) + foreach (string key in (string[])["\"level\"", "\"lvl\"", "\"severity\""]) { - var at = text.IndexOf(key, StringComparison.OrdinalIgnoreCase); + int at = text.IndexOf(key, StringComparison.OrdinalIgnoreCase); if (at < 0) { continue; } // 取键之后一小段,足够覆盖 `: "error"` 这种带空格的写法。 - var from = at + key.Length; - var value = text.AsSpan(from, Math.Min(24, text.Length - from)); + int from = at + key.Length; + ReadOnlySpan value = text.AsSpan(from, Math.Min(24, text.Length - from)); if (Contains(value, "error") || Contains(value, "fatal") || Contains(value, "panic")) { return LogLevel.Error; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs index 3c1d7e1..19fa3af 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Mvvm.cs @@ -1,8 +1,8 @@ -using Avalonia.Threading; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; +using Avalonia.Threading; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -71,7 +71,7 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) /// 触发一组通知。 protected void OnPropertiesChanged(params string[] propertyNames) { - foreach (var name in propertyNames) + foreach (string name in propertyNames) { OnPropertyChanged(name); } @@ -201,32 +201,32 @@ public sealed class KeyedCollection(Func keySelector) : Observable public void Merge(IReadOnlyList snapshot, Action update) { Dictionary existing = []; - foreach (var item in this) + foreach (T? item in this) { existing[keySelector(item)] = item; } - for (var i = 0; i < snapshot.Count; i++) + for (int i = 0; i < snapshot.Count; i++) { - var incoming = snapshot[i]; - var key = keySelector(incoming); - if (existing.TryGetValue(key, out var current)) + T? incoming = snapshot[i]; + string key = keySelector(incoming); + if (existing.TryGetValue(key, out T? current)) { update(current, incoming); - var at = IndexOf(current); + int at = IndexOf(current); if (at != i && at >= 0) { Move(at, i); } - existing.Remove(key); + _ = existing.Remove(key); } else { Insert(i, incoming); } } - foreach (var stale in existing.Values) + foreach (T? stale in existing.Values) { - Remove(stale); + _ = Remove(stale); } } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs index 3e5815b..78c92b6 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ComposePageViewModel.cs @@ -1,5 +1,5 @@ -using Avalonia.Controls; using System.Collections.ObjectModel; +using Avalonia.Controls; using VelaShell.Plugin.DockerPanel.Docker; using VelaShell.PluginSdk.RemoteExec; @@ -76,18 +76,18 @@ public static class MergedLog /// 这个名字是不是本项目的服务 / 容器。 public static (string Source, string Body) Split(string line, Func known) { - var bar = line.IndexOf('|'); + int bar = line.IndexOf('|'); if (bar is <= 0 or > MaxNameLength) { return ("", line); } - var name = line[..bar].TrimEnd(); + string name = line[..bar].TrimEnd(); if (name.Length == 0 || name.AsSpan().ContainsAny(' ', '\t') || !known(name)) { return ("", line); } // 竖线后面 compose 固定跟一个空格;没有也无所谓,别把正文的第一个字符吃掉。 - var body = line[(bar + 1)..]; + string body = line[(bar + 1)..]; return (name, body.StartsWith(' ') ? body[1..] : body); } } @@ -456,12 +456,12 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) try { ComposeAvailable = await compose.IsAvailableAsync(cancellationToken).ConfigureAwait(true); - var projects = ComposeAvailable + ComposeProject[] projects = ComposeAvailable ? await compose.ListProjectsAsync(cancellationToken).ConfigureAwait(true) : []; - var keep = Selected?.Name; + string? keep = Selected?.Name; Projects.Clear(); - foreach (var project in projects.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) + foreach (ComposeProject? project in projects.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) { Projects.Add(project); } @@ -579,8 +579,8 @@ private async Task SaveEnvAsync() { return; } - var path = ComposeCli.EnvPath(project); - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + string path = ComposeCli.EnvPath(project); + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"覆盖 {path}?", Icon = "Docker.shield-alert", @@ -611,7 +611,7 @@ private async Task SaveEnvAsync() OnPropertiesChanged(nameof(EnvModified), nameof(EnvModifiedText)); HasEnv = true; Log($"✔ 已写回 {path}"); - Shell.Feedback.Notify(FeedbackKind.Success, "已写回 .env", + _ = Shell.Feedback.Notify(FeedbackKind.Success, "已写回 .env", "已在跑的容器要 up -d 重建才会读到新值。"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -641,7 +641,7 @@ private async Task RunForServiceAsync(string arguments, ComposeService service, Log($"$ docker compose {ProjectPrefix} {arguments} {service.Service}", isCommand: true); try { - var exit = await compose.RunForServiceAsync(project, Argv(arguments), service.Service, + int exit = await compose.RunForServiceAsync(project, Argv(arguments), service.Service, new DirectProgress(output => Log(output.Line, output.Stream == ExecStream.StandardError)), Shell.Lifetime).ConfigureAwait(true); @@ -667,7 +667,7 @@ private async Task RunForServiceAsync(string arguments, ComposeService service, private async Task OpenServiceTerminalAsync(ComposeService service) { await Shell.GoToAsync(PanelPage.Containers).ConfigureAwait(true); - var row = Shell.Containers.View.FirstOrDefault(r => r.Name == service.Name); + ContainerRow? row = Shell.Containers.View.FirstOrDefault(r => r.Name == service.Name); if (row is null) { Shell.Feedback.Status(FeedbackKind.Warning, $"容器列表里没有 {service.Name} —— 它可能还没起来。"); @@ -695,14 +695,14 @@ private async Task ToggleLogsAsync() return; } _logsCts = CancellationTokenSource.CreateLinkedTokenSource(Shell.Lifetime); - var token = _logsCts.Token; + CancellationToken token = _logsCts.Token; LogsFollowing = true; - var tail = Shell.Settings.LogTail; + string tail = Shell.Settings.LogTail; _ = Task.Run(async () => { try { - await compose.FollowLogsAsync(project, tail == "all" ? "all" : tail, + _ = await compose.FollowLogsAsync(project, tail == "all" ? "all" : tail, new DirectProgress(output => _logs.Add(MergedLine(output))), token) .ConfigureAwait(false); } @@ -727,7 +727,7 @@ await compose.FollowLogsAsync(project, tail == "all" ? "all" : tail, /// private OutputLine MergedLine(ExecOutput output) { - (var source, var body) = MergedLog.Split(output.Line, IsKnownSource); + (string? source, string? body) = MergedLog.Split(output.Line, IsKnownSource); return new(DateTimeOffset.Now.ToString("HH:mm:ss"), body, output.Stream == ExecStream.StandardError, false, source, SourceIndex(source)); } @@ -741,7 +741,7 @@ private OutputLine MergedLine(ExecOutput output) /// private bool IsKnownSource(string name) { - foreach (var service in Services) + foreach (ComposeService service in Services) { if (service.Service == name || service.Name == name) { @@ -757,7 +757,7 @@ private int SourceIndex(string source) { return 0; } - for (var i = 0; i < Services.Count; i++) + for (int i = 0; i < Services.Count; i++) { if (Services[i].Service == source || Services[i].Name == source) { @@ -765,7 +765,7 @@ private int SourceIndex(string source) } } // 服务列表里没有的来源(还没刷新到的、一次性任务),各自也要一个稳定的序号。 - if (!_extraSources.TryGetValue(source, out var extra)) + if (!_extraSources.TryGetValue(source, out int extra)) { extra = Services.Count + _extraSources.Count; _extraSources[source] = extra; @@ -802,13 +802,13 @@ private async Task NewProjectAsync() { return; } - var path = ComposePath.Combine(form.Directory, "compose.yaml"); + string path = ComposePath.Combine(form.Directory, "compose.yaml"); try { await compose.WriteFileAsync(path, NewComposeProjectForm.Skeleton(form.ProjectName), Shell.Lifetime) .ConfigureAwait(true); Log($"✔ 已创建 {path}"); - Shell.Feedback.Notify(FeedbackKind.Success, "项目已创建", + _ = Shell.Feedback.Notify(FeedbackKind.Success, "项目已创建", $"{path} —— 改完 compose.yaml 之后按 up -d 起它。"); await OpenPathAsync(path).ConfigureAwait(true); } @@ -824,9 +824,9 @@ private async Task LoadServicesAsync(ComposeProject project, CancellationToken c { return; } - var services = await compose.ListServicesAsync(project, cancellationToken).ConfigureAwait(true); + ComposeService[] services = await compose.ListServicesAsync(project, cancellationToken).ConfigureAwait(true); Services.Clear(); - foreach (var service in services) + foreach (ComposeService service in services) { Services.Add(service); } @@ -865,11 +865,11 @@ private async Task RunAsync(string arguments, string title) return; } Running = true; - var task = Shell.Tasks.Start("Docker.boxes", $"{title} · {project.Name}", indeterminate: true); + PanelTask task = Shell.Tasks.Start("Docker.boxes", $"{title} · {project.Name}", indeterminate: true); Log($"$ docker compose {ProjectPrefix} {arguments}", isCommand: true); try { - var exit = await compose.RunAsync(project, Argv(arguments), + int exit = await compose.RunAsync(project, Argv(arguments), new DirectProgress(output => Log(output.Line, output.Stream == ExecStream.StandardError)), task.Token).ConfigureAwait(true); @@ -887,7 +887,7 @@ private async Task RunAsync(string arguments, string title) Log($"✘ {title} 失败 · 退出码 {exit}", isError: true); // 执行记录就在 Compose 页右侧那一栏 —— 把用户送过去并选中出事的那个项目。 // 原来这颗按钮挂的是一个空 lambda:点了什么都不会发生。 - Shell.Feedback.Notify(FeedbackKind.Error, $"{title} 失败", $"{project.Name} · 退出码 {exit}", + _ = Shell.Feedback.Notify(FeedbackKind.Error, $"{title} 失败", $"{project.Name} · 退出码 {exit}", new ToastAction("查看执行记录", () => _ = ShowOutputAsync(project))); } await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); @@ -917,7 +917,7 @@ private async Task DownAsync(bool withVolumes) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = withVolumes ? $"down -v 项目 {project.Name}?" : $"down 项目 {project.Name}?", Icon = withVolumes ? "Docker.shield-alert" : "Icon.trash-2", @@ -961,7 +961,7 @@ private async Task ShowConfigAsync() Config = "正在展开…"; try { - var result = await compose.ConfigAsync(project, Shell.Lifetime).ConfigureAwait(true); + ExecResult result = await compose.ConfigAsync(project, Shell.Lifetime).ConfigureAwait(true); // 展开的结果进它自己那一页,不再倒进执行记录 —— 几百行 YAML 会把记录冲干净。 Config = result.IsSuccess ? result.Output : result.Error; Log(result.IsSuccess ? "✔ 配置可以解析 —— 语法没问题" : "✘ 配置有问题(见 config 页签)", @@ -981,7 +981,7 @@ private async Task SaveYamlAsync() { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"覆盖 {project.PrimaryFile}?", Icon = "Docker.shield-alert", @@ -1009,7 +1009,7 @@ private async Task SaveYamlAsync() await compose.WriteFileAsync(project.PrimaryFile, Yaml, Shell.Lifetime).ConfigureAwait(true); _originalYaml = Yaml; OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText)); - Shell.Feedback.Notify(FeedbackKind.Success, "已写回", project.PrimaryFile); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "已写回", project.PrimaryFile); Log($"✔ 已写回 {project.PrimaryFile}"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -1037,8 +1037,8 @@ private async Task OpenPathAsync(string filePath, string? projectName = null) { // compose 的项目名默认取项目目录名 —— 与 compose 自己的规则一致, // 不一致的话面板起的容器会带上一个和命令行不同的前缀。 - var directory = filePath[..Math.Max(0, filePath.LastIndexOf('/'))]; - var name = projectName is { Length: > 0 } given + string directory = filePath[..Math.Max(0, filePath.LastIndexOf('/'))]; + string name = projectName is { Length: > 0 } given ? given : directory[(directory.LastIndexOf('/') + 1)..]; var project = new ComposeProject(name, "(未起过)", filePath); @@ -1172,13 +1172,7 @@ public NewComposeProjectForm(bool isLocal) public string Directory => _directory.Value.Trim(); /// 项目名。 - public string ProjectName - { - get - { - return _name.Value.Trim() is { Length: > 0 } explicitName ? explicitName : ComposePath.LastSegment(Directory); - } - } + public string ProjectName => _name.Value.Trim() is { Length: > 0 } explicitName ? explicitName : ComposePath.LastSegment(Directory); /// 骨架内容。注释里写清下一步该干什么,而不是丢一个空文件给用户。 public static string Skeleton(string projectName) => diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs index e2e0971..b430149 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class ContainerDetailView : UserControl { /// 建视图。 - public ContainerDetailView() => AvaloniaXamlLoader.Load(this); + public ContainerDetailView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs index fa76f22..f63f54c 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerDetailViewModel.cs @@ -349,9 +349,9 @@ private async Task CommitAsync() } try { - var result = await client.CommitContainerAsync(ContainerId, form.Repository, form.Tag, + CommitResponse result = await client.CommitContainerAsync(ContainerId, form.Repository, form.Tag, form.Comment, author: null, form.Pause, _shell.Lifetime).ConfigureAwait(true); - _shell.Feedback.Notify(FeedbackKind.Success, "已提交为镜像", + _ = _shell.Feedback.Notify(FeedbackKind.Success, "已提交为镜像", $"{form.Repository}:{form.Tag} · {Humanize.ShortId(result.Id)}"); await _shell.Images.RefreshAsync(_shell.Lifetime).ConfigureAwait(true); } @@ -482,7 +482,7 @@ private void BuildOverview() Basics.Add(new("特权模式", "是 —— 容器可以做几乎任何宿主能做的事", RowTone.Danger)); } - foreach (var port in _row.Summary.Ports ?? []) + foreach (DockerPort port in _row.Summary.Ports ?? []) { PortLines.Add(port.PublicPort > 0 ? new($"{port.PrivatePort}/{port.Type ?? "tcp"}", @@ -491,9 +491,9 @@ private void BuildOverview() : new($"{port.PrivatePort}/{port.Type ?? "tcp"}", "仅容器内可见(未发布)")); } - foreach (var mount in inspect.Mounts ?? []) + foreach (DockerMount mount in inspect.Mounts ?? []) { - var source = mount.Type == "volume" ? mount.Name ?? mount.Source ?? "" : mount.Source ?? ""; + string source = mount.Type == "volume" ? mount.Name ?? mount.Source ?? "" : mount.Source ?? ""; Mounts.Add(new( mount.Type == "volume" ? "Docker.database" : "Icon.folder", $"{source} → {mount.Destination}", @@ -501,14 +501,14 @@ private void BuildOverview() !mount.RW)); } - foreach ((var name, var endpoint) in inspect.NetworkSettings?.Networks ?? []) + foreach ((string? name, EndpointSettings? endpoint) in inspect.NetworkSettings?.Networks ?? []) { Networks.Add(new(name, endpoint.IPAddress is { Length: > 0 } ip ? ip : "(未分配)", RowTone.Ok)); } - foreach (var entry in inspect.Config?.Env ?? []) + foreach (string entry in inspect.Config?.Env ?? []) { - var equals = entry.IndexOf('='); + int equals = entry.IndexOf('='); Environment.Add(equals > 0 ? new(entry[..equals], entry[(equals + 1)..]) : new(entry, "")); @@ -568,10 +568,10 @@ private async Task RefreshProcessesAsync(CancellationToken cancellationToken) } try { - var top = await client.TopAsync(ContainerId, cancellationToken: cancellationToken) + ContainerTopResult top = await client.TopAsync(ContainerId, cancellationToken: cancellationToken) .ConfigureAwait(true); Processes.Clear(); - foreach (var row in ProcessTable.Normalize(top)) + foreach (ProcessRow row in ProcessTable.Normalize(top)) { Processes.Add(row); } @@ -599,8 +599,8 @@ private void StartStatsStream() } StopStatsStream(); _statsCts = CancellationTokenSource.CreateLinkedTokenSource(_shell.Lifetime); - var token = _statsCts.Token; - var id = ContainerId; + CancellationToken token = _statsCts.Token; + string id = ContainerId; _ = Task.Run(async () => { try diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs index a5dbc17..99b64aa 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesView.axaml.cs @@ -43,11 +43,11 @@ protected override void OnLoaded(RoutedEventArgs e) private static void ReportCaret(ContainerFilesViewModel viewModel, TextBox editor) { - var text = editor.Text ?? ""; - var caret = Math.Clamp(editor.CaretIndex, 0, text.Length); - var line = 1; - var lineStart = 0; - for (var i = 0; i < caret; i++) + string text = editor.Text ?? ""; + int caret = Math.Clamp(editor.CaretIndex, 0, text.Length); + int line = 1; + int lineStart = 0; + for (int i = 0; i < caret; i++) { if (text[i] == '\n') { @@ -60,7 +60,7 @@ private static void ReportCaret(ContainerFilesViewModel viewModel, TextBox edito private void OnDragOver(object? sender, DragEventArgs e) { - var acceptable = DataContext is ContainerFilesViewModel { CanPickFiles: true } + bool acceptable = DataContext is ContainerFilesViewModel { CanPickFiles: true } && e.DataTransfer.TryGetFiles()?.Length > 0; e.DragEffects = acceptable ? DragDropEffects.Copy : DragDropEffects.None; e.Handled = true; @@ -73,7 +73,7 @@ private void OnDrop(object? sender, DragEventArgs e) return; } // 只接第一个:一次拖十个文件进容器,失败到一半留下的半套文件比什么都不做更糟。 - var file = e.DataTransfer.TryGetFiles()?.OfType().FirstOrDefault(); + IStorageFile? file = e.DataTransfer.TryGetFiles()?.OfType().FirstOrDefault(); if (file is not null) { _ = viewModel.UploadDroppedAsync(file); diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs index b65cd1b..846e9cd 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerFilesViewModel.cs @@ -1,6 +1,6 @@ -using Avalonia.Platform.Storage; using System.Collections.ObjectModel; using System.Text; +using Avalonia.Platform.Storage; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -219,7 +219,7 @@ public string ModifiedLinesText { return "未修改"; } - var changed = LineDiff.CountChanged(LineDiff.Compute(_originalText, EditorText)); + int changed = LineDiff.CountChanged(LineDiff.Compute(_originalText, EditorText)); return $"已修改 {changed} 行"; } } @@ -250,8 +250,8 @@ var p when p.Contains("/redis", StringComparison.Ordinal) => "redis-cli CONFIG S private static string GuessLanguage(string name) { - var dot = name.LastIndexOf('.'); - var ext = dot > 0 ? name[(dot + 1)..].ToLowerInvariant() : ""; + int dot = name.LastIndexOf('.'); + string ext = dot > 0 ? name[(dot + 1)..].ToLowerInvariant() : ""; return ext switch { "conf" or "cnf" or "ini" => name.Contains("nginx", StringComparison.OrdinalIgnoreCase) ? "nginx" : "ini", @@ -362,9 +362,9 @@ public string ModifiedText { return "未修改"; } - var before = _originalText.Split('\n').Length; - var after = EditorText.Split('\n').Length; - var delta = after - before; + int before = _originalText.Split('\n').Length; + int after = EditorText.Split('\n').Length; + int delta = after - before; return delta switch { > 0 => $"未保存 · +{delta} 行", @@ -421,8 +421,8 @@ private async Task DownloadAsync(FileEntryItem item) { return; } - var suggested = item.IsDirectory ? $"{item.Name}.tar" : item.Name; - var target = await FilePicker + string suggested = item.IsDirectory ? $"{item.Name}.tar" : item.Name; + IStorageFile? target = await FilePicker .PickSaveAsync(item.IsDirectory ? $"把 {item.Name}/ 存成 tar" : $"保存 {item.Name}", suggested, item.IsDirectory ? "tar" : null) .ConfigureAwait(true); @@ -434,9 +434,9 @@ private async Task DownloadAsync(FileEntryItem item) Error = ""; try { - await using var archive = await client.DownloadArchiveAsync(containerId, item.FullPath, shell.Lifetime) + await using Stream archive = await client.DownloadArchiveAsync(containerId, item.FullPath, shell.Lifetime) .ConfigureAwait(true); - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); long written; if (item.IsDirectory) { @@ -448,7 +448,7 @@ private async Task DownloadAsync(FileEntryItem item) // 单文件走流式解 tar:整份读进内存的话,一个 2 GB 的日志会把宿主拖垮。 written = await TarUtil.ExtractFirstFileAsync(archive, output, shell.Lifetime).ConfigureAwait(true); } - shell.Feedback.Notify(FeedbackKind.Success, "已取到本地", + _ = shell.Feedback.Notify(FeedbackKind.Success, "已取到本地", $"{target.Name}{(written > 0 ? $" · {Humanize.Bytes(written)}" : "")}"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -487,9 +487,9 @@ private void RebuildEditorLines() { return; } - foreach (var line in LineDiff.Compute(_originalText, EditorText)) + foreach (DiffLine line in LineDiff.Compute(_originalText, EditorText)) { - (var marker, var tone) = line.Marker switch + (string? marker, RowTone tone) = line.Marker switch { DiffMarker.Added => ("+", RowTone.Ok), DiffMarker.Removed => ("−", RowTone.Danger), @@ -497,7 +497,7 @@ private void RebuildEditorLines() _ => (" ", RowTone.Idle) }; // 删除的行在新文里没有行号,显示原文的那个 —— 空着会让人对不上原文件。 - var number = line.Marker == DiffMarker.Removed ? line.OldNumber : line.NewNumber; + int number = line.Marker == DiffMarker.Removed ? line.OldNumber : line.NewNumber; DiffLines.Add(new(number > 0 ? number.ToString() : "", marker, line.Text, tone)); } } @@ -513,7 +513,7 @@ private void BuildFileProperties() FileProperties.Add(new("权限", entry.Mode)); FileProperties.Add(new("属主", entry.Owner)); FileProperties.Add(new("修改时间", entry.Modified)); - var marker = _changes.GetValueOrDefault(entry.FullPath, ""); + string marker = _changes.GetValueOrDefault(entry.FullPath, ""); FileProperties.Add(marker switch { "A" => new("相对镜像", "新增 (A)", RowTone.Ok), @@ -533,7 +533,7 @@ private void Record(string path, string summary) _history.RemoveAt(_history.Count - 1); } History.Clear(); - foreach (var record in _history) + foreach (FileWriteRecord record in _history) { History.Add(record); } @@ -543,9 +543,9 @@ private void Record(string path, string summary) /// 这次改动的摘要,进写入历史(+2 −1 这种)。 private string DescribeEdit() { - var diff = LineDiff.Compute(_originalText, EditorText); - var added = diff.Count(l => l.Marker is DiffMarker.Added or DiffMarker.Changed); - var removed = diff.Count(l => l.Marker is DiffMarker.Removed or DiffMarker.Changed); + IReadOnlyList diff = LineDiff.Compute(_originalText, EditorText); + int added = diff.Count(l => l.Marker is DiffMarker.Added or DiffMarker.Changed); + int removed = diff.Count(l => l.Marker is DiffMarker.Removed or DiffMarker.Changed); return _originalText.Length == 0 ? "新建" : added == 0 && removed == 0 ? "无改动" : $"+{added} −{removed}"; @@ -560,23 +560,23 @@ private string DescribeEdit() /// private async Task RunReloadAsync(DockerClient client) { - var command = ReloadCommand; + string command = ReloadCommand; try { - var result = await client + ExecCapture result = await client .ExecCaptureAsync(containerId, ["/bin/sh", "-c", command], cancellationToken: shell.Lifetime) .ConfigureAwait(true); if (result.ExitCode == 0) { - shell.Feedback.Notify(FeedbackKind.Success, "已重载配置", $"{containerName}:{command}"); + _ = shell.Feedback.Notify(FeedbackKind.Success, "已重载配置", $"{containerName}:{command}"); return; } - shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载失败", + _ = shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载失败", $"{command} 退出码 {result.ExitCode}\n{result.StandardError.Trim()}"); } catch (Exception ex) when (ex is not OperationCanceledException) { - shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载没跑成", $"{command}:{ex.Message}"); + _ = shell.Feedback.Notify(FeedbackKind.Warning, "文件已写回,但重载没跑成", $"{command}:{ex.Message}"); } } @@ -631,12 +631,12 @@ private async Task ExpandAsync(FileEntryItem item) Error = ""; try { - var entries = await client + ContainerFileEntry[] entries = await client .ListDirectoryAsync(containerId, item.FullPath, shell.Lifetime).ConfigureAwait(true); item.Children.Clear(); // 目录在前、再按名字排:树里最常做的动作是"往下钻", // 目录混在文件中间会让每一层都要重新找一遍。 - foreach (var entry in entries + foreach (ContainerFileEntry entry in entries .OrderByDescending(e => e.IsDirectory) .ThenBy(e => e.Name, StringComparer.Ordinal)) { @@ -680,7 +680,7 @@ void Append(FileEntryItem node) { return; } - foreach (var child in node.Children) + foreach (FileEntryItem child in node.Children) { Append(child); } @@ -698,7 +698,7 @@ private async Task BuildTreeAsync() /// 把树里指向某个路径的那一行标成"正在编辑"。 private void MarkCurrentFile(string? path) { - foreach (var node in Tree) + foreach (FileEntryItem node in Tree) { node.Current = node.FullPath == path; } @@ -711,8 +711,8 @@ private void MarkCurrentFile(string? path) { return null; } - var node = _root; - foreach (var segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) + FileEntryItem node = _root; + foreach (string segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) { if (node.Children.FirstOrDefault(c => c.Name == segment) is not { } child) { @@ -733,9 +733,9 @@ private async Task ExpandToAsync(string path) { return; } - var node = _root; + FileEntryItem node = _root; await ExpandAsync(node).ConfigureAwait(true); - foreach (var segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) + foreach (string segment in path.Split('/', StringSplitOptions.RemoveEmptyEntries)) { if (node.Children.FirstOrDefault(c => c.Name == segment) is not { } child) { @@ -758,7 +758,7 @@ private async Task ExpandToAsync(string path) private async Task ReloadTreeAsync() { await LoadChangesAsync().ConfigureAwait(true); - var previous = Path; + string previous = Path; _root = null; await BuildTreeAsync().ConfigureAwait(true); if (previous != "/") @@ -773,7 +773,7 @@ private async Task ReloadTreeAsync() private async Task UploadAsync() { - var source = await FilePicker.PickOpenAsync($"上传到 {Path}").ConfigureAwait(true); + IStorageFile? source = await FilePicker.PickOpenAsync($"上传到 {Path}").ConfigureAwait(true); if (source is not null) { await UploadFileAsync(source).ConfigureAwait(true); @@ -787,7 +787,7 @@ private async Task UploadFileAsync(IStorageFile source) return; } byte[] content; - await using (var input = await source.OpenReadAsync().ConfigureAwait(true)) + await using (Stream input = await source.OpenReadAsync().ConfigureAwait(true)) { if (input.CanSeek && input.Length > DockerClient.MaxUploadFileBytes) { @@ -799,9 +799,9 @@ private async Task UploadFileAsync(IStorageFile source) await input.CopyToAsync(buffer, shell.Lifetime).ConfigureAwait(true); content = buffer.ToArray(); } - var target = Path.TrimEnd('/') + "/" + source.Name; - var exists = FindNode(target) is not null; - var confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() + string target = Path.TrimEnd('/') + "/" + source.Name; + bool exists = FindNode(target) is not null; + bool confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() { Title = exists ? $"覆盖容器内的 {source.Name}?" : $"上传 {source.Name} 到 {containerName}?", Icon = exists ? "Docker.shield-alert" : "Icon.upload", @@ -831,7 +831,7 @@ private async Task UploadFileAsync(IStorageFile source) try { await client.WriteFileAsync(containerId, target, content, shell.Lifetime).ConfigureAwait(true); - shell.Feedback.Notify(FeedbackKind.Success, "已上传", $"{target} · {Humanize.Bytes(content.Length)}"); + _ = shell.Feedback.Notify(FeedbackKind.Success, "已上传", $"{target} · {Humanize.Bytes(content.Length)}"); await ReloadTreeAsync().ConfigureAwait(true); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -875,9 +875,9 @@ private async Task LoadChangesAsync() } try { - var changes = await client.ChangesAsync(containerId, shell.Lifetime).ConfigureAwait(true); + FilesystemChange[] changes = await client.ChangesAsync(containerId, shell.Lifetime).ConfigureAwait(true); _changes.Clear(); - foreach (var change in changes) + foreach (FilesystemChange change in changes) { _changes[change.Path] = change.Marker; } @@ -904,7 +904,7 @@ private async Task OpenFileAsync(string path) Error = ""; try { - var bytes = await client.ReadFileAsync(containerId, path, DockerClient.MaxEditableFileBytes, shell.Lifetime) + byte[] bytes = await client.ReadFileAsync(containerId, path, DockerClient.MaxEditableFileBytes, shell.Lifetime) .ConfigureAwait(true); if (LooksBinary(bytes)) { @@ -945,8 +945,8 @@ private async Task OpenFileAsync(string path) /// private static bool LooksBinary(ReadOnlySpan bytes) { - var limit = Math.Min(bytes.Length, 8192); - for (var i = 0; i < limit; i++) + int limit = Math.Min(bytes.Length, 8192); + for (int i = 0; i < limit; i++) { if (bytes[i] == 0) { @@ -962,7 +962,7 @@ private async Task SaveAsync() { return; } - var confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() + bool confirmed = await shell.Confirm.AskAsync(shell.BuildConfirm(new() { Title = $"覆盖容器内的 {OpenFileName}?", Icon = "Docker.shield-alert", @@ -994,7 +994,7 @@ await client.WriteFileAsync(containerId, path, Encoding.UTF8.GetBytes(EditorText _originalText = EditorText; OnPropertiesChanged(nameof(IsModified), nameof(ModifiedText), nameof(ModifiedLinesText)); RebuildEditorLines(); - shell.Feedback.Notify(FeedbackKind.Success, "已写回容器", $"{path} · {Humanize.Bytes(Encoding.UTF8.GetByteCount(EditorText))}"); + _ = shell.Feedback.Notify(FeedbackKind.Success, "已写回容器", $"{path} · {Humanize.Bytes(Encoding.UTF8.GetByteCount(EditorText))}"); await LoadChangesAsync().ConfigureAwait(true); if (ReloadAfterSave && HasReloadCommand) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs index f257faf..25c12a0 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalView.axaml.cs @@ -14,5 +14,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class ContainerTerminalView : UserControl { /// 建视图。 - public ContainerTerminalView() => AvaloniaXamlLoader.Load(this); + public ContainerTerminalView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs index 27f0bc4..269d603 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainerTerminalViewModel.cs @@ -104,14 +104,14 @@ public string HeaderText get { var sb = new StringBuilder(containerName); - sb.Append(" · ").Append(Shell); + _ = sb.Append(" · ").Append(Shell); if (User.Length > 0) { - sb.Append(" · ").Append(User); + _ = sb.Append(" · ").Append(User); } if (WorkingDir.Length > 0) { - sb.Append(" · ").Append(WorkingDir); + _ = sb.Append(" · ").Append(WorkingDir); } return sb.ToString(); } @@ -233,9 +233,9 @@ private async Task StartSessionAsync() Status = "正在建立 exec 会话…"; try { - var resolved = await ResolveShellAsync(client).ConfigureAwait(true); + string resolved = await ResolveShellAsync(client).ConfigureAwait(true); Shell = resolved; - var session = await client.StartExecAsync(containerId, [resolved], tty: true, + DockerExecSession session = await client.StartExecAsync(containerId, [resolved], tty: true, string.IsNullOrWhiteSpace(User) ? null : User, string.IsNullOrWhiteSpace(WorkingDir) ? null : WorkingDir, shell.Lifetime).ConfigureAwait(true); @@ -255,8 +255,8 @@ private async Task StartSessionAsync() view.Write($"\u001b[31m无法在 {containerName} 里启动 {Shell}:{ex.Message}\u001b[0m\r\n"); // daemon 的原文(多半是一句 "no such file or directory")指不出下一步该做什么。 // 建不起来的原因就那么三种,直说 —— 对着一句原文没人猜得到该换什么。 - var warn = (char)0x1b + "[33m"; - var reset = (char)0x1b + "[0m"; + string warn = (char)0x1b + "[33m"; + string reset = (char)0x1b + "[0m"; view.Write($"{warn}常见原因:容器没在跑;镜像里根本没有 shell(distroless 一类);" + $"或者指定的用户在容器里不存在。{reset}\r\n"); view.Write($"{warn}工具条上的「切换用户」可以换 shell 与身份,换完会自动重连。{reset}\r\n"); @@ -278,11 +278,11 @@ private async Task ResolveShellAsync(DockerClient client) { return Shell; } - foreach (var candidate in ShellCandidates) + foreach (string candidate in ShellCandidates) { try { - var probe = await client + ExecCapture probe = await client .ExecCaptureAsync(containerId, ["/bin/sh", "-c", $"command -v {candidate} >/dev/null"], cancellationToken: shell.Lifetime) .ConfigureAwait(true); @@ -385,18 +385,18 @@ private async Task OpenInHostTerminalAsync() { return; } - var command = $"docker exec -it {Sh.Quote(containerName)} {Sh.Quote(Shell)}"; + string command = $"docker exec -it {Sh.Quote(containerName)} {Sh.Quote(Shell)}"; try { // 回写走宿主的输入队列("如同用户键入"),并且需要用户授权 —— // 面板不直写 SSH 流,这条边界不为方便让路。 await shell.Context.Terminal.WriteAsync(endpoint.SessionId, command + "\n", shell.Lifetime) .ConfigureAwait(true); - shell.Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); + _ = shell.Feedback.Notify(FeedbackKind.Info, "已送到宿主终端", command); } catch (PluginPermissionDeniedException) { - shell.Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", + _ = shell.Feedback.Notify(FeedbackKind.Warning, "没有向终端回写的授权", $"可以自己在那条会话里执行:{command}"); } catch (Exception ex) when (ex is not OperationCanceledException) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageViewModel.cs index a99f9f9..1c42338 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ContainersPageViewModel.cs @@ -1,5 +1,5 @@ -using Avalonia.Controls; using System.Collections.ObjectModel; +using Avalonia.Controls; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -185,14 +185,14 @@ private set private void BuildProjectFilters() { ProjectFilters.Clear(); - foreach (var group in _all + foreach (IGrouping? group in _all .GroupBy(r => r.Project) .Where(g => g.Key.Length > 0) .OrderBy(g => g.Key, StringComparer.OrdinalIgnoreCase)) { ProjectFilters.Add(new(group.Key, group.Key, group.Count())); } - var loose = _all.Count(r => r.Project.Length == 0); + int loose = _all.Count(r => r.Project.Length == 0); if (loose > 0) { ProjectFilters.Add(new("", "(不属于任何项目)", loose)); @@ -237,13 +237,13 @@ public bool? AllSelected { return false; } - var picked = View.Count(r => r.Selected); + int picked = View.Count(r => r.Selected); return picked == 0 ? false : picked == View.Count ? true : null; } set { - var select = value is true; - foreach (var row in View) + bool select = value is true; + foreach (ContainerRow row in View) { row.Selected = select; } @@ -411,7 +411,7 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) Busy = true; try { - var summaries = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); + ContainerSummary[] summaries = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); // 在跑的排前面,同组内按名字 —— 运维找的十有八九是正在跑的那几个。 List incoming = [ @@ -422,9 +422,9 @@ .. summaries ]; var previous = _all.ToDictionary(r => r.Id); _all.Clear(); - foreach (var row in incoming) + foreach (ContainerRow row in incoming) { - if (previous.TryGetValue(row.Id, out var existing)) + if (previous.TryGetValue(row.Id, out ContainerRow? existing)) { existing.Update(row); _all.Add(existing); @@ -472,8 +472,8 @@ public override void Reset() private void ApplyView() { - var needle = Search.Trim(); - var filtered = _all.Where(row => Filter switch + string needle = Search.Trim(); + IEnumerable filtered = _all.Where(row => Filter switch { ContainerFilter.Running => row.IsRunning, ContainerFilter.Stopped => !row.IsRunning && !row.IsPaused, @@ -504,7 +504,7 @@ private void RecountSelection() private void ClearSelection() { - foreach (var row in _all.Where(r => r.Selected)) + foreach (ContainerRow? row in _all.Where(r => r.Selected)) { row.Selected = false; } @@ -529,16 +529,16 @@ private async Task BatchAsync(string verb, IReadOnlyList targets, { return; } - foreach (var row in targets) + foreach (ContainerRow row in targets) { row.Busy = true; } - var task = Shell.Tasks.Start("Docker.box", $"{verb} {targets.Count} 个容器", indeterminate: targets.Count == 1); + PanelTask task = Shell.Tasks.Start("Docker.box", $"{verb} {targets.Count} 个容器", indeterminate: targets.Count == 1); // 选中条原地变进度条:不弹窗、不遮列表,选中也不丢。 BatchProgress = new(verb, targets.Count, task); try { - var result = await BatchRunner.RunAsync( + BatchResult result = await BatchRunner.RunAsync( [.. targets.Select(r => (Target: r, r.Name))], (row, ct) => action(client, row.Id, ct), (done, total, current) => @@ -568,7 +568,7 @@ [.. targets.Select(r => (Target: r, r.Name))], finally { BatchProgress = null; - foreach (var row in targets) + foreach (ContainerRow row in targets) { row.Busy = false; } @@ -641,7 +641,7 @@ private async Task KillAsync(IReadOnlyList targets) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = targets.Count == 1 ? $"强制杀死 {targets[0].Name}?" : $"强制杀死 {targets.Count} 个容器?", Icon = "Icon.zap", @@ -669,7 +669,7 @@ private async Task RemoveAsync(IReadOnlyList targets) { return; } - var anyRunning = targets.Any(t => t.IsRunning); + bool anyRunning = targets.Any(t => t.IsRunning); string[] projects = [.. targets.Select(t => t.Project).Where(p => p.Length > 0).Distinct()]; List consequences = [ @@ -685,7 +685,7 @@ private async Task RemoveAsync(IReadOnlyList targets) consequences.Add(new(0, $"它们属于 compose 项目 {string.Join('、', projects)},下次 up -d 会按 compose.yaml 重建。")); } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = targets.Count == 1 ? $"删除容器 {targets[0].Name}?" : $"删除 {targets.Count} 个容器?", Icon = "Icon.trash-2", @@ -789,8 +789,8 @@ private void RemoveLogSource(LogSource source) /// 来源全选 / 全不选。 public RelayCommand ToggleAllSourcesCommand => field ??= new(_ => { - var select = LogSources.Any(s => !s.Selected); - foreach (var item in LogSources) + bool select = LogSources.Any(s => !s.Selected); + foreach (LogSourceItem item in LogSources) { item.Selected = select; } @@ -820,7 +820,7 @@ private async Task ExitLogsModeAsync() MergedLogs = null; await logs.DisposeAsync().ConfigureAwait(true); } - foreach (var item in LogSources) + foreach (LogSourceItem item in LogSources) { item.SelectionChanged -= OnLogSourceToggled; } @@ -829,7 +829,7 @@ private async Task ExitLogsModeAsync() private void BuildLogSources(IReadOnlyList seed) { - foreach (var existing in LogSources) + foreach (LogSourceItem existing in LogSources) { existing.SelectionChanged -= OnLogSourceToggled; } @@ -837,8 +837,8 @@ private void BuildLogSources(IReadOnlyList seed) HashSet seeded = [.. seed.Select(r => r.Id)]; // 全部容器都列出来(含已停止的)—— 已停止容器的日志依然读得到, // 而"为什么它退出了"恰恰是最常要看的一份日志。 - var index = 0; - foreach (var row in _all) + int index = 0; + foreach (ContainerRow row in _all) { var item = new LogSourceItem( new(row.Id, row.Name, row.Summary.State == "running"), @@ -867,7 +867,7 @@ private List SelectedSources() // 颜色序号按**选中顺序里的位置**重新编号,而不是用列表里的下标: // 否则只选两个相邻容器时会拿到两个几乎一样的颜色。 List selected = [.. LogSources.Where(s => s.Selected)]; - for (var i = 0; i < selected.Count; i++) + for (int i = 0; i < selected.Count; i++) { selected[i].Index = i; } @@ -876,8 +876,8 @@ private List SelectedSources() private void ApplyLogSourceView() { - var needle = LogSourceSearch.Trim(); - foreach (var item in LogSources) + string needle = LogSourceSearch.Trim(); + foreach (LogSourceItem item in LogSources) { item.Visible = needle.Length == 0 || item.Name.Contains(needle, StringComparison.OrdinalIgnoreCase); @@ -958,13 +958,13 @@ private async Task CopyRunCommandAsync(ContainerRow row) } try { - var inspect = await client.InspectContainerAsync(row.Id, Shell.Lifetime).ConfigureAwait(true); + ContainerInspect inspect = await client.InspectContainerAsync(row.Id, Shell.Lifetime).ConfigureAwait(true); string[]? imageEnv = null; if (inspect.Config?.Image is { Length: > 0 } image) { try { - var imageInspect = await client.InspectImageAsync(image, Shell.Lifetime).ConfigureAwait(true); + ImageInspect imageInspect = await client.InspectImageAsync(image, Shell.Lifetime).ConfigureAwait(true); imageEnv = imageInspect.Config?.Env; } catch (Exception) @@ -972,10 +972,10 @@ private async Task CopyRunCommandAsync(ContainerRow row) // 镜像已经被删了(容器还在跑)也很常见 —— 那就不减,多几条环境变量而已。 } } - var command = RunCommandBuilder.Build(inspect, imageEnv); + string command = RunCommandBuilder.Build(inspect, imageEnv); await Shell.Context.Clipboard .SetTextAsync($"{command}\n{RunCommandBuilder.Caveat}", Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "已复制 docker run 命令", + _ = Shell.Feedback.Notify(FeedbackKind.Success, "已复制 docker run 命令", "由 inspect 反推,是近似值 —— 执行前请核对(提醒已一并复制)。"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -1025,7 +1025,7 @@ private async Task CloseDetail(bool force = false) /// private void MarkCurrent(string? id) { - foreach (var row in _all) + foreach (ContainerRow row in _all) { row.Current = row.Id == id; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs index e23ac80..f8c0af8 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class ImageDetailView : UserControl { /// 建视图。 - public ImageDetailView() => AvaloniaXamlLoader.Load(this); + public ImageDetailView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs index f9f1539..22239ac 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImageDetailViewModel.cs @@ -175,7 +175,7 @@ public async Task LoadAsync(CancellationToken cancellationToken) { return; } - var inspect = await client.InspectImageAsync(ImageId, cancellationToken).ConfigureAwait(true); + ImageInspect inspect = await client.InspectImageAsync(ImageId, cancellationToken).ConfigureAwait(true); BuildOverview(inspect); } @@ -202,7 +202,7 @@ private void BuildOverview(ImageInspect inspect) Basics.Add(new("与其它镜像共享", Humanize.Bytes(_row.Summary.SharedSize))); } Basics.Add(new("创建", Humanize.AgoFromIso(inspect.Created))); - var platform = string.Join("/", new[] { inspect.Os, inspect.Architecture, inspect.Variant } + string platform = string.Join("/", new[] { inspect.Os, inspect.Architecture, inspect.Variant } .Where(s => !string.IsNullOrWhiteSpace(s))); if (platform.Length > 0) { @@ -219,16 +219,16 @@ private void BuildOverview(ImageInspect inspect) } Basics.Add(new("使用情况", UsageText, _row.Summary.Containers > 0 ? RowTone.Ok : RowTone.Idle)); - foreach (var tag in inspect.RepoTags ?? _row.Summary.RepoTags ?? []) + foreach (string tag in inspect.RepoTags ?? _row.Summary.RepoTags ?? []) { Tags.Add(tag); } - foreach (var digest in inspect.RepoDigests ?? _row.Summary.RepoDigests ?? []) + foreach (string digest in inspect.RepoDigests ?? _row.Summary.RepoDigests ?? []) { Digests.Add(digest); } - var config = inspect.Config; + ContainerConfig? config = inspect.Config; if (config?.Entrypoint is { Length: > 0 } entrypoint) { Runtime.Add(new("Entrypoint", string.Join(" ", entrypoint))); @@ -250,14 +250,14 @@ private void BuildOverview(ImageInspect inspect) Runtime.Add(new("暴露端口", string.Join(", ", exposed.Keys.OrderBy(k => k, StringComparer.Ordinal)))); } - foreach (var entry in config?.Env ?? []) + foreach (string entry in config?.Env ?? []) { - var split = entry.IndexOf('=', StringComparison.Ordinal); + int split = entry.IndexOf('=', StringComparison.Ordinal); Environment.Add(split > 0 ? new(entry[..split], entry[(split + 1)..]) : new(entry, "")); } - foreach ((var key, var value) in config?.Labels ?? []) + foreach ((string? key, string? value) in config?.Labels ?? []) { Labels.Add(new(key, value)); } @@ -286,14 +286,14 @@ private async Task LoadHistoryAsync(CancellationToken cancellationToken) } try { - var history = await client.ImageHistoryAsync(ImageId, cancellationToken) + ImageHistoryEntry[] history = await client.ImageHistoryAsync(ImageId, cancellationToken) .ConfigureAwait(true); Layers.Clear(); - var largest = history.Length > 0 ? history.Max(h => h.Size) : 0; + long largest = history.Length > 0 ? history.Max(h => h.Size) : 0; // daemon 返回的是从新到旧;Dockerfile 是从旧到新读的,按后者排更容易对上。 - foreach (var entry in history.Reverse()) + foreach (ImageHistoryEntry? entry in history.Reverse()) { - var weight = largest > 0 ? Math.Clamp((double)entry.Size / largest, 0, 1) : 0; + double weight = largest > 0 ? Math.Clamp((double)entry.Size / largest, 0, 1) : 0; Layers.Add(new( CleanInstruction(entry.CreatedBy), entry.Size > 0 ? Humanize.Bytes(entry.Size) : "—", @@ -338,7 +338,7 @@ private async Task LoadRawAsync(CancellationToken cancellationToken) /// public static string CleanInstruction(string? createdBy) { - var text = (createdBy ?? "").Trim(); + string text = (createdBy ?? "").Trim(); if (text.Length == 0) { return "(无记录)"; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageViewModel.cs index f349e12..ff3407f 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/ImagesPageViewModel.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Platform.Storage; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -142,13 +143,13 @@ public bool? AllSelected { return false; } - var picked = View.Count(r => r.Selected); + int picked = View.Count(r => r.Selected); return picked == 0 ? false : picked == View.Count ? true : null; } set { - var select = value is true; - foreach (var row in View) + bool select = value is true; + foreach (ImageRow row in View) { row.Selected = select; } @@ -259,7 +260,7 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) Busy = true; try { - var summaries = await client.ListImagesAsync(false, cancellationToken).ConfigureAwait(true); + ImageSummary[] summaries = await client.ListImagesAsync(false, cancellationToken).ConfigureAwait(true); List incoming = [ .. summaries @@ -268,9 +269,9 @@ .. summaries ]; var previous = _all.ToDictionary(r => r.Id); _all.Clear(); - foreach (var row in incoming) + foreach (ImageRow row in incoming) { - if (previous.TryGetValue(row.Id, out var existing)) + if (previous.TryGetValue(row.Id, out ImageRow? existing)) { existing.Update(row); _all.Add(existing); @@ -288,7 +289,7 @@ .. summaries OnPropertiesChanged(nameof(TotalCount), nameof(UsedCount), nameof(UnusedCount), nameof(DanglingCount)); if (Detail is { } detail) { - var updated = _all.FirstOrDefault(r => r.Id == detail.ImageId); + ImageRow? updated = _all.FirstOrDefault(r => r.Id == detail.ImageId); // 抽屉里那个镜像被删掉了(或者被 prune 掉了)就关上, // 留一个指向已消失对象的抽屉只会让下一步操作报一个看不懂的错。 if (updated is null) @@ -335,7 +336,7 @@ private void CloseDetail() /// 把"抽屉里开着的那一行"标出来。 private void MarkCurrent(string? id) { - foreach (var row in _all) + foreach (ImageRow row in _all) { row.Current = row.Id == id; } @@ -357,8 +358,8 @@ public override void Reset() private void ApplyView() { - var needle = Search.Trim(); - var filtered = _all.Where(row => Filter switch + string needle = Search.Trim(); + IEnumerable filtered = _all.Where(row => Filter switch { ImageFilter.Used => row.Summary.Containers > 0, ImageFilter.Unused => row.Summary.Containers <= 0 && !row.IsDangling, @@ -384,7 +385,7 @@ private void RecountSelection() private void ClearSelection() { - foreach (var row in _all.Where(r => r.Selected)) + foreach (ImageRow? row in _all.Where(r => r.Selected)) { row.Selected = false; } @@ -427,7 +428,7 @@ private async Task TagAsync(ImageRow row) /// private static string RegistryOf(string repository) { - var head = repository.Split('/')[0]; + string head = repository.Split('/')[0]; return repository.Contains('/') && (head.Contains('.') || head.Contains(':') || head == "localhost") ? head @@ -440,12 +441,12 @@ private async Task PushAsync(ImageRow row) { return; } - var reference = $"{row.Repository}:{row.Tag}"; - var task = Shell.Tasks.Start("Icon.upload", $"推送 {reference}", indeterminate: false); + string reference = $"{row.Repository}:{row.Tag}"; + PanelTask task = Shell.Tasks.Start("Icon.upload", $"推送 {reference}", indeterminate: false); var aggregator = new PullAggregator(); try { - var header = await auth.GetAuthHeaderAsync(row.Repository, task.Token).ConfigureAwait(true); + string? header = await auth.GetAuthHeaderAsync(row.Repository, task.Token).ConfigureAwait(true); await client.PushImageAsync(row.Repository, row.Tag, header, new DirectProgress(frame => { @@ -457,7 +458,7 @@ await client.PushImageAsync(row.Repository, row.Tag, header, }); }), task.Token).ConfigureAwait(true); task.Finish(PanelTaskState.Succeeded, "完成"); - Shell.Feedback.Notify(FeedbackKind.Success, "推送完成", reference); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "推送完成", reference); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -486,34 +487,34 @@ private async Task SaveAsync(IReadOnlyList targets) } // 悬空镜像没有可用的引用,只能按 id 存。 List names = [.. targets.Select(t => t.IsDangling ? t.Id : $"{t.Repository}:{t.Tag}")]; - var suggested = targets.Count == 1 + string suggested = targets.Count == 1 ? $"{(targets[0].IsDangling ? targets[0].ShortId : targets[0].Repository.Replace('/', '_'))}.tar" : $"images-{targets.Count}.tar"; - var target = await FilePicker + IStorageFile? target = await FilePicker .PickSaveAsync(targets.Count == 1 ? $"导出 {names[0]}" : $"导出 {targets.Count} 个镜像", suggested, "tar") .ConfigureAwait(true); if (target is null) { return; } - var task = Shell.Tasks.Start("Docker.file-archive", + PanelTask task = Shell.Tasks.Start("Docker.file-archive", targets.Count == 1 ? $"导出 {names[0]}" : $"导出 {targets.Count} 个镜像", indeterminate: true); try { - await using var archive = await client.SaveImagesAsync(names, task.Token).ConfigureAwait(true); - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); - var buffer = new byte[128 * 1024]; + await using Stream archive = await client.SaveImagesAsync(names, task.Token).ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); + byte[] buffer = new byte[128 * 1024]; long total = 0; int read; while ((read = await archive.ReadAsync(buffer, task.Token).ConfigureAwait(true)) > 0) { await output.WriteAsync(buffer.AsMemory(0, read), task.Token).ConfigureAwait(true); total += read; - var written = total; + long written = total; Ui.Post(() => task.Detail = $"已写出 {Humanize.Bytes(written)}"); } task.Finish(PanelTaskState.Succeeded, "完成", Humanize.Bytes(total)); - Shell.Feedback.Notify(FeedbackKind.Success, "镜像已导出", $"{target.Name} · {Humanize.Bytes(total)}"); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "镜像已导出", $"{target.Name} · {Humanize.Bytes(total)}"); } catch (OperationCanceledException) { @@ -533,17 +534,17 @@ private async Task LoadAsync() { return; } - var source = await FilePicker.PickOpenAsync("选一个 docker save 出来的 tar").ConfigureAwait(true); + IStorageFile? source = await FilePicker.PickOpenAsync("选一个 docker save 出来的 tar").ConfigureAwait(true); if (source is null) { return; } - var task = Shell.Tasks.Start("Docker.arrow-down-to-line", $"导入 {source.Name}", indeterminate: false); + PanelTask task = Shell.Tasks.Start("Docker.arrow-down-to-line", $"导入 {source.Name}", indeterminate: false); var aggregator = new PullAggregator(); try { // 请求体直接挂在文件流上,不缓冲。 - await using var input = await source.OpenReadAsync().ConfigureAwait(true); + await using Stream input = await source.OpenReadAsync().ConfigureAwait(true); await client.LoadImagesAsync(input, new DirectProgress(frame => { aggregator.Accept(frame); @@ -554,7 +555,7 @@ private async Task LoadAsync() }); }), task.Token).ConfigureAwait(true); task.Finish(PanelTaskState.Succeeded, "完成"); - Shell.Feedback.Notify(FeedbackKind.Success, "镜像已导入", source.Name); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "镜像已导入", source.Name); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } catch (OperationCanceledException) @@ -574,7 +575,7 @@ private async Task RemoveAsync(IReadOnlyList targets) { return; } - var anyInUse = targets.Any(t => t.Summary.Containers > 0); + bool anyInUse = targets.Any(t => t.Summary.Containers > 0); List consequences = [ new(2, "镜像层被删掉之后,重新拉回来要花时间与带宽。"), @@ -584,7 +585,7 @@ private async Task RemoveAsync(IReadOnlyList targets) { consequences.Insert(0, new(3, "其中有正在被容器使用的镜像 —— 需要 force,那会让那些容器失去它们的镜像引用。")); } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = targets.Count == 1 ? $"删除镜像 {targets[0].Repository}:{targets[0].Tag}?" : $"删除 {targets.Count} 个镜像?", Icon = "Icon.trash-2", @@ -599,7 +600,7 @@ private async Task RemoveAsync(IReadOnlyList targets) { return; } - var result = await BatchRunner.RunAsync( + BatchResult result = await BatchRunner.RunAsync( [.. targets.Select(t => (Target: t, Name: $"{t.Repository}:{t.Tag}"))], async (row, ct) => await client.RemoveImageAsync(row.Id, anyInUse, ct).ConfigureAwait(false), null, Shell.Lifetime).ConfigureAwait(true); @@ -614,7 +615,7 @@ private async Task PruneAsync(bool danglingOnly) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = danglingOnly ? "清理悬空镜像?" : "清理全部未使用的镜像?", Icon = "Docker.broom", @@ -641,8 +642,8 @@ private async Task PruneAsync(bool danglingOnly) } try { - var report = await client.PruneImagesAsync(danglingOnly, Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", + PruneReport report = await client.PruneImagesAsync(danglingOnly, Shell.Lifetime).ConfigureAwait(true); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", $"删除 {report.DeletedCount} 项 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs index e26d201..e30366c 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class LogsView : UserControl { /// 建视图。 - public LogsView() => AvaloniaXamlLoader.Load(this); + public LogsView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs index ef0908f..a333f2b 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/LogsViewModel.cs @@ -1,7 +1,8 @@ -using Avalonia.Threading; using System.Collections.ObjectModel; using System.Text; using System.Text.RegularExpressions; +using Avalonia.Platform.Storage; +using Avalonia.Threading; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -183,7 +184,7 @@ public string StreamSummary { return "没有选中任何来源"; } - var elapsed = _startedAt == default ? "" : $" · 已运行 {Humanize.Duration(DateTimeOffset.UtcNow - _startedAt)}"; + string elapsed = _startedAt == default ? "" : $" · 已运行 {Humanize.Duration(DateTimeOffset.UtcNow - _startedAt)}"; return $"docker logs{(Follow ? " -f" : "")} · {_sources.Count} 条流{elapsed}"; } } @@ -343,13 +344,13 @@ public bool Wrap public RelayCommand CopyCommand => field ??= new(_ => { var sb = new StringBuilder(); - foreach (var line in Lines) + foreach (LogLineItem line in Lines) { if (line.Timestamp.Length > 0) { - sb.Append(line.Timestamp).Append(' '); + _ = sb.Append(line.Timestamp).Append(' '); } - sb.AppendLine(line.Text); + _ = sb.AppendLine(line.Text); } return shell.Context.Clipboard.SetTextAsync(sb.ToString(), shell.Lifetime); }); @@ -369,10 +370,10 @@ public bool Wrap /// private async Task DownloadAsync() { - var suggested = _sources.Count == 1 && _sources[0].Name.Length > 0 + string suggested = _sources.Count == 1 && _sources[0].Name.Length > 0 ? $"{_sources[0].Name}.log" : $"docker-logs-{_sources.Count}.log"; - var target = await FilePicker + IStorageFile? target = await FilePicker .PickSaveAsync("保存日志", suggested, "log").ConfigureAwait(true); if (target is null) { @@ -380,9 +381,9 @@ private async Task DownloadAsync() } try { - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); await using var writer = new StreamWriter(output, Encoding.UTF8); - foreach (var line in Lines) + foreach (LogLineItem line in Lines) { if (line.Timestamp.Length > 0) { @@ -394,7 +395,7 @@ private async Task DownloadAsync() } await writer.WriteLineAsync(line.Text).ConfigureAwait(true); } - shell.Feedback.Notify(FeedbackKind.Success, "日志已保存", $"{target.Name} · {Lines.Count:N0} 行"); + _ = shell.Feedback.Notify(FeedbackKind.Success, "日志已保存", $"{target.Name} · {Lines.Count:N0} 行"); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -439,23 +440,23 @@ public async Task RestartAsync() return; } _cts = CancellationTokenSource.CreateLinkedTokenSource(shell.Lifetime); - var token = _cts.Token; + CancellationToken token = _cts.Token; _startedAt = DateTimeOffset.UtcNow; StartFlushTimer(); StatusText = Follow ? "跟随中 · 新行即时到达" : "已加载历史"; OnPropertyChanged(nameof(StreamSummary)); - var timestamps = Timestamps; - var tail = Tail; - var live = _sources.Count; + bool timestamps = Timestamps; + string tail = Tail; + int live = _sources.Count; // 每个来源一条独立的流,全部写进同一个待刷队列 —— // 合并的顺序就是**到达顺序**,不按时间戳重排:重排要缓冲、要等, // 而"跟随"这个功能的全部意义就是不等。 - for (var i = 0; i < _sources.Count; i++) + for (int i = 0; i < _sources.Count; i++) { - var source = _sources[i]; - var index = i; - var tty = _sources.Count == 1 ? ttyAccessor() : source.Tty; + LogSource source = _sources[i]; + int index = i; + bool tty = _sources.Count == 1 ? ttyAccessor() : source.Tty; _ = Task.Run(async () => { try @@ -524,7 +525,7 @@ private void OnFlush(object? sender, EventArgs e) batch = [.. _pending]; _pending.Clear(); } - foreach (var item in batch) + foreach (LogLineItem item in batch) { if (ErrorsOnly && !item.IsError) { @@ -565,7 +566,7 @@ private void CompileFilter() private void ApplyFilter() { - foreach (var line in Lines) + foreach (LogLineItem line in Lines) { line.Matched = _filter?.IsMatch(line.Text) ?? false; } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs index 6385344..5f66312 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/NetworksPageViewModel.cs @@ -1,5 +1,6 @@ -using Avalonia.Controls; using System.Collections.ObjectModel; +using Avalonia.Controls; +using Avalonia.Platform.Storage; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -192,7 +193,7 @@ private async Task ExportAsync() { await LoadRawAsync().ConfigureAwait(true); } - var target = await FilePicker + IStorageFile? target = await FilePicker .PickSaveAsync($"导出 {row.Name} 的配置", $"{row.Name}.json", "json").ConfigureAwait(true); if (target is null) { @@ -200,13 +201,13 @@ private async Task ExportAsync() } try { - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); await using var writer = new StreamWriter(output); await writer.WriteLineAsync( "// docker network inspect 的原文 —— 含运行态(已接入容器与已分配地址),不是可直接重建的清单。") .ConfigureAwait(true); await writer.WriteAsync(RawInspect).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "网络配置已导出", target.Name); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "网络配置已导出", target.Name); } catch (Exception ex) when (ex is not OperationCanceledException) { @@ -221,7 +222,7 @@ private async Task DisconnectAllAsync() return; } AttachedContainer[] targets = [.. Attached]; - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"把 {targets.Length} 个容器从 {row.Name} 上摘掉?", Icon = "Icon.unplug", @@ -241,7 +242,7 @@ private async Task DisconnectAllAsync() { return; } - foreach (var target in targets) + foreach (AttachedContainer target in targets) { try { @@ -250,7 +251,7 @@ private async Task DisconnectAllAsync() catch (Exception ex) when (ex is not OperationCanceledException) { // 逐个判定:一个摘不掉不该让其余的也不摘。 - Shell.Feedback.Notify(FeedbackKind.Warning, $"{target.Name} 摘不掉", ex.Message); + _ = Shell.Feedback.Notify(FeedbackKind.Warning, $"{target.Name} 摘不掉", ex.Message); } } Shell.Feedback.Status(FeedbackKind.Success, $"已从 {row.Name} 摘除 {targets.Length} 个容器"); @@ -306,7 +307,7 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) Busy = true; try { - var networks = await client.ListNetworksAsync(cancellationToken).ConfigureAwait(true); + NetworkSummary[] networks = await client.ListNetworksAsync(cancellationToken).ConfigureAwait(true); List incoming = [ .. networks @@ -316,9 +317,9 @@ .. networks ]; var previous = _all.ToDictionary(r => r.Id); _all.Clear(); - foreach (var row in incoming) + foreach (NetworkRow row in incoming) { - if (previous.TryGetValue(row.Id, out var existing)) + if (previous.TryGetValue(row.Id, out NetworkRow? existing)) { existing.Update(row); _all.Add(existing); @@ -334,7 +335,7 @@ .. networks ApplyView(); if (Selected is { } selected) { - var still = _all.FirstOrDefault(r => r.Id == selected.Id); + NetworkRow? still = _all.FirstOrDefault(r => r.Id == selected.Id); Selected = still; if (still is not null) { @@ -367,7 +368,7 @@ public override void Reset() private void ApplyView() { - var needle = Search.Trim(); + string needle = Search.Trim(); IEnumerable filtered = _all; if (CustomOnly) { @@ -405,7 +406,7 @@ private async Task SelectAsync(NetworkRow? row) { return null; } - if (!int.TryParse(text[(slash + 1)..], out var prefix)) + if (!int.TryParse(text[(slash + 1)..], out int prefix)) { return null; } @@ -414,7 +415,7 @@ private async Task SelectAsync(NetworkRow? row) { return null; } - var total = 1L << (32 - prefix); + long total = 1L << (32 - prefix); return total > 3 ? total - 3 : 0; } @@ -426,9 +427,9 @@ private async Task LoadDetailAsync(NetworkRow row, CancellationToken cancellatio } try { - var detail = await client.InspectNetworkAsync(row.Id, cancellationToken).ConfigureAwait(true); + NetworkSummary detail = await client.InspectNetworkAsync(row.Id, cancellationToken).ConfigureAwait(true); Attached.Clear(); - foreach ((var id, var container) in detail.Containers ?? []) + foreach ((string? id, NetworkContainer? container) in detail.Containers ?? []) { Attached.Add(new(id, container.Name ?? Humanize.ShortId(id), container.IPv4Address ?? "—")); } @@ -439,7 +440,7 @@ private async Task LoadDetailAsync(NetworkRow row, CancellationToken cancellatio // 「已分配 3 / 65533」:子网快用满是一个到了才发现就太晚的问题。 if (SubnetCapacity(detail.FirstSubnet) is { } capacity) { - var used = Attached.Count; + int used = Attached.Count; Ipam.Add(new("已分配", $"{used} / {capacity:N0}", capacity > 0 && used > capacity * 0.8 ? RowTone.Warn : RowTone.Idle)); } @@ -467,7 +468,7 @@ private async Task CreateAsync() } try { - var info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); + SystemInfo info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); _swarmActive = info.Swarm?.LocalNodeState == "active"; } catch (Exception ex) when (ex is not OperationCanceledException) @@ -481,11 +482,11 @@ private async Task CreateAsync() } try { - var created = await client.CreateNetworkAsync(form.Name, form.Driver, form.Subnet, + CreateNetworkResponse created = await client.CreateNetworkAsync(form.Name, form.Driver, form.Subnet, form.Gateway, form.Internal, form.Attachable, form.EnableIPv6, Shell.Lifetime).ConfigureAwait(true); if (created.Warning is { Length: > 0 } warning) { - Shell.Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); + _ = Shell.Feedback.Notify(FeedbackKind.Warning, "daemon 有话说", warning); } else { @@ -505,7 +506,7 @@ private async Task RemoveAsync(NetworkRow row) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"删除网络 {row.Name}?", Icon = "Icon.trash-2", @@ -551,7 +552,7 @@ private async Task PruneAsync() { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = "清理未使用的网络?", Icon = "Docker.broom", @@ -573,8 +574,8 @@ private async Task PruneAsync() } try { - var report = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", $"删除 {report.DeletedCount} 个网络"); + PruneReport report = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(true); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", $"删除 {report.DeletedCount} 个网络"); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -589,7 +590,7 @@ private async Task ConnectAsync(NetworkRow row) { return; } - var containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); + ContainerSummary[] containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); HashSet already = [.. Attached.Select(a => a.Id)]; var form = new ConnectNetworkForm( $"{row.Name} · {row.Driver} · {row.Subnet}", @@ -603,7 +604,7 @@ private async Task ConnectAsync(NetworkRow row) { return; } - var result = await BatchRunner.RunAsync( + BatchResult result = await BatchRunner.RunAsync( [.. form.SelectedIds.Select(id => (Target: id, containers.First(c => c.Id == id).Name))], (id, ct) => client.ConnectNetworkAsync(row.Id, id, form.Aliases.Length > 0 ? form.Aliases : null, ct), null, Shell.Lifetime).ConfigureAwait(true); @@ -617,7 +618,7 @@ private async Task DisconnectAsync(AttachedContainer attached) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"把 {attached.Name} 从 {row.Name} 上摘掉?", Icon = "Icon.unplug", diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs index 0118ddb..3e21a7e 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageView.axaml.cs @@ -8,7 +8,10 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class OverviewPageView : UserControl { /// 建视图。 - public OverviewPageView() => AvaloniaXamlLoader.Load(this); + public OverviewPageView() + { + AvaloniaXamlLoader.Load(this); + } /// “需要关注”里那条的动作天生是个 Action,不必为它造一个 ICommand。 private void OnAttentionClick(object? sender, RoutedEventArgs e) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs index c9b6ce3..4ce9ada 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/OverviewPageViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Avalonia.Platform.Storage; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -32,7 +33,7 @@ public sealed class EventItem(DockerEvent source) private static string Describe(DockerEvent source) { - var exitCode = source.Actor?.Attributes?.GetValueOrDefault("exitCode"); + string? exitCode = source.Actor?.Attributes?.GetValueOrDefault("exitCode"); // 卷与网络的 create 与容器的 create 是同一个词,先按对象类型分流。 if (source is { Type: "volume", Action: "create" }) { @@ -237,15 +238,15 @@ public string TrendScaleText /// 按当前窗口重画趋势图。 private void RebuildTrend() { - var peak = _cpuTrend.Count > 0 ? _cpuTrend.Max() : 0; - var scale = Math.Max(peak, TrendFloorPercent); + double peak = _cpuTrend.Count > 0 ? _cpuTrend.Max() : 0; + double scale = Math.Max(peak, TrendFloorPercent); TrendScaleText = $"每 5 秒一个采样点 · 纵轴 0–{Humanize.Percent(scale)}"; CpuTrend.Clear(); - for (var i = _cpuTrend.Count; i < TrendSlots; i++) + for (int i = _cpuTrend.Count; i < TrendSlots; i++) { CpuTrend.Add(new(0, false)); } - foreach (var sample in _cpuTrend) + foreach (double sample in _cpuTrend) { CpuTrend.Add(new(Math.Max(2, Math.Clamp(sample / scale, 0, 1) * TrendHeight), true)); } @@ -327,7 +328,7 @@ private async Task ExportDiagnosticsAsync() { return; } - var target = await FilePicker + IStorageFile? target = await FilePicker .PickSaveAsync("导出诊断信息", $"docker-diagnostics-{Shell.SelectedEndpoint?.DisplayName ?? "host"}.txt", "txt") .ConfigureAwait(true); if (target is null) @@ -336,29 +337,29 @@ private async Task ExportDiagnosticsAsync() } try { - var info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); - var version = await client.VersionAsync(Shell.Lifetime).ConfigureAwait(true); - var containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); + SystemInfo info = await client.InfoAsync(Shell.Lifetime).ConfigureAwait(true); + SystemVersion version = await client.VersionAsync(Shell.Lifetime).ConfigureAwait(true); + ContainerSummary[] containers = await client.ListContainersAsync(true, Shell.Lifetime).ConfigureAwait(true); var sb = new System.Text.StringBuilder(); - sb.AppendLine("# VelaShell Docker 面板 · 诊断信息"); - sb.AppendLine("# 不含凭据、环境变量与日志正文 —— 可以直接发给别人。"); - sb.AppendLine($"主机 {Shell.SelectedEndpoint?.DisplayName}"); - sb.AppendLine($"socket {Shell.SelectedEndpoint?.Endpoint.SocketPath}"); - sb.AppendLine($"Engine {version.Version} (API {version.ApiVersion})"); - sb.AppendLine($"操作系统 {info.OperatingSystem} · {info.KernelVersion}"); - sb.AppendLine($"架构 / CPU {info.Architecture} · {info.NCPU} 核 · {Humanize.Bytes(info.MemTotal)}"); - sb.AppendLine($"存储驱动 {info.Driver}"); - sb.AppendLine($"容器 / 镜像 {info.Containers} / {info.Images}"); - sb.AppendLine(); - sb.AppendLine("## 容器"); - foreach (var container in containers) + _ = sb.AppendLine("# VelaShell Docker 面板 · 诊断信息"); + _ = sb.AppendLine("# 不含凭据、环境变量与日志正文 —— 可以直接发给别人。"); + _ = sb.AppendLine($"主机 {Shell.SelectedEndpoint?.DisplayName}"); + _ = sb.AppendLine($"socket {Shell.SelectedEndpoint?.Endpoint.SocketPath}"); + _ = sb.AppendLine($"Engine {version.Version} (API {version.ApiVersion})"); + _ = sb.AppendLine($"操作系统 {info.OperatingSystem} · {info.KernelVersion}"); + _ = sb.AppendLine($"架构 / CPU {info.Architecture} · {info.NCPU} 核 · {Humanize.Bytes(info.MemTotal)}"); + _ = sb.AppendLine($"存储驱动 {info.Driver}"); + _ = sb.AppendLine($"容器 / 镜像 {info.Containers} / {info.Images}"); + _ = sb.AppendLine(); + _ = sb.AppendLine("## 容器"); + foreach (ContainerSummary container in containers) { - sb.AppendLine($"{container.Name,-28} {container.State,-10} {container.Image,-42} {container.Status}"); + _ = sb.AppendLine($"{container.Name,-28} {container.State,-10} {container.Image,-42} {container.Status}"); } - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); await using var writer = new StreamWriter(output); await writer.WriteAsync(sb.ToString()).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "诊断信息已导出", + _ = Shell.Feedback.Notify(FeedbackKind.Success, "诊断信息已导出", $"{target.Name} —— 不含凭据与日志正文。"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -377,11 +378,11 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) Busy = true; try { - var containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); - var info = await client.InfoAsync(cancellationToken).ConfigureAwait(true); - var running = containers.Count(c => c.State == "running"); - var unhealthy = containers.Count(c => (c.Status ?? "").Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase)); - var failed = containers.Count(c => c.State == "exited" && !(c.Status ?? "").Contains("(0)", StringComparison.Ordinal)); + ContainerSummary[] containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); + SystemInfo info = await client.InfoAsync(cancellationToken).ConfigureAwait(true); + int running = containers.Count(c => c.State == "running"); + int unhealthy = containers.Count(c => (c.Status ?? "").Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase)); + int failed = containers.Count(c => c.State == "exited" && !(c.Status ?? "").Contains("(0)", StringComparison.Ordinal)); RunningText = $"{running} / {containers.Length}"; RunningDetail = $"{unhealthy} 个不健康 · {failed} 个异常退出"; _hostMemory = info.MemTotal; @@ -447,7 +448,7 @@ public void AcceptEvent(DockerEvent dockerEvent) public void AcceptStatsSnapshot(IReadOnlyList rows) { List running = [.. rows.Where(r => r.IsRunning && r.CpuPercent > 0)]; - var total = running.Sum(r => r.CpuPercent); + double total = running.Sum(r => r.CpuPercent); CpuText = Humanize.Percent(total); _cpuPeak = Math.Max(_cpuPeak, total); CpuDetail = running.Count > 0 @@ -456,7 +457,7 @@ public void AcceptStatsSnapshot(IReadOnlyList rows) TrackHotContainers(running); // 内存卡走的是同一批采样:容器占用之和 / 宿主总量, // 单独再问一次 daemon 只会得到同样的数字。 - var usedMemory = rows.Where(r => r.IsRunning).Sum(r => r.MemoryBytes); + long usedMemory = rows.Where(r => r.IsRunning).Sum(r => r.MemoryBytes); MemText = usedMemory > 0 ? Humanize.Bytes(usedMemory) : "0 B"; MemDetail = _hostMemory > 0 ? $"容器占用 · 宿主共 {Humanize.Bytes(_hostMemory)}{(_hostCpus > 0 ? $" · {_hostCpus} 核" : "")}" @@ -467,9 +468,9 @@ public void AcceptStatsSnapshot(IReadOnlyList rows) _cpuTrend.RemoveAt(0); } RebuildTrend(); - var max = running.Count > 0 ? running.Max(r => r.CpuPercent) : 0; + double max = running.Count > 0 ? running.Max(r => r.CpuPercent) : 0; TopCpu.Clear(); - foreach (var row in running.OrderByDescending(r => r.CpuPercent).Take(5)) + foreach (ContainerRow? row in running.OrderByDescending(r => r.CpuPercent).Take(5)) { TopCpu.Add(new(row.Name, row.CpuPercent, max, Humanize.Percent(row.CpuPercent), row.CpuHot)); } @@ -516,7 +517,7 @@ public async Task RefreshReclaimAsync(CancellationToken cancellationToken) ReclaimDetail = "正在统计"; try { - var usage = await client.DiskUsageAsync(cancellationToken).ConfigureAwait(true); + DiskUsage usage = await client.DiskUsageAsync(cancellationToken).ConfigureAwait(true); AcceptReclaim(DiskMath.Reclaimable(usage)); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -537,9 +538,9 @@ public async Task RefreshReclaimAsync(CancellationToken cancellationToken) private void TrackHotContainers(IReadOnlyList running) { HashSet stillHot = []; - foreach (var row in running.Where(r => r.CpuPercent >= HotCpuThreshold)) + foreach (ContainerRow? row in running.Where(r => r.CpuPercent >= HotCpuThreshold)) { - stillHot.Add(row.Id); + _ = stillHot.Add(row.Id); if (!_hotSince.ContainsKey(row.Id)) { _hotSince[row.Id] = DateTimeOffset.UtcNow; @@ -548,11 +549,11 @@ private void TrackHotContainers(IReadOnlyList running) _hotPercent[row.Id] = row.CpuPercent; } // 掉下去就清零 —— 「已持续 12 分钟」得是真的连续,不能把两段拼起来。 - foreach (var id in _hotSince.Keys.Where(id => !stillHot.Contains(id)).ToList()) + foreach (string? id in _hotSince.Keys.Where(id => !stillHot.Contains(id)).ToList()) { - _hotSince.Remove(id); - _hotNames.Remove(id); - _hotPercent.Remove(id); + _ = _hotSince.Remove(id); + _ = _hotNames.Remove(id); + _ = _hotPercent.Remove(id); } RebuildHotAttention(); } @@ -560,18 +561,18 @@ private void TrackHotContainers(IReadOnlyList running) /// 把"持续高 CPU"那几条插进关注列表,并把过期的那些拿掉。 private void RebuildHotAttention() { - foreach (var stale in Attention.Where(a => a.Icon == "Icon.cpu").ToList()) + foreach (AttentionItem? stale in Attention.Where(a => a.Icon == "Icon.cpu").ToList()) { - Attention.Remove(stale); + _ = Attention.Remove(stale); } - foreach ((var id, var since) in _hotSince) + foreach ((string? id, DateTimeOffset since) in _hotSince) { - var held = DateTimeOffset.UtcNow - since; + TimeSpan held = DateTimeOffset.UtcNow - since; if (held < HotCpuHold) { continue; } - var name = _hotNames.GetValueOrDefault(id, Humanize.ShortId(id)); + string name = _hotNames.GetValueOrDefault(id, Humanize.ShortId(id)); Attention.Insert(0, new("Icon.cpu", RowTone.Warn, $"{name} CPU 持续高于 {HotCpuThreshold:F0}%", $"已持续 {Humanize.Duration(held)} · 当前 {Humanize.Percent(_hotPercent.GetValueOrDefault(id))}", @@ -584,9 +585,9 @@ private void RebuildHotAttention() private void BuildAttention(IReadOnlyList containers) { Attention.Clear(); - foreach (var container in containers) + foreach (ContainerSummary container in containers) { - var status = container.Status ?? ""; + string status = container.Status ?? ""; if (status.Contains("(unhealthy)", StringComparison.OrdinalIgnoreCase)) { Attention.Add(new("Icon.triangle-alert", RowTone.Danger, diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs index 77ebd02..1ab0010 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class PullImageView : UserControl { /// 建视图。 - public PullImageView() => AvaloniaXamlLoader.Load(this); + public PullImageView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs index c6f8faa..c3f2906 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/PullImageViewModel.cs @@ -71,8 +71,8 @@ public PullImageViewModel(DockerPanelViewModel shell, string? initialReference) _shell = shell; if (!string.IsNullOrWhiteSpace(initialReference)) { - var colon = initialReference!.LastIndexOf(':'); - var slash = initialReference.LastIndexOf('/'); + int colon = initialReference!.LastIndexOf(':'); + int slash = initialReference.LastIndexOf('/'); if (colon > slash && colon > 0) { _reference = initialReference[..colon]; @@ -353,7 +353,7 @@ private async Task RefreshAuthAsync() } try { - var status = await auth.GetStatusAsync(Reference.Trim(), _shell.Lifetime).ConfigureAwait(true); + RegistryAuthStatus status = await auth.GetStatusAsync(Reference.Trim(), _shell.Lifetime).ConfigureAwait(true); AuthState = status.State; AuthText = status.State switch { @@ -382,13 +382,13 @@ private async Task StartAsync() _lastBytes = 0; _cts = CancellationTokenSource.CreateLinkedTokenSource(_shell.Lifetime); _task = _shell.Tasks.Start("Docker.arrow-down-to-line", $"拉取 {FullReference}", indeterminate: false); - var reference = Reference.Trim(); - var tag = Tag.Trim(); - var platform = Platform.Trim(); - var allTags = AllTags; + string reference = Reference.Trim(); + string tag = Tag.Trim(); + string platform = Platform.Trim(); + bool allTags = AllTags; try { - var header = _shell.RegistryAuth is { } auth + string? header = _shell.RegistryAuth is { } auth ? await auth.GetAuthHeaderAsync(reference, _cts.Token).ConfigureAwait(true) : null; await client.PullImageAsync(reference, tag, platform, allTags, header, @@ -438,22 +438,22 @@ private void OnFrame(PullProgressFrame frame) private void UpdateSpeed() { - var now = DateTimeOffset.UtcNow; - var span = now - _lastSample; + DateTimeOffset now = DateTimeOffset.UtcNow; + TimeSpan span = now - _lastSample; if (span < TimeSpan.FromMilliseconds(700)) { return; } - var bytes = _aggregator.CurrentBytes; - var delta = bytes - _lastBytes; + long bytes = _aggregator.CurrentBytes; + long delta = bytes - _lastBytes; _lastBytes = bytes; _lastSample = now; if (delta <= 0) { return; } - var perSecond = delta / span.TotalSeconds; - var remaining = _aggregator.TotalBytes - bytes; + double perSecond = delta / span.TotalSeconds; + long remaining = _aggregator.TotalBytes - bytes; SpeedText = remaining > 0 && perSecond > 0 ? $"{Humanize.Bytes((long)perSecond)}/s · 剩余 ~{Humanize.Duration(TimeSpan.FromSeconds(remaining / perSecond))}" : $"{Humanize.Bytes((long)perSecond)}/s"; @@ -465,15 +465,15 @@ private void UpdateSpeed() /// private void SyncLayers() { - foreach ((var id, var status, var progress, var sizeText) in _aggregator.Snapshot()) + foreach ((string? id, string? status, double progress, string? sizeText) in _aggregator.Snapshot()) { - var complete = progress >= 1; - var reused = status.Contains("Already exists", StringComparison.OrdinalIgnoreCase); + bool complete = progress >= 1; + bool reused = status.Contains("Already exists", StringComparison.OrdinalIgnoreCase); if (reused) { continue; } - if (_layerMap.TryGetValue(id, out var item)) + if (_layerMap.TryGetValue(id, out PullLayerItem? item)) { item.Update(status, progress, sizeText); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs index 77d164c..c2c78a1 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui.Pages; public sealed partial class SystemPageView : UserControl { /// 建视图。 - public SystemPageView() => AvaloniaXamlLoader.Load(this); + public SystemPageView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs index c14abd2..7b13b72 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/SystemPageViewModel.cs @@ -139,9 +139,9 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) { // 这三条一起发:df 在镜像多的机器上要几秒,而它是这一页的主角, // 不该等 info 与 version 串行跑完才开始。 - var usageTask = client.DiskUsageAsync(cancellationToken); - var infoTask = client.InfoAsync(cancellationToken); - var versionTask = client.VersionAsync(cancellationToken); + Task usageTask = client.DiskUsageAsync(cancellationToken); + Task infoTask = client.InfoAsync(cancellationToken); + Task versionTask = client.VersionAsync(cancellationToken); await Task.WhenAll(usageTask, infoTask, versionTask).ConfigureAwait(true); _usage = usageTask.Result; _info = infoTask.Result; @@ -197,10 +197,10 @@ private void BuildStats() } // 每张卡的第三行都是"这个数字的分母或去向" —— 光看「镜像 34」说明不了任何事, // 「其中 14 个没人用」才是让人动手的那一句。 - var volumes = _usage?.Volumes?.Length ?? 0; - var volumesIdle = _usage?.Volumes?.Count(v => v.UsageData is not { RefCount: > 0 }) ?? 0; - var imagesDangling = _usage?.Images?.Count(i => i.IsDangling) ?? 0; - var cacheRecords = _usage?.BuildCache?.Length ?? 0; + int volumes = _usage?.Volumes?.Length ?? 0; + int volumesIdle = _usage?.Volumes?.Count(v => v.UsageData is not { RefCount: > 0 }) ?? 0; + int imagesDangling = _usage?.Images?.Count(i => i.IsDangling) ?? 0; + int cacheRecords = _usage?.BuildCache?.Length ?? 0; Stats.Add(new("Docker.box", "容器", info.Containers.ToString(), $"运行中 {info.ContainersRunning}", info.ContainersRunning > 0 ? RowTone.Ok : RowTone.Idle)); @@ -226,10 +226,10 @@ private void BuildStats() /// private async Task LoadHostDiskAsync(DockerClient client, CancellationToken cancellationToken) { - var root = _info?.DockerRootDir is { Length: > 0 } dir ? dir : "/var/lib/docker"; + string root = _info?.DockerRootDir is { Length: > 0 } dir ? dir : "/var/lib/docker"; try { - var result = await client + ExecCapture result = await client .ExecCaptureAsync(FirstRunningContainerId ?? "", ["/bin/sh", "-c", $"df -PB1 {Sh.Quote(root)}"], cancellationToken: cancellationToken) .ConfigureAwait(true); @@ -258,11 +258,11 @@ private async Task LoadHostDiskAsync(DockerClient client, CancellationToken canc /// 解一行 df -PB1。返回 (总量, 已用)。 internal static (long Total, long Used)? ParseDf(string output) { - foreach (var line in output.Split('\n').Skip(1)) + foreach (string? line in output.Split('\n').Skip(1)) { - var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); // Filesystem 1-blocks Used Available Capacity Mounted - if (parts.Length >= 4 && long.TryParse(parts[1], out var total) && long.TryParse(parts[2], out var used)) + if (parts.Length >= 4 && long.TryParse(parts[1], out long total) && long.TryParse(parts[2], out long used)) { return (total, used); } @@ -278,16 +278,16 @@ private void BuildDisk() { return; } - var reclaim = DiskMath.Reclaimable(usage); - var imageTotal = usage.Images?.Sum(i => i.Size) ?? 0; - var imageReclaim = reclaim.Images; + ReclaimBreakdown reclaim = DiskMath.Reclaimable(usage); + long imageTotal = usage.Images?.Sum(i => i.Size) ?? 0; + long imageReclaim = reclaim.Images; // 可写层大小只有 /system/df 会给(/containers/json 要带 size=1 才算,那对每个容器做一次 // diff,贵得多)—— 这一页正好是 df,所以这里能拿到真值。 - var containerTotal = usage.Containers?.Sum(c => c.SizeRw) ?? 0; + long containerTotal = usage.Containers?.Sum(c => c.SizeRw) ?? 0; // 「已停止的容器」能回收的是**已停止那些**的可写层,不是全部容器的。 - var containerReclaim = usage.Containers?.Where(c => c.State != "running").Sum(c => c.SizeRw) ?? 0; - var volumeTotal = usage.Volumes?.Sum(v => v.UsageData is { Size: > 0 } u ? u.Size : 0) ?? 0; - var volumeReclaim = reclaim.Volumes; + long containerReclaim = usage.Containers?.Where(c => c.State != "running").Sum(c => c.SizeRw) ?? 0; + long volumeTotal = usage.Volumes?.Sum(v => v.UsageData is { Size: > 0 } u ? u.Size : 0) ?? 0; + long volumeReclaim = reclaim.Volumes; _buildCacheBytes = reclaim.BuildCache; DiskRows.Add(new("镜像", (usage.Images?.Length ?? 0).ToString(), @@ -307,7 +307,7 @@ private void BuildDisk() Ratio(_buildCacheBytes, usage.BuildCache?.Sum(c => c.Size) ?? 0), _buildCacheBytes > 0 ? RowTone.Danger : RowTone.Idle)); - var total = imageTotal + containerTotal + volumeTotal + _buildCacheBytes; + long total = imageTotal + containerTotal + volumeTotal + _buildCacheBytes; if (total > 0) { // 权重归一化成 0–1:界面用星形列宽画堆叠条,那条路要的是比例而不是字节。 @@ -324,7 +324,7 @@ private void BuildDisk() // df 是这一页跑一次就有的东西,顺手喂给总览那张卡 —— // 让用户为了看一眼"能清多少"而必须先来系统页,是没道理的。 Shell.Overview.AcceptReclaim(reclaim); - foreach (var card in PruneCards) + foreach (PruneCard card in PruneCards) { card.SizeText = card.Title switch { @@ -370,7 +370,7 @@ private async Task PruneContainersAsync() { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = "清理已停止的容器?", Icon = "Icon.trash-2", @@ -399,7 +399,7 @@ private async Task PruneBuildCacheAsync() { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = "清理构建缓存?", Icon = "Docker.database", @@ -427,7 +427,7 @@ private async Task PruneAllAsync(bool withVolumes) { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = withVolumes ? "清理以上全部,并删除未使用的卷?" : "清理以上全部?", Icon = withVolumes ? "Docker.shield-alert" : "Docker.broom", @@ -469,11 +469,11 @@ private async Task PruneAllAsync(bool withVolumes) } await RunPruneAsync(withVolumes ? "全部清理(含卷)" : "全部清理", async () => { - var containers = await client.PruneContainersAsync(Shell.Lifetime).ConfigureAwait(false); - var images = await client.PruneImagesAsync(false, Shell.Lifetime).ConfigureAwait(false); - var networks = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(false); - var cache = await client.PruneBuildCacheAsync(true, Shell.Lifetime).ConfigureAwait(false); - var volumes = withVolumes + PruneReport containers = await client.PruneContainersAsync(Shell.Lifetime).ConfigureAwait(false); + PruneReport images = await client.PruneImagesAsync(false, Shell.Lifetime).ConfigureAwait(false); + PruneReport networks = await client.PruneNetworksAsync(Shell.Lifetime).ConfigureAwait(false); + PruneReport cache = await client.PruneBuildCacheAsync(true, Shell.Lifetime).ConfigureAwait(false); + PruneReport volumes = withVolumes ? await client.PruneVolumesAsync(Shell.Lifetime).ConfigureAwait(false) : new PruneReport(); return new PruneReport @@ -491,13 +491,13 @@ await RunPruneAsync(withVolumes ? "全部清理(含卷)" : "全部清理", async private async Task RunPruneAsync(string title, Func> action) { - var task = Shell.Tasks.Start("Docker.broom", title, indeterminate: true); + PanelTask task = Shell.Tasks.Start("Docker.broom", title, indeterminate: true); try { - var report = await action().ConfigureAwait(true); + PruneReport report = await action().ConfigureAwait(true); task.Finish(PanelTaskState.Succeeded, "完成", $"删除 {report.DeletedCount} 项 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); - Shell.Feedback.Notify(FeedbackKind.Success, $"{title} 完成", + _ = Shell.Feedback.Notify(FeedbackKind.Success, $"{title} 完成", $"删除 {report.DeletedCount} 项 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs index 69132a5..afbd125 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Pages/VolumesPageViewModel.cs @@ -1,5 +1,6 @@ -using Avalonia.Controls; using System.Collections.ObjectModel; +using Avalonia.Controls; +using Avalonia.Platform.Storage; using VelaShell.Plugin.DockerPanel.Docker; namespace VelaShell.Plugin.DockerPanel.Ui.Pages; @@ -76,7 +77,7 @@ public string TotalSizeText { get { - var total = _all.Sum(r => r.Summary.UsageData is { Size: > 0 } u ? u.Size : 0); + long total = _all.Sum(r => r.Summary.UsageData is { Size: > 0 } u ? u.Size : 0); return total > 0 ? Humanize.Bytes(total) : "未统计"; } } @@ -130,7 +131,7 @@ private set /// 选中卷的使用者。 public IReadOnlyList SelectedUsers => - Selected is { } row && _users.TryGetValue(row.Name, out var users) ? users : []; + Selected is { } row && _users.TryGetValue(row.Name, out List? users) ? users : []; /// /// 能不能浏览卷内文件。 @@ -191,20 +192,20 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) Busy = true; try { - var volumes = await client.ListVolumesAsync(cancellationToken).ConfigureAwait(true); + VolumeSummary[] volumes = await client.ListVolumesAsync(cancellationToken).ConfigureAwait(true); // 引用计数得自己算:/volumes 不带 UsageData,只有 /system/df 才带, // 而后者在镜像多的机器上要几秒 —— 不值得为一列数字每次都付这个钱。 - var containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); + ContainerSummary[] containers = await client.ListContainersAsync(true, cancellationToken).ConfigureAwait(true); _users.Clear(); - foreach (var container in containers) + foreach (ContainerSummary container in containers) { - foreach (var mount in container.Mounts ?? []) + foreach (DockerMount mount in container.Mounts ?? []) { if (mount.Type != "volume" || mount.Name is not { Length: > 0 } name) { continue; } - if (!_users.TryGetValue(name, out var list)) + if (!_users.TryGetValue(name, out List? list)) { _users[name] = list = []; } @@ -215,13 +216,13 @@ public override async Task RefreshAsync(CancellationToken cancellationToken) [ .. volumes .OrderBy(v => v.Name, StringComparer.OrdinalIgnoreCase) - .Select(v => new VolumeRow(v, _users.TryGetValue(v.Name, out var u) ? u.Count : 0)) + .Select(v => new VolumeRow(v, _users.TryGetValue(v.Name, out List? u) ? u.Count : 0)) ]; var previous = _all.ToDictionary(r => r.Id); _all.Clear(); - foreach (var row in incoming) + foreach (VolumeRow row in incoming) { - if (previous.TryGetValue(row.Id, out var existing)) + if (previous.TryGetValue(row.Id, out VolumeRow? existing)) { existing.Update(row); _all.Add(existing); @@ -266,7 +267,7 @@ public override bool WantsRefresh(DockerEvent dockerEvent) => private void ApplyView() { - var needle = Search.Trim(); + string needle = Search.Trim(); IEnumerable filtered = _all; if (UnusedOnly) { @@ -295,7 +296,7 @@ private async Task CreateAsync() } try { - await client.CreateVolumeAsync(form.Name, form.Driver, form.DriverOptions, form.LabelMap, Shell.Lifetime) + _ = await client.CreateVolumeAsync(form.Name, form.Driver, form.DriverOptions, form.LabelMap, Shell.Lifetime) .ConfigureAwait(true); Shell.Feedback.Status(FeedbackKind.Success, $"已新建卷 {form.Name}"); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); @@ -312,8 +313,8 @@ private async Task RemoveAsync(VolumeRow row) { return; } - var users = _users.TryGetValue(row.Name, out var list) ? list : []; - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + List users = _users.TryGetValue(row.Name, out List? list) ? list : []; + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = $"删除卷 {row.Name}?", Icon = "Docker.shield-alert", @@ -347,21 +348,21 @@ private async Task RemoveAsync(VolumeRow row) // 备份失败就**不删** —— 用户勾这个框的意思正是"没有备份就别删"。 if (!await BackupAsync(row).ConfigureAwait(true)) { - Shell.Feedback.Notify(FeedbackKind.Warning, "没有删除", "备份没成功,卷保留原样。"); + _ = Shell.Feedback.Notify(FeedbackKind.Warning, "没有删除", "备份没成功,卷保留原样。"); return; } } try { await client.RemoveVolumeAsync(row.Name, false, Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "卷已删除", row.Name); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "卷已删除", row.Name); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } catch (Exception ex) when (ex is not OperationCanceledException) { // 409 的真实含义是"还有容器挂着它",而挂着它的是谁,这一页早就知道 —— // 把这句话直接说出来,并给一条过去看的路。 - ToastAction[] actions = ex is DockerApiException { IsConflict: true } && _users.TryGetValue(row.Name, out var holders) && holders.Count > 0 + ToastAction[] actions = ex is DockerApiException { IsConflict: true } && _users.TryGetValue(row.Name, out List? holders) && holders.Count > 0 ? [new($"看看是谁在占({holders.Count} 个容器)", () => Selected = row)] : []; Shell.Feedback.ReportError("删除卷", ex, actions); @@ -374,7 +375,7 @@ private async Task PruneAsync() { return; } - var confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() + bool confirmed = await Shell.Confirm.AskAsync(Shell.BuildConfirm(new() { Title = "清理全部未使用的卷?", Icon = "Docker.shield-alert", @@ -398,8 +399,8 @@ .. _all.Where(r => r.RefCount == 0).Take(5).Select(r => $"将被删除:{r.Name}( } try { - var report = await client.PruneVolumesAsync(Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", + PruneReport report = await client.PruneVolumesAsync(Shell.Lifetime).ConfigureAwait(true); + _ = Shell.Feedback.Notify(FeedbackKind.Success, "清理完成", $"删除 {report.DeletedCount} 个卷 · 回收 {Humanize.Bytes(report.SpaceReclaimed)}"); await RefreshAsync(Shell.Lifetime).ConfigureAwait(true); } @@ -413,12 +414,12 @@ private async Task BrowseAsync(VolumeRow row) { if (await FindMountAsync(row.Name).ConfigureAwait(true) is not { } mount) { - Shell.Feedback.Notify(FeedbackKind.Warning, "没法浏览这个卷", BrowseHint); + _ = Shell.Feedback.Notify(FeedbackKind.Warning, "没法浏览这个卷", BrowseHint); return; } - (var container, var destination) = mount; + (ContainerSummary? container, string? destination) = mount; await Shell.GoToAsync(PanelPage.Containers).ConfigureAwait(true); - var target = Shell.Containers.View.FirstOrDefault(r => r.Id == container.Id); + ContainerRow? target = Shell.Containers.View.FirstOrDefault(r => r.Id == container.Id); if (target is null) { return; @@ -440,7 +441,7 @@ private async Task BrowseAsync(VolumeRow row) /// /// 这一行能不能备份(有运行中的容器挂着它,且能弹文件对话框)。 private bool CanBackupRow(VolumeRow row) => - FilePicker.IsAvailable && _users.TryGetValue(row.Name, out var users) && users.Count > 0; + FilePicker.IsAvailable && _users.TryGetValue(row.Name, out List? users) && users.Count > 0; /// 备份;返回是否真的存下来了(删卷前的那个勾选靠它决定要不要继续)。 private async Task BackupAsync(VolumeRow row) @@ -451,12 +452,12 @@ private async Task BackupAsync(VolumeRow row) } if (await FindMountAsync(row.Name).ConfigureAwait(true) is not { } mount) { - Shell.Feedback.Notify(FeedbackKind.Warning, "没法备份这个卷", + _ = Shell.Feedback.Notify(FeedbackKind.Warning, "没法备份这个卷", "没有运行中的容器挂着它 —— 先起一个挂载了这个卷的容器再来备份。"); return false; } - (var container, var destination) = mount; - var target = await FilePicker + (ContainerSummary? container, string? destination) = mount; + IStorageFile? target = await FilePicker .PickSaveAsync($"把卷 {row.Name} 存成 tar", $"{row.Name}.tar", "tar") .ConfigureAwait(true); if (target is null) @@ -466,11 +467,11 @@ private async Task BackupAsync(VolumeRow row) Busy = true; try { - await using var archive = await client + await using Stream archive = await client .DownloadArchiveAsync(container.Id, destination, Shell.Lifetime).ConfigureAwait(true); - await using var output = await target.OpenWriteAsync().ConfigureAwait(true); + await using Stream output = await target.OpenWriteAsync().ConfigureAwait(true); await archive.CopyToAsync(output, Shell.Lifetime).ConfigureAwait(true); - Shell.Feedback.Notify(FeedbackKind.Success, "卷已备份", + _ = Shell.Feedback.Notify(FeedbackKind.Success, "卷已备份", $"{target.Name} · 经容器 {container.Name} 的 {destination}"); return true; } @@ -500,11 +501,11 @@ private void BuildSelectedDetails(VolumeRow? row) { return; } - foreach ((var key, var value) in row.Summary.Labels ?? []) + foreach ((string? key, string? value) in row.Summary.Labels ?? []) { SelectedLabels.Add(new(key, value)); } - foreach ((var key, var value) in row.Summary.Options ?? []) + foreach ((string? key, string? value) in row.Summary.Options ?? []) { SelectedOptions.Add(new(key, value)); } @@ -517,10 +518,10 @@ private void BuildSelectedDetails(VolumeRow? row) { return null; } - var containers = await client.ListContainersAsync(false, Shell.Lifetime).ConfigureAwait(true); - foreach (var container in containers) + ContainerSummary[] containers = await client.ListContainersAsync(false, Shell.Lifetime).ConfigureAwait(true); + foreach (ContainerSummary container in containers) { - var mount = (container.Mounts ?? []) + DockerMount? mount = (container.Mounts ?? []) .FirstOrDefault(m => m.Type == "volume" && m.Name == volumeName); if (mount?.Destination is { Length: > 0 } destination) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs index 22ceb3b..6c9fafa 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelForm.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using Avalonia.Platform.Storage; namespace VelaShell.Plugin.DockerPanel.Ui; @@ -164,7 +165,7 @@ public string Value private void SyncPicked() { - foreach (var option in Options) + foreach (ChoiceOption option in Options) { option.Picked = option.Value == Value; } @@ -219,7 +220,7 @@ public string Value private void SyncPicked() { - foreach (var option in Options) + foreach (ChoiceOption option in Options) { option.Picked = option.Value == Value; } @@ -264,18 +265,18 @@ public PairListField(string label) : base(label) { if (p is PairRow row) { - Rows.Remove(row); + _ = Rows.Remove(row); } }); // 增删行、以及行里任一格的改动,都要把"等效命令"重算一遍 —— // 那条预览存在的全部意义就是让用户核对自己填的东西,它一旦滞后就成了误导。 Rows.CollectionChanged += (_, e) => { - foreach (var added in e.NewItems?.OfType() ?? []) + foreach (PairRow added in e.NewItems?.OfType() ?? []) { added.PropertyChanged += OnRowChanged; } - foreach (var removed in e.OldItems?.OfType() ?? []) + foreach (PairRow removed in e.OldItems?.OfType() ?? []) { removed.PropertyChanged -= OnRowChanged; } @@ -295,11 +296,11 @@ public PairListField(string label) : base(label) /// public (int Imported, int Skipped) ImportDotEnv(string text) { - var imported = 0; - var skipped = 0; - foreach (var raw in text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n')) + int imported = 0; + int skipped = 0; + foreach (string raw in text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n')) { - var line = raw.Trim(); + string line = raw.Trim(); if (line.Length == 0 || line.StartsWith('#')) { continue; @@ -309,14 +310,14 @@ public PairListField(string label) : base(label) { line = line[7..].TrimStart(); } - var equals = line.IndexOf('=', StringComparison.Ordinal); + int equals = line.IndexOf('=', StringComparison.Ordinal); if (equals <= 0) { skipped++; continue; } - var key = line[..equals].Trim(); - var value = line[(equals + 1)..].Trim(); + string key = line[..equals].Trim(); + string value = line[(equals + 1)..].Trim(); if (value.Length >= 2 && ((value[0] == '"' && value[^1] == '"') || (value[0] == '\'' && value[^1] == '\''))) { value = value[1..^1]; @@ -333,9 +334,9 @@ public PairListField(string label) : base(label) imported++; } // 空的占位行留着没意义,导完清一遍。 - foreach (var blank in Rows.Where(r => r.Key.Length == 0 && r.Value.Length == 0).ToList()) + foreach (PairRow? blank in Rows.Where(r => r.Key.Length == 0 && r.Value.Length == 0).ToList()) { - Rows.Remove(blank); + _ = Rows.Remove(blank); } return (imported, skipped); } @@ -369,16 +370,16 @@ public PairListField(string label) : base(label) /// public RelayCommand ImportCommand => field ??= new(async _ => { - var file = + IStorageFile? file = await FilePicker.PickOpenAsync("选一个 .env 文件").ConfigureAwait(true); if (file is null) { return; } - await using var stream = await file.OpenReadAsync().ConfigureAwait(true); + await using Stream stream = await file.OpenReadAsync().ConfigureAwait(true); using var reader = new StreamReader(stream); - var text = await reader.ReadToEndAsync().ConfigureAwait(true); - (var imported, var skipped) = ImportDotEnv(text); + string text = await reader.ReadToEndAsync().ConfigureAwait(true); + (int imported, int skipped) = ImportDotEnv(text); ImportReport = skipped == 0 ? $"已导入 {imported} 条" : $"已导入 {imported} 条 · 跳过 {skipped} 行(不是 KEY=VALUE)"; @@ -427,7 +428,7 @@ public bool Selected { if (Enabled) { - SetField(ref field, value); + _ = SetField(ref field, value); } } } @@ -466,7 +467,7 @@ public string Search public void ApplyFilter() { View.Clear(); - foreach (var item in Items.Where(i => + foreach (SelectItem? item in Items.Where(i => Search.Length == 0 || i.Label.Contains(Search, StringComparison.OrdinalIgnoreCase))) { View.Add(item); diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs index 5bb3f38..e409d02 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelFormView.axaml.cs @@ -7,5 +7,8 @@ namespace VelaShell.Plugin.DockerPanel.Ui; public sealed partial class PanelFormView : UserControl { /// 建视图。 - public PanelFormView() => AvaloniaXamlLoader.Load(this); + public PanelFormView() + { + AvaloniaXamlLoader.Load(this); + } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs index a5ad49d..8700836 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/PanelSettings.cs @@ -110,7 +110,7 @@ public void NotifyLogTailSegments() => /// 读全局设置。 public async Task LoadAsync(CancellationToken cancellationToken = default) { - var state = await TryGetAsync(GlobalKey, cancellationToken).ConfigureAwait(false); + GlobalState? state = await TryGetAsync(GlobalKey, cancellationToken).ConfigureAwait(false); if (state is null) { return; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs index 607f1ba..44214d2 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ProcessTable.cs @@ -24,23 +24,23 @@ public static class ProcessTable /// 把一份 top 结果拍平成四列。 public static IReadOnlyList Normalize(ContainerTopResult? result) { - var titles = result?.Titles ?? []; - var rows = result?.Processes ?? []; + string[] titles = result?.Titles ?? []; + string[][] rows = result?.Processes ?? []; if (rows.Length == 0) { return []; } - var pid = IndexOf(titles, "PID"); - var user = IndexOf(titles, "USER", "UID", "RUSER"); - var cpu = IndexOf(titles, "%CPU", "TIME", "PCPU"); - var command = IndexOf(titles, "COMMAND", "CMD", "ARGS"); + int pid = IndexOf(titles, "PID"); + int user = IndexOf(titles, "USER", "UID", "RUSER"); + int cpu = IndexOf(titles, "%CPU", "TIME", "PCPU"); + int command = IndexOf(titles, "COMMAND", "CMD", "ARGS"); List normalized = [with(rows.Length)]; - foreach (var row in rows) + foreach (string[] row in rows) { // 命令列认不出来时退回"最后一列":ps 的输出里命令永远排在末尾, // 因为只有它能带空格。 - var commandText = command >= 0 ? Cell(row, command) + string commandText = command >= 0 ? Cell(row, command) : row.Length > 0 ? row[^1] : ""; normalized.Add(new( @@ -55,8 +55,8 @@ public static IReadOnlyList Normalize(ContainerTopResult? result) /// 这一列在 top 结果里叫什么(空表示这台机器的 ps 没给这一列)。 public static string CpuColumnTitle(ContainerTopResult? result) { - var titles = result?.Titles ?? []; - var index = IndexOf(titles, "%CPU", "TIME", "PCPU"); + string[] titles = result?.Titles ?? []; + int index = IndexOf(titles, "%CPU", "TIME", "PCPU"); return index >= 0 ? titles[index] : "CPU"; } @@ -65,9 +65,9 @@ private static string Cell(string[] row, int index) => private static int IndexOf(string[] titles, params string[] candidates) { - foreach (var candidate in candidates) + foreach (string candidate in candidates) { - for (var i = 0; i < titles.Length; i++) + for (int i = 0; i < titles.Length; i++) { if (string.Equals(titles[i], candidate, StringComparison.OrdinalIgnoreCase)) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs index 560bdcc..fcaeac9 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/Rows.cs @@ -139,7 +139,7 @@ public string Uptime { get { - var status = Status; + string status = Status; if (status.Length == 0) { return "—"; @@ -150,8 +150,8 @@ public string Uptime return "退出 " + status[7..]; } // 运行中的容器:括号里是健康状态,它已经由左侧色条与圆点表达了,这一列只要时长。 - var paren = status.IndexOf(" (", StringComparison.Ordinal); - var text = paren > 0 ? status[..paren] : status; + int paren = status.IndexOf(" (", StringComparison.Ordinal); + string text = paren > 0 ? status[..paren] : status; return text.StartsWith("Up ", StringComparison.Ordinal) ? text[3..] : text; } } @@ -267,10 +267,10 @@ public void Update(ImageRow incoming) private (string Repository, string Tag) SplitTag() { - var first = Summary.RepoTags is { Length: > 0 } tags ? tags[0] : ":"; - var colon = first.LastIndexOf(':'); + string first = Summary.RepoTags is { Length: > 0 } tags ? tags[0] : ":"; + int colon = first.LastIndexOf(':'); // 冒号可能属于端口(registry:5000/foo),所以只有它出现在最后一个斜杠之后才是标签分隔。 - var slash = first.LastIndexOf('/'); + int slash = first.LastIndexOf('/'); return colon > slash && colon > 0 ? (first[..colon], first[(colon + 1)..]) : (first, "latest"); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs index 3f6d6ec..d86ace6 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/RunContainerForm.cs @@ -64,7 +64,7 @@ public RunContainerForm(string image, string imageDetail, IEnumerable ne ImportLabel = "从 .env 导入" }; _network = new("网络") { Value = "bridge" }; - foreach (var network in networks) + foreach (string network in networks) { _network.Options.Add(new(network, network)); } @@ -73,7 +73,7 @@ public RunContainerForm(string image, string imageDetail, IEnumerable ne _network.Options.Add(new("bridge", "bridge")); } _restart = new("重启策略") { Value = "unless-stopped" }; - foreach (var policy in new[] { "no", "on-failure", "always", "unless-stopped" }) + foreach (string? policy in new[] { "no", "on-failure", "always", "unless-stopped" }) { _restart.Options.Add(new(policy, policy)); } @@ -153,16 +153,16 @@ public ContainerCreateRequest ToRequest() { Dictionary bindings = []; Dictionary exposed = []; - foreach (var row in Ports.Filled) + foreach (PairRow row in Ports.Filled) { - var containerPort = row.Value.Trim(); - var key = containerPort.Contains('/', StringComparison.Ordinal) ? containerPort : $"{containerPort}/tcp"; + string containerPort = row.Value.Trim(); + string key = containerPort.Contains('/', StringComparison.Ordinal) ? containerPort : $"{containerPort}/tcp"; bindings[key] = [new() { HostPort = row.Key.Trim() }]; exposed[key] = new object(); } string[] binds = [.. Volumes.Filled.Select(r => $"{r.Key.Trim()}:{r.Value.Trim()}")]; string[] env = [.. Env.Filled.Select(r => $"{r.Key.Trim()}={r.Value.Trim()}")]; - var command = _command.Value.Trim(); + string command = _command.Value.Trim(); return new() { Image = Image, @@ -191,21 +191,21 @@ public override bool Validate() _name.Error = "只能包含字母、数字与 _ . -,且必须以字母或数字开头。"; return false; } - foreach (var row in Ports.Filled) + foreach (PairRow row in Ports.Filled) { - if (!int.TryParse(row.Key.Trim(), out var host) || host is < 1 or > 65535) + if (!int.TryParse(row.Key.Trim(), out int host) || host is < 1 or > 65535) { Ports.Error = $"宿主端口 {row.Key} 不是一个合法端口。"; return false; } - var container = row.Value.Trim().Split('/')[0]; - if (!int.TryParse(container, out var inner) || inner is < 1 or > 65535) + string container = row.Value.Trim().Split('/')[0]; + if (!int.TryParse(container, out int inner) || inner is < 1 or > 65535) { Ports.Error = $"容器端口 {row.Value} 不是一个合法端口。"; return false; } } - foreach (var row in Volumes.Filled) + foreach (PairRow row in Volumes.Filled) { if (!row.Value.Trim().StartsWith('/')) { @@ -228,56 +228,56 @@ protected override void UpdatePreview() var sb = new StringBuilder("docker run"); if (_detach.Value) { - sb.Append(" -d"); + _ = sb.Append(" -d"); } if (_autoRemove.Value) { - sb.Append(" --rm"); + _ = sb.Append(" --rm"); } if (_tty.Value) { - sb.Append(" -t"); + _ = sb.Append(" -t"); } if (_privileged.Value) { - sb.Append(" --privileged"); + _ = sb.Append(" --privileged"); } if (ContainerName.Length > 0) { - sb.Append(" --name ").Append(ContainerName); + _ = sb.Append(" --name ").Append(ContainerName); } - foreach (var row in Ports.Filled) + foreach (PairRow row in Ports.Filled) { - sb.Append(" -p ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); + _ = sb.Append(" -p ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); } - foreach (var row in Volumes.Filled) + foreach (PairRow row in Volumes.Filled) { - sb.Append(" -v ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); + _ = sb.Append(" -v ").Append(row.Key.Trim()).Append(':').Append(row.Value.Trim()); } - foreach (var row in Env.Filled) + foreach (PairRow row in Env.Filled) { - sb.Append(" -e ").Append(row.Key.Trim()).Append('=').Append(row.Value.Trim()); + _ = sb.Append(" -e ").Append(row.Key.Trim()).Append('=').Append(row.Value.Trim()); } if (_network.Value.Length > 0) { - sb.Append(" --network ").Append(_network.Value); + _ = sb.Append(" --network ").Append(_network.Value); } if (!_autoRemove.Value) { - sb.Append(" --restart ").Append(_restart.Value); + _ = sb.Append(" --restart ").Append(_restart.Value); } if (_workdir.Value.Trim().Length > 0) { - sb.Append(" -w ").Append(_workdir.Value.Trim()); + _ = sb.Append(" -w ").Append(_workdir.Value.Trim()); } if (ExtraArgs.Value.Trim().Length > 0) { - sb.Append(' ').Append(ExtraArgs.Value.Trim()); + _ = sb.Append(' ').Append(ExtraArgs.Value.Trim()); } - sb.Append(' ').Append(Image); + _ = sb.Append(' ').Append(Image); if (_command.Value.Trim().Length > 0) { - sb.Append(' ').Append(_command.Value.Trim()); + _ = sb.Append(' ').Append(_command.Value.Trim()); } CommandPreview = "POST /containers/create + POST /containers/{id}/start"; CommandNote = $"等价于 {sb}"; @@ -290,8 +290,8 @@ internal static string[] SplitArguments(string command) { List parts = []; var current = new StringBuilder(); - var quote = '\0'; - foreach (var c in command) + char quote = '\0'; + foreach (char c in command) { if (quote != '\0') { @@ -301,7 +301,7 @@ internal static string[] SplitArguments(string command) } else { - current.Append(c); + _ = current.Append(c); } } else if (c is '"' or '\'') @@ -313,12 +313,12 @@ internal static string[] SplitArguments(string command) if (current.Length > 0) { parts.Add(current.ToString()); - current.Clear(); + _ = current.Clear(); } } else { - current.Append(c); + _ = current.Append(c); } } if (current.Length > 0) diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs index daf3157..85224ec 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/StatsSampler.cs @@ -39,7 +39,7 @@ public void Start(Func> targets, Action LoopAsync(token), token); } @@ -56,8 +56,8 @@ private async Task LoopAsync(CancellationToken token) { while (!token.IsCancellationRequested) { - var client = clientAccessor(); - var rows = _targets?.Invoke() ?? []; + DockerClient? client = clientAccessor(); + IReadOnlyList rows = _targets?.Invoke() ?? []; if (client is not null && rows.Count > 0) { await Task.WhenAll(rows.Select(row => SampleAsync(client, row, token))).ConfigureAwait(false); @@ -89,7 +89,7 @@ private async Task SampleAsync(DockerClient client, ContainerRow row, Cancellati } try { - var stats = await client.StatsSnapshotAsync(row.Id, token).ConfigureAwait(false); + ContainerStats? stats = await client.StatsSnapshotAsync(row.Id, token).ConfigureAwait(false); if (stats is not null) { Ui.Post(() => row.ApplyStats(stats)); @@ -101,7 +101,7 @@ private async Task SampleAsync(DockerClient client, ContainerRow row, Cancellati } finally { - _gate.Release(); + _ = _gate.Release(); } } diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs index 7ad6308..bd0df4d 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/TaskCenter.cs @@ -181,7 +181,7 @@ public string IndicatorPercent { get { - var progress = OverallProgress; + double progress = OverallProgress; return progress <= 0 ? "" : $"{progress * 100:0}%"; } } @@ -205,9 +205,9 @@ public void ClearFinished() { Ui.Post(() => { - foreach (var task in Tasks.Where(t => t.IsFinished).ToArray()) + foreach (PanelTask? task in Tasks.Where(t => t.IsFinished).ToArray()) { - Tasks.Remove(task); + _ = Tasks.Remove(task); task.Dispose(); } RaiseSummary(); @@ -217,7 +217,7 @@ public void ClearFinished() /// 取消全部进行中的任务(面板关闭时)。 public void CancelAll() { - foreach (var task in Tasks.Where(t => t.IsRunning).ToArray()) + foreach (PanelTask? task in Tasks.Where(t => t.IsRunning).ToArray()) { task.Cancel(); } @@ -226,9 +226,9 @@ public void CancelAll() private void Trim() { PanelTask[] finished = [.. Tasks.Where(t => t.IsFinished)]; - for (var i = MaxFinished; i < finished.Length; i++) + for (int i = MaxFinished; i < finished.Length; i++) { - Tasks.Remove(finished[i]); + _ = Tasks.Remove(finished[i]); finished[i].Dispose(); } } @@ -260,12 +260,12 @@ public void Accept(PullProgressFrame frame) // 没有 id 的帧是总览行("Pulling from library/nginx"、最终摘要),不计进层里。 return; } - var current = frame.ProgressDetail?.Current ?? 0; - var total = frame.ProgressDetail?.Total ?? 0; - var status = frame.Status ?? ""; + long current = frame.ProgressDetail?.Current ?? 0; + long total = frame.ProgressDetail?.Total ?? 0; + string status = frame.Status ?? ""; // "Pull complete" / "Already exists" 之后不再有字节明细, // 但它们已经算 100% —— 沿用上一次拿到的 total,免得进度条在最后一刻回退。 - if (_layers.TryGetValue(frame.Id, out var previous)) + if (_layers.TryGetValue(frame.Id, out LayerState? previous)) { if (total == 0) { diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs index 4bfbd82..9fae775 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/ThemeBrushes.cs @@ -42,7 +42,7 @@ namespace VelaShell.Plugin.DockerPanel.Ui; internal static class ThemeBrushes { /// 令牌名 → 那支长期有效的画刷。只在 UI 线程上读写(转换器与资源事件都在 UI 线程)。 - private static readonly Dictionary Tracked = new(StringComparer.Ordinal); + private static readonly Dictionary Tracked = [with(StringComparer.Ordinal)]; private static EventHandler? _hook; diff --git a/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs b/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs index 7b6d438..3d90e32 100644 --- a/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs +++ b/plugins/VelaShell.Plugin.DockerPanel/Ui/WeightedStackPanel.cs @@ -36,10 +36,12 @@ public sealed class WeightedStackPanel : Panel public static readonly AttachedProperty WeightProperty = AvaloniaProperty.RegisterAttached("Weight"); - static WeightedStackPanel() => + static WeightedStackPanel() + { // 权重变了要重排的是**父面板**(每一段的宽度都跟着变),不是那一段自己。 - WeightProperty.Changed.AddClassHandler((child, _) => + _ = WeightProperty.Changed.AddClassHandler((child, _) => (child.GetVisualParent() as WeightedStackPanel)?.InvalidateArrange()); + } /// 读 。 public static double GetWeight(Control control) => control.GetValue(WeightProperty); @@ -51,7 +53,7 @@ static WeightedStackPanel() => protected override Size MeasureOverride(Size availableSize) { double height = 0; - foreach (var child in Children) + foreach (Control child in Children) { child.Measure(availableSize); height = Math.Max(height, child.DesiredSize.Height); @@ -63,10 +65,10 @@ protected override Size MeasureOverride(Size availableSize) protected override Size ArrangeOverride(Size finalSize) { double total = 0; - var last = -1; - for (var i = 0; i < Children.Count; i++) + int last = -1; + for (int i = 0; i < Children.Count; i++) { - var weight = Weight(Children[i]); + double weight = Weight(Children[i]); total += weight; if (weight > 0) { @@ -78,13 +80,13 @@ protected override Size ArrangeOverride(Size finalSize) return finalSize; } double x = 0; - for (var i = 0; i < Children.Count; i++) + for (int i = 0; i < Children.Count; i++) { - var child = Children[i]; + Control child = Children[i]; // 舍入误差交给**最后一段有宽度的**吃掉,免得右边留一条一像素的缝。 // 不能简单地给最后一个孩子:一个 0 字节的构建缓存排在末尾时, // 它会把整条剩余宽度都画成自己的颜色 —— 明明什么都没占。 - var width = i == last + double width = i == last ? Math.Max(0, finalSize.Width - x) : finalSize.Width * (Weight(child) / total); child.Arrange(new(x, 0, width, finalSize.Height)); @@ -97,6 +99,6 @@ protected override Size ArrangeOverride(Size finalSize) private static double Weight(Control child) => child.GetValue(WeightProperty) is var attached && attached > 0 ? attached : child.Tag is double weight && weight > 0 ? weight - : child.Tag is string text && double.TryParse(text, out var parsed) && parsed > 0 ? parsed + : child.Tag is string text && double.TryParse(text, out double parsed) && parsed > 0 ? parsed : 0; } diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/ExplainPlan.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/ExplainPlan.cs index 0550729..af9fe63 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/ExplainPlan.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/ExplainPlan.cs @@ -264,7 +264,7 @@ public string Summary /// 解析一份 explain 输出。 public static ExplainPlan Parse(BsonDocument explain) { - BsonDocument? command = explain.GetValue("command", null) as BsonDocument; + var command = explain.GetValue("command", null) as BsonDocument; BsonDocument queryPlanner; BsonDocument? executionStats; var pipelineStages = new List(); @@ -524,7 +524,7 @@ private static List BuildCandidates(BsonDocument queryPlanner, { var candidates = new List(); var all = executionStats?.GetValue("allPlansExecution", null) as BsonArray; - var rejected = queryPlanner.GetValue("rejectedPlans", null) as BsonArray ?? []; + BsonArray rejected = queryPlanner.GetValue("rejectedPlans", null) as BsonArray ?? []; if (winning is null) { return candidates; @@ -536,7 +536,7 @@ private static List BuildCandidates(BsonDocument queryPlanner, var used = new HashSet(ReferenceEqualityComparer.Instance); if (winnerStats is not null) { - used.Add(winnerStats); + _ = used.Add(winnerStats); } foreach (BsonDocument plan in rejected.OfType()) { @@ -545,7 +545,7 @@ private static List BuildCandidates(BsonDocument queryPlanner, .FirstOrDefault(p => !used.Contains(p) && p.GetValue("executionStages", null) is BsonDocument s && Chain(Unwrap(s)!) == chain); if (stats is not null) { - used.Add(stats); + _ = used.Add(stats); } candidates.Add(Candidate(false, chain, stats)); } diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/IndexAdvisor.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/IndexAdvisor.cs index 39696c3..2185ce0 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/IndexAdvisor.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/IndexAdvisor.cs @@ -55,7 +55,7 @@ public BsonDocument EsrKey() var key = new BsonDocument(); foreach (string field in Equality) { - key.Set(field, 1); + _ = key.Set(field, 1); } foreach ((string path, int direction) in Sort) { @@ -103,7 +103,7 @@ public BsonDocument EsrKey() } foreach (string e in equality) { - range.Remove(e); + _ = range.Remove(e); } return equality.Count + sorts.Count + range.Count == 0 ? null : new EsrShape(equality, sorts, range); } @@ -260,7 +260,7 @@ public static IReadOnlyList GroupSlowQueries(IEnumerable? list)) { list = []; groups[shape.Signature] = list; @@ -272,19 +272,19 @@ public static IReadOnlyList GroupSlowQueries(IEnumerable { - var list = groups[sig]; - var latest = list[0]; + List<(EsrShape Shape, BsonDocument Entry, BsonDocument Filter, BsonDocument? Sort)> list = groups[sig]; + (EsrShape Shape, BsonDocument Entry, BsonDocument Filter, BsonDocument? Sort) = list[0]; return new AdvisorQueryGroup( - latest.Shape, + Shape, list.Count, list.Average(static x => Number(x.Entry, "millis")), list.Average(static x => Number(x.Entry, "docsExamined")), list.Average(static x => Number(x.Entry, "nreturned")), list.Any(static x => x.Entry.GetValue("planSummary", "").ToString()!.StartsWith("COLLSCAN", StringComparison.Ordinal)), list.Any(static x => x.Entry.GetValue("hasSortStage", false).ToBoolean()), - latest.Entry.GetValue("planSummary", "").ToString() ?? "", - latest.Filter, - latest.Sort); + Entry.GetValue("planSummary", "").ToString() ?? "", + Filter, + Sort); }) .OrderByDescending(static g => g.Count * g.AverageMillis) ]; @@ -316,24 +316,24 @@ internal static bool TryExtract(BsonDocument entry, out BsonDocument filter, out filter = Doc(command, "query"); return true; case "command" when command.Contains("aggregate") && command.GetValue("pipeline", BsonNull.Value) is BsonArray pipeline: - { - // 只看管道开头的 $match / $sort:后面的阶段吃的是上一阶段的输出,与集合索引无关。 - foreach (BsonDocument stage in pipeline.OfType()) { - if (stage.TryGetValue("$match", out BsonValue match) && match is BsonDocument m && sort is null && filter.ElementCount == 0) - { - filter = m; - continue; - } - if (stage.TryGetValue("$sort", out BsonValue s) && s is BsonDocument sd && sort is null) + // 只看管道开头的 $match / $sort:后面的阶段吃的是上一阶段的输出,与集合索引无关。 + foreach (BsonDocument stage in pipeline.OfType()) { - sort = sd; - continue; + if (stage.TryGetValue("$match", out BsonValue match) && match is BsonDocument m && sort is null && filter.ElementCount == 0) + { + filter = m; + continue; + } + if (stage.TryGetValue("$sort", out BsonValue s) && s is BsonDocument sd && sort is null) + { + sort = sd; + continue; + } + break; } - break; + return filter.ElementCount > 0 || sort is not null; } - return filter.ElementCount > 0 || sort is not null; - } default: return false; } diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaGenerator.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaGenerator.cs index 7e9e4ca..1b487ab 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaGenerator.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaGenerator.cs @@ -42,7 +42,7 @@ private static BsonDocument ObjectSchema(AnalyzedSchema report, string? parent, } if (parentPresent > 0 && field.Present >= parentPresent) { - required.Add(field.Name); + _ = required.Add(field.Name); } properties[field.Name] = PropertySchema(report, field); } @@ -170,9 +170,9 @@ public static string Format(BsonValue value, int width = 88, bool compactRoot = var builder = new StringBuilder(); if (compactRoot && value is BsonDocument { ElementCount: 1 } root && root.GetElement(0).Value is BsonDocument { ElementCount: > 0 } inner) { - builder.Append("{ ").Append(BsonText.FieldName(root.GetElement(0).Name)).Append(": "); + _ = builder.Append("{ ").Append(BsonText.FieldName(root.GetElement(0).Name)).Append(": "); WriteExpanded(builder, inner, 0, width); - builder.Append(" }"); + _ = builder.Append(" }"); return builder.ToString(); } Write(builder, value, 0, width); @@ -184,7 +184,7 @@ private static void Write(StringBuilder b, BsonValue value, int depth, int width string inline = BsonText.Literal(value); if (value is not (BsonDocument or BsonArray) || (depth * 2) + used + inline.Length <= width) { - b.Append(inline); + _ = b.Append(inline); return; } WriteExpanded(b, value, depth, width); @@ -195,20 +195,20 @@ private static void WriteExpanded(StringBuilder b, BsonValue value, int depth, i string pad = new(' ', (depth + 1) * 2); if (value is BsonDocument doc) { - b.Append('{'); + _ = b.Append('{'); int i = 0; foreach (BsonElement element in doc) { - b.Append(i++ == 0 ? "\n" : ",\n").Append(pad); + _ = b.Append(i++ == 0 ? "\n" : ",\n").Append(pad); string key = BsonText.FieldName(element.Name) + ": "; - b.Append(key); + _ = b.Append(key); Write(b, element.Value, depth + 1, width, key.Length); } - b.Append('\n').Append(' ', depth * 2).Append('}'); + _ = b.Append('\n').Append(' ', depth * 2).Append('}'); return; } BsonArray array = value.AsBsonArray; - b.Append('['); + _ = b.Append('['); if (array.All(static v => v is not (BsonDocument or BsonArray))) { // 全是标量(required、enum 的取值表):按宽度流式排,一行放得下几个放几个 —— @@ -219,25 +219,25 @@ private static void WriteExpanded(StringBuilder b, BsonValue value, int depth, i string item = BsonText.Literal(array[i]) + (i < array.Count - 1 ? "," : ""); if (column + 1 + item.Length > width && column > pad.Length) { - b.Append('\n').Append(pad); + _ = b.Append('\n').Append(pad); column = pad.Length; } else if (i > 0) { - b.Append(' '); + _ = b.Append(' '); column++; } - b.Append(item); + _ = b.Append(item); column += item.Length; } - b.Append('\n').Append(' ', depth * 2).Append(']'); + _ = b.Append('\n').Append(' ', depth * 2).Append(']'); return; } for (int i = 0; i < array.Count; i++) { - b.Append(i == 0 ? "\n" : ",\n").Append(pad); + _ = b.Append(i == 0 ? "\n" : ",\n").Append(pad); Write(b, array[i], depth + 1, width); } - b.Append('\n').Append(' ', depth * 2).Append(']'); + _ = b.Append('\n').Append(' ', depth * 2).Append(']'); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaValidator.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaValidator.cs index a82dae7..37f16c1 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaValidator.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/JsonSchemaValidator.cs @@ -30,12 +30,13 @@ internal static class JsonSchemaValidator private static readonly TimeSpan PatternTimeout = TimeSpan.FromMilliseconds(100); /// 认作"这是一份 schema 本身"的关键字(区分整个 validator 与其中的 schema)。 - private static readonly HashSet SchemaKeywords = new(StringComparer.Ordinal) - { + private static readonly HashSet SchemaKeywords = + [ + with(StringComparer.Ordinal), "bsonType", "type", "properties", "required", "additionalProperties", "patternProperties", "items", "enum", "minimum", "maximum", "minLength", "maxLength", "pattern", "anyOf", "allOf", "oneOf", "not", "minItems", "maxItems", "minProperties", "maxProperties", "title", "description" - }; + ]; /// /// 校验一份文档。 可以是整个 validator(含 $jsonSchema) @@ -133,9 +134,7 @@ public static IReadOnlyList Validate(BsonDocument validator, Bs /// 一趟递归校验。collect = false 时只数违规(anyOf / oneOf / not 的试探分支用)。 private sealed class Walker(Loc loc, bool collect) { - private readonly List _found = []; - - public List Found => _found; + public List Found { get; } = []; public int Count { get; private set; } @@ -167,7 +166,7 @@ private void Add(string path, string keyword, string message) Count++; if (collect) { - _found.Add(new(path, keyword, message)); + Found.Add(new(path, keyword, message)); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/QueryShape.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/QueryShape.cs index 2d9ef8b..a7e835f 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/QueryShape.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/QueryShape.cs @@ -35,11 +35,11 @@ public string Summary var builder = new StringBuilder(Filter); if (Sort is { Length: > 0 }) { - builder.Append(" sort ").Append(Sort); + _ = builder.Append(" sort ").Append(Sort); } if (Extra is { Length: > 0 }) { - builder.Append(" ").Append(Extra); + _ = builder.Append(" ").Append(Extra); } return builder.ToString(); } @@ -127,14 +127,14 @@ public static string NormalizePipeline(BsonArray pipeline) var builder = new StringBuilder("["); for (int i = 0; i < pipeline.Count; i++) { - builder.Append(i == 0 ? "" : ", "); + _ = builder.Append(i == 0 ? "" : ", "); if (pipeline[i] is not BsonDocument { ElementCount: > 0 } stage) { - builder.Append('?'); + _ = builder.Append('?'); continue; } BsonElement head = stage.GetElement(0); - builder.Append(head.Name).Append(' '); + _ = builder.Append(head.Name).Append(' '); if (head.Name == "$match") { WriteQuery(builder, head.Value); @@ -158,7 +158,7 @@ internal static string Plain(BsonDocument doc) int i = 0; foreach (BsonElement element in doc) { - builder.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": ").Append(BsonText.Literal(element.Value)); + _ = builder.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": ").Append(BsonText.Literal(element.Value)); } return builder.Append(" }").ToString(); } @@ -186,32 +186,32 @@ private static void WriteQuery(StringBuilder b, BsonValue value) { if (value is not BsonDocument doc) { - b.Append('?'); + _ = b.Append('?'); return; } if (doc.ElementCount == 0) { - b.Append("{}"); + _ = b.Append("{}"); return; } - b.Append("{ "); + _ = b.Append("{ "); int i = 0; foreach (BsonElement element in doc) { - b.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": "); + _ = b.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": "); switch (element.Name) { case "$and" or "$or" or "$nor" when element.Value is BsonArray branches: - b.Append('['); + _ = b.Append('['); for (int j = 0; j < branches.Count; j++) { - b.Append(j == 0 ? "" : ", "); + _ = b.Append(j == 0 ? "" : ", "); WriteQuery(b, branches[j]); } - b.Append(']'); + _ = b.Append(']'); break; case "$exists" when element.Value.IsBoolean: - b.Append(element.Value.AsBoolean ? "true" : "false"); + _ = b.Append(element.Value.AsBoolean ? "true" : "false"); break; case "$expr": WriteExpression(b, element.Value); @@ -224,12 +224,12 @@ private static void WriteQuery(StringBuilder b, BsonValue value) } else { - b.Append('?'); + _ = b.Append('?'); } break; } } - b.Append(" }"); + _ = b.Append(" }"); } private static void WriteExpression(StringBuilder b, BsonValue value) @@ -239,14 +239,14 @@ private static void WriteExpression(StringBuilder b, BsonValue value) case BsonDocument doc: if (doc.ElementCount == 0) { - b.Append("{}"); + _ = b.Append("{}"); return; } - b.Append("{ "); + _ = b.Append("{ "); int i = 0; foreach (BsonElement element in doc) { - b.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": "); + _ = b.Append(i++ == 0 ? "" : ", ").Append(element.Name).Append(": "); if (element.Name == "$match") { // $lookup / $facet 里嵌套的子管道:$match 依旧按条件打码。 @@ -257,27 +257,27 @@ private static void WriteExpression(StringBuilder b, BsonValue value) WriteExpression(b, element.Value); } } - b.Append(" }"); + _ = b.Append(" }"); return; case BsonArray array: - b.Append('['); + _ = b.Append('['); for (int j = 0; j < array.Count; j++) { - b.Append(j == 0 ? "" : ", "); + _ = b.Append(j == 0 ? "" : ", "); WriteExpression(b, array[j]); } - b.Append(']'); + _ = b.Append(']'); return; case BsonString s: // 表达式里的字符串多半是结构($字段路径、$lookup 的 from / as);常量字符串也一并保留, // 宁可两个形状分开,也不要把 "$device" 这种关键信息抹成问号。 - b.Append(s.Value.StartsWith('$') ? s.Value : BsonText.Quote(s.Value)); + _ = b.Append(s.Value.StartsWith('$') ? s.Value : BsonText.Quote(s.Value)); return; case BsonInt32 or BsonInt64 or BsonDouble or BsonDecimal128 or BsonBoolean or BsonNull: - b.Append(BsonText.Literal(value)); + _ = b.Append(BsonText.Literal(value)); return; default: - b.Append('?'); + _ = b.Append('?'); return; } } @@ -287,12 +287,13 @@ private static void WriteExpression(StringBuilder b, BsonValue value) ["find", "aggregate", "count", "distinct", "findAndModify", "findandmodify", "update", "delete", "insert"]; /// 命令里与语句无关的会话 / 路由字段(还原 mongosh 语句时去掉)。 - private static readonly HashSet Plumbing = new(StringComparer.Ordinal) - { + private static readonly HashSet Plumbing = + [ + with(StringComparer.Ordinal), "lsid", "$clusterTime", "$db", "$readPreference", "txnNumber", "autocommit", "startTransaction", "$audit", "$client", "mayBypassWriteBlocking", "cursor", "batchSize", "singleBatch", "comment", "maxTimeMS", "readConcern", "writeConcern", "$configTime", "$topologyTime", "shardVersion", "databaseVersion" - }; + ]; /// /// 把一条 profile 记录还原成可以粘进查询编辑器的 mongosh 语句,并给出对应的执行计划写法。 @@ -320,24 +321,24 @@ public static (string Statement, string Explain) Rebuild(BsonDocument entry) var builder = new StringBuilder($"{db}.find({Arg(Doc(command, "filter") ?? [])}"); if (Doc(command, "projection") is { ElementCount: > 0 } projection) { - builder.Append(", ").Append(BsonText.Literal(projection)); + _ = builder.Append(", ").Append(BsonText.Literal(projection)); } - builder.Append(')'); + _ = builder.Append(')'); if (Doc(command, "sort") is { ElementCount: > 0 } sort) { - builder.Append(".sort(").Append(BsonText.Literal(sort)).Append(')'); + _ = builder.Append(".sort(").Append(BsonText.Literal(sort)).Append(')'); } if (command.TryGetValue("hint", out BsonValue hint)) { - builder.Append(".hint(").Append(BsonText.Literal(hint)).Append(')'); + _ = builder.Append(".hint(").Append(BsonText.Literal(hint)).Append(')'); } if (command.TryGetValue("skip", out BsonValue skip) && skip.IsNumeric && skip.ToInt64() != 0) { - builder.Append(".skip(").Append(skip.ToInt64()).Append(')'); + _ = builder.Append(".skip(").Append(skip.ToInt64()).Append(')'); } if (command.TryGetValue("limit", out BsonValue limit) && limit.IsNumeric && limit.ToInt64() != 0) { - builder.Append(".limit(").Append(Math.Abs(limit.ToInt64())).Append(')'); + _ = builder.Append(".limit(").Append(Math.Abs(limit.ToInt64())).Append(')'); } string find = builder.ToString(); return (find, $"{find}.explain({verbosity})"); diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/SchemaAnalyzer.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/SchemaAnalyzer.cs index 4f71d0a..30be840 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/SchemaAnalyzer.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/SchemaAnalyzer.cs @@ -148,16 +148,16 @@ private void ObserveValue(BsonValue value, BsonKind kind) case BsonKind.Object: return; case BsonKind.Array: - { - BsonArray array = value.AsBsonArray; - ArrayLengths[array.Count] = ArrayLengths.GetValueOrDefault(array.Count) + 1; - foreach (BsonValue item in array) { - BsonKind itemKind = BsonKinds.Of(item); - ElementKinds[itemKind] = ElementKinds.GetValueOrDefault(itemKind) + 1; + BsonArray array = value.AsBsonArray; + ArrayLengths[array.Count] = ArrayLengths.GetValueOrDefault(array.Count) + 1; + foreach (BsonValue item in array) + { + BsonKind itemKind = BsonKinds.Of(item); + ElementKinds[itemKind] = ElementKinds.GetValueOrDefault(itemKind) + 1; + } + return; } - return; - } } ScalarCount++; ValueBytes += EstimateBytes(value); @@ -323,7 +323,7 @@ private static void Collect(BsonDocument doc, string? prefix, int depth, bool in list.Add(element.Value); if (inArray) { - arrayPaths.Add(path); + _ = arrayPaths.Add(path); } if (depth <= 0) { @@ -438,7 +438,7 @@ public static (double[] Bins, double Min, double Max, bool Clipped) Histogram(An { min = 0; } - var bins = new double[HistogramBins]; + double[] bins = new double[HistogramBins]; double width = max > min ? (max - min) / HistogramBins : 1; foreach (double v in sorted) { @@ -462,7 +462,7 @@ public static (double[] Bins, DateTime Start, DateTime End, int DaysPerBin) Time int days = (int)(end - start).TotalDays + 1; int perBin = days > 120 ? 7 : 1; int count = Math.Max(1, (int)Math.Ceiling(days / (double)perBin)); - var bins = new double[count]; + double[] bins = new double[count]; foreach (DateTime date in field.Dates) { int index = (int)((date.Date - start).TotalDays / perBin); @@ -524,31 +524,31 @@ public static (string Text, bool Warning) Describe(AnalyzedField field, Loc loc) ? (loc.Format("Design_HintNearlyUnique", name), false) : (loc.Format("Design_HintDistinct", name, BsonText.Grouped(field.DistinctCount)), false); case BsonKind.Int32 or BsonKind.Int64 or BsonKind.Double or BsonKind.Decimal128 when field.Numbers.Count > 0: - { - double[] sorted = [.. field.Numbers.Order()]; - return (loc.Format("Design_HintNumeric", name, Number(Percentile(sorted, 0.5)), Number(Percentile(sorted, 0.95))), false); - } + { + double[] sorted = [.. field.Numbers.Order()]; + return (loc.Format("Design_HintNumeric", name, Number(Percentile(sorted, 0.5)), Number(Percentile(sorted, 0.95))), false); + } case BsonKind.Date when field.Dates.Count > 0: return (WeekendDip(field) ? loc.Format("Design_HintWeekendDip", name) : loc.Format("Design_HintDateSpan", name, (int)(field.Dates.Max() - field.Dates.Min()).TotalDays + 1), false); case BsonKind.Array: - { - if (field.ElementDocuments > 0) { - return (loc.Format("Design_HintArrayObjects", name, Pct(field.CompleteElementDocuments, field.ElementDocuments)), false); + if (field.ElementDocuments > 0) + { + return (loc.Format("Design_HintArrayObjects", name, Pct(field.CompleteElementDocuments, field.ElementDocuments)), false); + } + int arrays = field.ArrayLengths.Values.Sum(); + double avg = arrays == 0 ? 0 : field.ArrayLengths.Sum(static p => (double)p.Key * p.Value) / arrays; + return (loc.Format("Design_HintArrayAvg", name, avg.ToString("0.#", CultureInfo.InvariantCulture)), false); } - int arrays = field.ArrayLengths.Values.Sum(); - double avg = arrays == 0 ? 0 : field.ArrayLengths.Sum(static p => (double)p.Key * p.Value) / arrays; - return (loc.Format("Design_HintArrayAvg", name, avg.ToString("0.#", CultureInfo.InvariantCulture)), false); - } case BsonKind.Object: return (loc.Format("Design_HintObject", name, field.Children.Count), false); case BsonKind.Boolean: - { - int trues = field.Values.TryGetValue(BsonBoolean.True, out int t) ? t : 0; - return (loc.Format("Design_HintBoolean", name, Pct(trues, field.ScalarCount)), false); - } + { + int trues = field.Values.TryGetValue(BsonBoolean.True, out int t) ? t : 0; + return (loc.Format("Design_HintBoolean", name, Pct(trues, field.ScalarCount)), false); + } case BsonKind.ObjectId: return (loc.Format("Design_HintReference", name, BsonText.Grouped(field.DistinctCount)), false); default: @@ -582,28 +582,28 @@ public static string VizText(AnalyzedField field, Loc loc) switch (kind) { case BsonKind.ObjectId when field.IdTimes.Count > 0: - { - string head = field.IsUnique - ? loc.Format("Design_VizUniqueValues", BsonText.Grouped(field.DistinctCount)) - : loc.Format("Design_VizDistinct", BsonText.Grouped(field.DistinctCount)); - DateTime from = field.IdTimes.Min().ToLocalTime(); - DateTime to = field.IdTimes.Max().ToLocalTime(); - string range = from.Year == to.Year - ? $"{from:yyyy-MM-dd} → {to:MM-dd}" - : $"{from:yyyy-MM-dd} → {to:yyyy-MM-dd}"; - return $"{head} · {loc.Format("Design_VizIdTime", range)}"; - } + { + string head = field.IsUnique + ? loc.Format("Design_VizUniqueValues", BsonText.Grouped(field.DistinctCount)) + : loc.Format("Design_VizDistinct", BsonText.Grouped(field.DistinctCount)); + DateTime from = field.IdTimes.Min().ToLocalTime(); + DateTime to = field.IdTimes.Max().ToLocalTime(); + string range = from.Year == to.Year + ? $"{from:yyyy-MM-dd} → {to:MM-dd}" + : $"{from:yyyy-MM-dd} → {to:yyyy-MM-dd}"; + return $"{head} · {loc.Format("Design_VizIdTime", range)}"; + } case BsonKind.String: - { - string head = field.IsUnique - ? loc.Format("Design_VizUniqueValues", BsonText.Grouped(field.DistinctCount)) - : loc.Format("Design_VizDistinct", BsonText.Grouped(field.DistinctCount) + (field.DistinctOverflow ? "+" : "")); - string example = field.Values.Keys.FirstOrDefault() is { } first ? Label(first) : ""; - string lengths = field.StringLengths.Count == 0 - ? "" - : " · " + loc.Format("Design_VizLength", field.StringLengths.Min(), field.StringLengths.Max()); - return example.Length == 0 ? head + lengths : $"{head} · {loc.Format("Design_VizExample", example)}{lengths}"; - } + { + string head = field.IsUnique + ? loc.Format("Design_VizUniqueValues", BsonText.Grouped(field.DistinctCount)) + : loc.Format("Design_VizDistinct", BsonText.Grouped(field.DistinctCount) + (field.DistinctOverflow ? "+" : "")); + string example = field.Values.Keys.FirstOrDefault() is { } first ? Label(first) : ""; + string lengths = field.StringLengths.Count == 0 + ? "" + : " · " + loc.Format("Design_VizLength", field.StringLengths.Min(), field.StringLengths.Max()); + return example.Length == 0 ? head + lengths : $"{head} · {loc.Format("Design_VizExample", example)}{lengths}"; + } case BsonKind.Object: return loc.Format("Design_VizChildren", string.Join(" · ", field.Children.Take(6)) + (field.Children.Count > 6 ? " …" : "")); case BsonKind.Null: @@ -634,7 +634,7 @@ internal static bool WeekendDip(AnalyzedField field) { return false; } - var byDay = new int[7]; + int[] byDay = new int[7]; foreach (DateTime date in field.Dates) { byDay[(int)date.ToLocalTime().DayOfWeek]++; diff --git a/plugins/VelaShell.Plugin.Mongo/Analysis/ServerSampler.cs b/plugins/VelaShell.Plugin.Mongo/Analysis/ServerSampler.cs index 70f7bd2..4b1ca84 100644 --- a/plugins/VelaShell.Plugin.Mongo/Analysis/ServerSampler.cs +++ b/plugins/VelaShell.Plugin.Mongo/Analysis/ServerSampler.cs @@ -257,7 +257,7 @@ public IEnumerable Within(TimeSpan window, DateTime now) => /// 采样间隔。 public BucketSet Buckets(TimeSpan window, int maxBars, DateTime now, TimeSpan interval) { - List points = Within(window, now).ToList(); + var points = Within(window, now).ToList(); if (points.Count == 0 || maxBars <= 0) { return new([], window.TotalSeconds / Math.Max(1, maxBars), TimeSpan.Zero); @@ -318,7 +318,7 @@ public BucketSet Buckets(TimeSpan window, int maxBars, DateTime now, TimeSpan in return null; } RateSample latest = _rates[^1]; - List baseline = _rates.Take(_rates.Count - 1) + var baseline = _rates.Take(_rates.Count - 1) .Where(r => r.Time > latest.Time - TimeSpan.FromMinutes(5)) .Select(select) .ToList(); @@ -411,7 +411,7 @@ public static ReplicaSnapshot Parse(BsonDocument status, BsonDocument? config) int voters = 0; foreach ((string name, string state, bool healthy, DateTime? applied, DateTime? heartbeat, bool self) in raw) { - (double priority, int votes) = settings.TryGetValue(name, out var cfgMember) ? cfgMember : (1, 1); + (double priority, int votes) = settings.TryGetValue(name, out (double Priority, int Votes) cfgMember) ? cfgMember : (1, 1); double? lag = state is "PRIMARY" or "ARBITER" || applied is null || primaryApplied is null ? null : Math.Max(0, (primaryApplied.Value - applied.Value).TotalSeconds); diff --git a/plugins/VelaShell.Plugin.Mongo/Bson/BsonEdit.cs b/plugins/VelaShell.Plugin.Mongo/Bson/BsonEdit.cs index 5fba271..e8d1703 100644 --- a/plugins/VelaShell.Plugin.Mongo/Bson/BsonEdit.cs +++ b/plugins/VelaShell.Plugin.Mongo/Bson/BsonEdit.cs @@ -87,27 +87,27 @@ public static bool TryParse(string? text, BsonKind kind, out BsonValue value, ou error = "Edit_NotDate"; return false; case BsonKind.ObjectId: - { - string hex = Unwrap(trimmed, "ObjectId"); - if (ObjectId.TryParse(hex, out ObjectId id)) { - value = id; - return true; + string hex = Unwrap(trimmed, "ObjectId"); + if (ObjectId.TryParse(hex, out ObjectId id)) + { + value = id; + return true; + } + error = "Edit_NotObjectId"; + return false; } - error = "Edit_NotObjectId"; - return false; - } case BsonKind.Uuid: - { - string guid = Unwrap(trimmed, "UUID"); - if (Guid.TryParse(guid, out Guid g)) { - value = new BsonBinaryData(g, GuidRepresentation.Standard); - return true; + string guid = Unwrap(trimmed, "UUID"); + if (Guid.TryParse(guid, out Guid g)) + { + value = new BsonBinaryData(g, GuidRepresentation.Standard); + return true; + } + error = "Edit_NotUuid"; + return false; } - error = "Edit_NotUuid"; - return false; - } case BsonKind.Object or BsonKind.Array: try { diff --git a/plugins/VelaShell.Plugin.Mongo/Bson/BsonPath.cs b/plugins/VelaShell.Plugin.Mongo/Bson/BsonPath.cs index 0d4f35f..6146cef 100644 --- a/plugins/VelaShell.Plugin.Mongo/Bson/BsonPath.cs +++ b/plugins/VelaShell.Plugin.Mongo/Bson/BsonPath.cs @@ -73,7 +73,7 @@ public static bool Set(BsonDocument root, string path, BsonValue value) list[at] = value; return true; case BsonArray append when last == append.Count.ToString(CultureInfo.InvariantCulture): - append.Add(value); + _ = append.Add(value); return true; default: return false; diff --git a/plugins/VelaShell.Plugin.Mongo/Bson/BsonText.cs b/plugins/VelaShell.Plugin.Mongo/Bson/BsonText.cs index fce9027..c21039c 100644 --- a/plugins/VelaShell.Plugin.Mongo/Bson/BsonText.cs +++ b/plugins/VelaShell.Plugin.Mongo/Bson/BsonText.cs @@ -135,123 +135,123 @@ private static void WriteShell(StringBuilder b, BsonValue value, string? indent, switch (value.BsonType) { case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - if (doc.ElementCount == 0) { - b.Append("{}"); + BsonDocument doc = value.AsBsonDocument; + if (doc.ElementCount == 0) + { + _ = b.Append("{}"); + return; + } + _ = b.Append('{'); + int i = 0; + foreach (BsonElement element in doc) + { + _ = b.Append(i++ == 0 ? "" : ","); + NewLine(b, indent, depth + 1); + _ = b.Append(FieldName(element.Name)).Append(": "); + WriteShell(b, element.Value, indent, depth + 1); + } + NewLine(b, indent, depth); + _ = b.Append('}'); return; } - b.Append('{'); - int i = 0; - foreach (BsonElement element in doc) - { - b.Append(i++ == 0 ? "" : ","); - NewLine(b, indent, depth + 1); - b.Append(FieldName(element.Name)).Append(": "); - WriteShell(b, element.Value, indent, depth + 1); - } - NewLine(b, indent, depth); - b.Append('}'); - return; - } case BsonType.Array: - { - BsonArray array = value.AsBsonArray; - if (array.Count == 0) - { - b.Append("[]"); - return; - } - // 全是标量的短数组写在一行:["企业", "开票"] 拆成三行只会把 JSON 视图拉长。 - bool flat = indent is null || array.All(static v => !v.IsBsonDocument && !v.IsBsonArray) && array.Count <= 8; - b.Append('['); - for (int i = 0; i < array.Count; i++) { - b.Append(i == 0 ? "" : ","); + BsonArray array = value.AsBsonArray; + if (array.Count == 0) + { + _ = b.Append("[]"); + return; + } + // 全是标量的短数组写在一行:["企业", "开票"] 拆成三行只会把 JSON 视图拉长。 + bool flat = indent is null || array.All(static v => !v.IsBsonDocument && !v.IsBsonArray) && array.Count <= 8; + _ = b.Append('['); + for (int i = 0; i < array.Count; i++) + { + _ = b.Append(i == 0 ? "" : ","); + if (flat) + { + _ = b.Append(' '); + } + else + { + NewLine(b, indent, depth + 1); + } + WriteShell(b, array[i], indent, depth + 1); + } if (flat) { - b.Append(' '); + _ = b.Append(" ]"); } else { - NewLine(b, indent, depth + 1); + NewLine(b, indent, depth); + _ = b.Append(']'); } - WriteShell(b, array[i], indent, depth + 1); - } - if (flat) - { - b.Append(" ]"); - } - else - { - NewLine(b, indent, depth); - b.Append(']'); + return; } - return; - } case BsonType.ObjectId: - b.Append("ObjectId(\"").Append(value.AsObjectId).Append("\")"); + _ = b.Append("ObjectId(\"").Append(value.AsObjectId).Append("\")"); return; case BsonType.String: - b.Append(Quote(value.AsString)); + _ = b.Append(Quote(value.AsString)); return; case BsonType.Int32: - b.Append(value.AsInt32.ToString(CultureInfo.InvariantCulture)); + _ = b.Append(value.AsInt32.ToString(CultureInfo.InvariantCulture)); return; case BsonType.Int64: - b.Append("NumberLong(\"").Append(value.AsInt64.ToString(CultureInfo.InvariantCulture)).Append("\")"); + _ = b.Append("NumberLong(\"").Append(value.AsInt64.ToString(CultureInfo.InvariantCulture)).Append("\")"); return; case BsonType.Double: - b.Append(FormatDouble(value.AsDouble, forceDecimalPoint: false)); + _ = b.Append(FormatDouble(value.AsDouble, forceDecimalPoint: false)); return; case BsonType.Decimal128: - b.Append("NumberDecimal(\"").Append(value.AsDecimal128).Append("\")"); + _ = b.Append("NumberDecimal(\"").Append(value.AsDecimal128).Append("\")"); return; case BsonType.Boolean: - b.Append(value.AsBoolean ? "true" : "false"); + _ = b.Append(value.AsBoolean ? "true" : "false"); return; case BsonType.DateTime: - b.Append("ISODate(\"").Append(IsoDate(value)).Append("\")"); + _ = b.Append("ISODate(\"").Append(IsoDate(value)).Append("\")"); return; case BsonType.Null: - b.Append("null"); + _ = b.Append("null"); return; case BsonType.Binary: - { - BsonBinaryData binary = value.AsBsonBinaryData; - if (binary.SubType == BsonBinarySubType.UuidStandard) { - b.Append("UUID(\"").Append(binary.ToGuid()).Append("\")"); + BsonBinaryData binary = value.AsBsonBinaryData; + if (binary.SubType == BsonBinarySubType.UuidStandard) + { + _ = b.Append("UUID(\"").Append(binary.ToGuid()).Append("\")"); + } + else + { + _ = b.Append("BinData(").Append((int)binary.SubType).Append(", \"") + .Append(Convert.ToBase64String(binary.Bytes)).Append("\")"); + } + return; } - else + case BsonType.RegularExpression: { - b.Append("BinData(").Append((int)binary.SubType).Append(", \"") - .Append(Convert.ToBase64String(binary.Bytes)).Append("\")"); + BsonRegularExpression regex = value.AsBsonRegularExpression; + _ = b.Append('/').Append(regex.Pattern.Replace("/", "\\/", StringComparison.Ordinal)).Append('/').Append(regex.Options); + return; } - return; - } - case BsonType.RegularExpression: - { - BsonRegularExpression regex = value.AsBsonRegularExpression; - b.Append('/').Append(regex.Pattern.Replace("/", "\\/", StringComparison.Ordinal)).Append('/').Append(regex.Options); - return; - } case BsonType.Timestamp: - b.Append("Timestamp({ t: ").Append(value.AsBsonTimestamp.Timestamp) + _ = b.Append("Timestamp({ t: ").Append(value.AsBsonTimestamp.Timestamp) .Append(", i: ").Append(value.AsBsonTimestamp.Increment).Append(" })"); return; case BsonType.MinKey: - b.Append("MinKey()"); + _ = b.Append("MinKey()"); return; case BsonType.MaxKey: - b.Append("MaxKey()"); + _ = b.Append("MaxKey()"); return; case BsonType.Undefined: - b.Append("undefined"); + _ = b.Append("undefined"); return; default: - b.Append(value.ToJson()); + _ = b.Append(value.ToJson()); return; } } @@ -260,13 +260,13 @@ private static void NewLine(StringBuilder b, string? indent, int depth) { if (indent is null) { - b.Append(' '); + _ = b.Append(' '); return; } - b.Append('\n'); + _ = b.Append('\n'); for (int i = 0; i < depth; i++) { - b.Append(indent); + _ = b.Append(indent); } } @@ -277,29 +277,29 @@ private static void NewLine(StringBuilder b, string? indent, int depth) public static string Quote(string text) { var b = new StringBuilder(text.Length + 2); - b.Append('"'); + _ = b.Append('"'); foreach (char c in text) { switch (c) { - case '"': b.Append("\\\""); break; - case '\\': b.Append("\\\\"); break; - case '\n': b.Append("\\n"); break; - case '\r': b.Append("\\r"); break; - case '\t': b.Append("\\t"); break; + case '"': _ = b.Append("\\\""); break; + case '\\': _ = b.Append("\\\\"); break; + case '\n': _ = b.Append("\\n"); break; + case '\r': _ = b.Append("\\r"); break; + case '\t': _ = b.Append("\\t"); break; default: if (c < 0x20) { - b.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); + _ = b.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture)); } else { - b.Append(c); + _ = b.Append(c); } break; } } - b.Append('"'); + _ = b.Append('"'); return b.ToString(); } @@ -307,7 +307,7 @@ public static string Quote(string text) public static string IsoDate(BsonValue value) { long ms = value.AsBsonDateTime.MillisecondsSinceEpoch; - DateTimeOffset at = DateTimeOffset.FromUnixTimeMilliseconds(ms); + var at = DateTimeOffset.FromUnixTimeMilliseconds(ms); return at.UtcDateTime.ToString(at.Millisecond == 0 ? "yyyy-MM-ddTHH:mm:ssZ" : "yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture); } @@ -316,7 +316,7 @@ public static string IsoDate(BsonValue value) public static string FormatDate(BsonValue value) { long ms = value.AsBsonDateTime.MillisecondsSinceEpoch; - if (ms < -62135596800000L || ms > 253402300799999L) + if (ms is < -62135596800000L or > 253402300799999L) { return ms.ToString(CultureInfo.InvariantCulture); } diff --git a/plugins/VelaShell.Plugin.Mongo/Bson/ShellJson.cs b/plugins/VelaShell.Plugin.Mongo/Bson/ShellJson.cs index bbab3c6..e13d610 100644 --- a/plugins/VelaShell.Plugin.Mongo/Bson/ShellJson.cs +++ b/plugins/VelaShell.Plugin.Mongo/Bson/ShellJson.cs @@ -129,7 +129,7 @@ internal static string Normalize(string text, out IReadOnlyList if (c is '"' or '\'') { int end = SkipString(text, i); - output.Append(text, i, end - i); + _ = output.Append(text, i, end - i); i = end; continue; } @@ -152,14 +152,14 @@ internal static string Normalize(string text, out IReadOnlyList if (c == '/' && PreviousSignificant(output) is ':' or ',' or '[' or '(' or '\0') { int end = SkipRegex(text, i); - output.Append(text, i, end - i); + _ = output.Append(text, i, end - i); i = end; continue; } if (c is '{' or '[' or '(') { stack.Push(c); - output.Append(c); + _ = output.Append(c); i++; continue; } @@ -169,9 +169,9 @@ internal static string Normalize(string text, out IReadOnlyList TrimTrailingComma(output); if (stack.Count > 0) { - stack.Pop(); + _ = stack.Pop(); } - output.Append(c); + _ = output.Append(c); i++; continue; } @@ -196,15 +196,15 @@ internal static string Normalize(string text, out IReadOnlyList { found.Add(new(start, end - start, "Shell_DottedKey", word, "\"" + word + "\"")); } - output.Append('"').Append(word).Append('"'); + _ = output.Append('"').Append(word).Append('"'); i = end; continue; } - output.Append(word); + _ = output.Append(word); i = end; continue; } - output.Append(c); + _ = output.Append(c); i++; } diagnostics = found; @@ -235,7 +235,7 @@ private static void TrimTrailingComma(StringBuilder output) } if (output[k] == ',') { - output.Remove(k, 1); + _ = output.Remove(k, 1); } return; } diff --git a/plugins/VelaShell.Plugin.Mongo/Core/ConnectionProbe.cs b/plugins/VelaShell.Plugin.Mongo/Core/ConnectionProbe.cs index 9f71766..3b25100 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/ConnectionProbe.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/ConnectionProbe.cs @@ -5,7 +5,6 @@ using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Driver.Core.Configuration; -using VelaShell.PluginSdk.Protocols; using VelaShell.PluginSdk.Workspaces; namespace VelaShell.Plugin.Mongo.Core; @@ -243,10 +242,10 @@ public static async Task ProbeAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - run.Finish("ssh", ProbeState.Failed, MongoConnector.Describe(ex)); + _ = run.Finish("ssh", ProbeState.Failed, MongoConnector.Describe(ex)); return run.Report(null); } - run.Finish("ssh", ProbeState.Passed, + _ = run.Finish("ssh", ProbeState.Passed, loc.Format("Conn_SshDetail", tunnel.JumpName, tunnel.LocalPort, (int)watch.ElapsedMilliseconds), (int)watch.ElapsedMilliseconds); request = tunnel.Rewrite(request); } @@ -264,7 +263,7 @@ public static async Task ProbeAsync( } catch (Exception ex) when (ex is not OperationCanceledException) { - run.Finish("auth", ProbeState.Failed, translate(ex, settings)); + _ = run.Finish("auth", ProbeState.Failed, translate(ex, settings)); return run.Report(null); } await using (connection.ConfigureAwait(false)) @@ -307,12 +306,12 @@ private static async Task TcpStepAsync( try { int ms = await tunnel.CheckTargetAsync(cancellationToken).ConfigureAwait(false); - run.Finish("tcp", ProbeState.Passed, loc.Format("Conn_TcpViaTunnel", tunnel.Target, ms), ms); + _ = run.Finish("tcp", ProbeState.Passed, loc.Format("Conn_TcpViaTunnel", tunnel.Target, ms), ms); return true; } catch (Exception ex) when (ex is not OperationCanceledException) { - run.Finish("tcp", ProbeState.Failed, MongoConnector.Describe(ex)); + _ = run.Finish("tcp", ProbeState.Failed, MongoConnector.Describe(ex)); return false; } } @@ -323,20 +322,20 @@ private static async Task TcpStepAsync( if (target.Srv) { // SRV 的成员由驱动按 DNS 解析,这里没有现成的地址可敲;交给后面的认证一步去证明连得上。 - run.Finish("tcp", ProbeState.Skipped, loc["Conn_TcpSrv"]); + _ = run.Finish("tcp", ProbeState.Skipped, loc["Conn_TcpSrv"]); return true; } targets = [.. target.Hosts.Select(ToEndPoint)]; } catch (Exception ex) when (ex is MongoConfigurationException or ArgumentException or FormatException) { - run.Finish("tcp", ProbeState.Failed, ex.Message); + _ = run.Finish("tcp", ProbeState.Failed, ex.Message); return false; } (bool Ok, int Ms)[] results = await Task.WhenAll(targets.Select(t => TcpAsync(t, cancellationToken))).ConfigureAwait(false); int reachable = results.Count(static r => r.Ok); int fastest = results.Where(static r => r.Ok).Select(static r => r.Ms).DefaultIfEmpty(0).Min(); - run.Finish("tcp", + _ = run.Finish("tcp", reachable == results.Length ? ProbeState.Passed : reachable > 0 ? ProbeState.Warning : ProbeState.Failed, loc.Format("Conn_TcpDetail", reachable, results.Length, fastest), reachable > 0 ? fastest : null); @@ -372,14 +371,14 @@ private static async Task AuthStepAsync(Run run, MongoConnection connectio BsonDocument status = await connection.RunCommandAsync("admin", new BsonDocument("connectionStatus", 1), cancellationToken).ConfigureAwait(false); BsonArray users = status.GetValue("authInfo", new BsonDocument()).AsBsonDocument .GetValue("authenticatedUsers", new BsonArray()).AsBsonArray; - run.Finish("auth", ProbeState.Passed, users.Count == 0 + _ = run.Finish("auth", ProbeState.Passed, users.Count == 0 ? loc["Conn_Anonymous"] : string.Join(", ", users.Select(static u => $"{u["user"].AsString}@{u["db"].AsString}"))); return true; } catch (Exception ex) when (ex is MongoException or TimeoutException) { - run.Finish("auth", ProbeState.Failed, MongoConnector.Describe(ex)); + _ = run.Finish("auth", ProbeState.Failed, MongoConnector.Describe(ex)); return false; } } @@ -403,13 +402,13 @@ private static async Task AuthStepAsync(Run run, MongoConnection connectio } ServerInfo info = MongoConnection.ParseHello(hello, build); string head = info.SetName is { Length: > 0 } set ? $"{set} · {info.Role}" : info.Role; - run.Finish("hello", ProbeState.Passed, + _ = run.Finish("hello", ProbeState.Passed, $"{head} {info.Me}".TrimEnd() + (info.Version.Length > 0 ? $" · {info.Version}" : ""), ms); return ms; } catch (Exception ex) when (ex is MongoException or TimeoutException) { - run.Finish("hello", ProbeState.Failed, MongoConnector.Describe(ex)); + _ = run.Finish("hello", ProbeState.Failed, MongoConnector.Describe(ex)); return null; } } @@ -424,25 +423,25 @@ private static async Task PrivilegeStepAsync(Run run, MongoConnection connection } catch (Exception ex) when (ex is MongoException or TimeoutException) { - run.Finish("privileges", ProbeState.Warning, MongoConnector.Describe(ex)); + _ = run.Finish("privileges", ProbeState.Warning, MongoConnector.Describe(ex)); return; } PrivilegeSummary privileges = connection.Privileges; string roles = string.Join(loc["Conn_ListSeparator"], privileges.Roles.Select(static r => r.ToString())); if (privileges.User.Length == 0) { - run.Finish("privileges", ProbeState.Passed, loc["Conn_PrivUnrestricted"]); + _ = run.Finish("privileges", ProbeState.Passed, loc["Conn_PrivUnrestricted"]); } else if (!privileges.CanWriteAnything) { - run.Finish("privileges", ProbeState.Warning, loc.Format("Conn_PrivReadOnly", roles.Length > 0 ? roles : "—")); + _ = run.Finish("privileges", ProbeState.Warning, loc.Format("Conn_PrivReadOnly", roles.Length > 0 ? roles : "—")); } else { string writable = privileges.WritableDatabases.Contains("*") ? loc["Conn_PrivAllDatabases"] : string.Join(", ", privileges.WritableDatabases.Order(StringComparer.Ordinal)); - run.Finish("privileges", ProbeState.Passed, loc.Format("Conn_PrivWritable", roles, writable)); + _ = run.Finish("privileges", ProbeState.Passed, loc.Format("Conn_PrivWritable", roles, writable)); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Core/GridFsService.cs b/plugins/VelaShell.Plugin.Mongo/Core/GridFsService.cs index afefe1f..a2e2000 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/GridFsService.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/GridFsService.cs @@ -126,20 +126,50 @@ internal static partial class GridFsPaths { private static readonly Dictionary MimeByExtension = new(StringComparer.OrdinalIgnoreCase) { - [".jpg"] = "image/jpeg", [".jpeg"] = "image/jpeg", [".png"] = "image/png", [".gif"] = "image/gif", - [".webp"] = "image/webp", [".bmp"] = "image/bmp", [".svg"] = "image/svg+xml", [".ico"] = "image/x-icon", - [".tif"] = "image/tiff", [".tiff"] = "image/tiff", [".avif"] = "image/avif", [".heic"] = "image/heic", - [".pdf"] = "application/pdf", [".txt"] = "text/plain", [".md"] = "text/markdown", [".csv"] = "text/csv", - [".json"] = "application/json", [".xml"] = "application/xml", [".html"] = "text/html", [".htm"] = "text/html", - [".doc"] = "application/msword", [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - [".xls"] = "application/vnd.ms-excel", [".xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - [".ppt"] = "application/vnd.ms-powerpoint", [".pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation", - [".rtf"] = "application/rtf", [".odt"] = "application/vnd.oasis.opendocument.text", - [".mp4"] = "video/mp4", [".m4v"] = "video/mp4", [".mov"] = "video/quicktime", [".webm"] = "video/webm", - [".mkv"] = "video/x-matroska", [".avi"] = "video/x-msvideo", [".wmv"] = "video/x-ms-wmv", - [".mp3"] = "audio/mpeg", [".wav"] = "audio/wav", [".ogg"] = "audio/ogg", - [".zip"] = "application/zip", [".gz"] = "application/gzip", [".tar"] = "application/x-tar", - [".js"] = "text/javascript", [".css"] = "text/css", [".bin"] = "application/octet-stream" + [".jpg"] = "image/jpeg", + [".jpeg"] = "image/jpeg", + [".png"] = "image/png", + [".gif"] = "image/gif", + [".webp"] = "image/webp", + [".bmp"] = "image/bmp", + [".svg"] = "image/svg+xml", + [".ico"] = "image/x-icon", + [".tif"] = "image/tiff", + [".tiff"] = "image/tiff", + [".avif"] = "image/avif", + [".heic"] = "image/heic", + [".pdf"] = "application/pdf", + [".txt"] = "text/plain", + [".md"] = "text/markdown", + [".csv"] = "text/csv", + [".json"] = "application/json", + [".xml"] = "application/xml", + [".html"] = "text/html", + [".htm"] = "text/html", + [".doc"] = "application/msword", + [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + [".xls"] = "application/vnd.ms-excel", + [".xlsx"] = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + [".ppt"] = "application/vnd.ms-powerpoint", + [".pptx"] = "application/vnd.openxmlformats-officedocument.presentationml.presentation", + [".rtf"] = "application/rtf", + [".odt"] = "application/vnd.oasis.opendocument.text", + [".mp4"] = "video/mp4", + [".m4v"] = "video/mp4", + [".mov"] = "video/quicktime", + [".webm"] = "video/webm", + [".mkv"] = "video/x-matroska", + [".avi"] = "video/x-msvideo", + [".wmv"] = "video/x-ms-wmv", + [".mp3"] = "audio/mpeg", + [".wav"] = "audio/wav", + [".ogg"] = "audio/ogg", + [".zip"] = "application/zip", + [".gz"] = "application/gzip", + [".tar"] = "application/x-tar", + [".js"] = "text/javascript", + [".css"] = "text/css", + [".bin"] = "application/octet-stream" }; private static readonly string[] ImageExtensions = ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg", "ico", "tif", "tiff", "avif", "heic"]; @@ -286,15 +316,15 @@ _ when key.StartsWith("metadata.", StringComparison.Ordinal) => key, var any = new BsonArray { new BsonDocument(field, new BsonRegularExpression(Regex.Escape(value), "i")) }; if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long integer)) { - any.Add(new BsonDocument(field, integer)); + _ = any.Add(new BsonDocument(field, integer)); } else if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double real)) { - any.Add(new BsonDocument(field, real)); + _ = any.Add(new BsonDocument(field, real)); } else if (bool.TryParse(value, out bool flag)) { - any.Add(new BsonDocument(field, flag)); + _ = any.Add(new BsonDocument(field, flag)); } return any.Count == 1 ? any[0].AsBsonDocument : new BsonDocument("$or", any); } @@ -407,16 +437,16 @@ public async Task ListAsync(string prefix, bool virtualDirs, Grid BsonDocument prefixFilter = GridFsPaths.PrefixFilter(prefix); if (prefixFilter.ElementCount > 0) { - clauses.Add(prefixFilter); + _ = clauses.Add(prefixFilter); } if (GridFsPaths.TypeFilter(type) is { } typeFilter) { - clauses.Add(typeFilter); + _ = clauses.Add(typeFilter); } BsonDocument? searchFilter = GridFsPaths.SearchFilter(search); if (searchFilter is not null) { - clauses.Add(searchFilter); + _ = clauses.Add(searchFilter); } BsonDocument match = clauses.Count switch { @@ -474,7 +504,7 @@ public async Task ListAsync(string prefix, bool virtualDirs, Grid var items = new List(rows.Count); foreach (BsonDocument row in rows.Take(ListLimit)) { - GridFsFile latest = GridFsFile.From(row["doc"].AsBsonDocument); + var latest = GridFsFile.From(row["doc"].AsBsonDocument); int versions = row["versions"].ToInt32(); if (tree && row["_id"].AsBsonDocument.TryGetValue("dir", out BsonValue dir)) { @@ -583,7 +613,7 @@ public async Task UploadAsync(string filename, Stream source, BsonDocu // 取消时最后那一批块的 insert 可能已经发到服务器、只是客户端不再等它 —— // 它会在 Abort 的 deleteMany 之后才落地。隔一小会儿按 files_id 再扫一遍,不留孤儿块。 await Task.Delay(300, CancellationToken.None).ConfigureAwait(false); - await Chunks.DeleteManyAsync(new BsonDocument("files_id", upload.Id), CancellationToken.None).ConfigureAwait(false); + _ = await Chunks.DeleteManyAsync(new BsonDocument("files_id", upload.Id), CancellationToken.None).ConfigureAwait(false); } catch (Exception abort) when (abort is MongoException or InvalidOperationException or TimeoutException) { @@ -662,7 +692,7 @@ public async Task DeleteManyAsync(IReadOnlyCollection ids, Canc { var inBatch = new BsonDocument("$in", new BsonArray(batch)); DeleteResult files = await Files.DeleteManyAsync(new BsonDocument("_id", inBatch), cancellationToken).ConfigureAwait(false); - await Chunks.DeleteManyAsync(new BsonDocument("files_id", inBatch), cancellationToken).ConfigureAwait(false); + _ = await Chunks.DeleteManyAsync(new BsonDocument("files_id", inBatch), cancellationToken).ConfigureAwait(false); deleted += files.DeletedCount; } return deleted; @@ -674,7 +704,7 @@ public async Task UpdateMetadataAsync(BsonValue id, BsonDocument? metadata, Canc BsonDocument update = metadata is null ? new BsonDocument("$unset", new BsonDocument("metadata", "")) : new BsonDocument("$set", new BsonDocument("metadata", metadata)); - await Files.UpdateOneAsync(new BsonDocument("_id", id), update, cancellationToken: cancellationToken).ConfigureAwait(false); + _ = await Files.UpdateOneAsync(new BsonDocument("_id", id), update, cancellationToken: cancellationToken).ConfigureAwait(false); } // ── 孤儿块 ───────────────────────────────────────────────────────────── @@ -689,7 +719,7 @@ public async Task UpdateMetadataAsync(BsonValue id, BsonDocument? metadata, Canc /// public async Task> FindOrphansAsync(CancellationToken cancellationToken = default) { - var pipeline = new[] + BsonDocument[] pipeline = new[] { new BsonDocument("$sort", new BsonDocument("files_id", 1)), new BsonDocument("$group", new BsonDocument("_id", "$files_id")), @@ -714,7 +744,7 @@ public async Task> FindOrphansAsync(CancellationToke var orphans = new List(ids.Count); foreach (BsonValue[] batch in ids.Chunk(1000)) { - var stats = new[] + BsonDocument[] stats = new[] { new BsonDocument("$match", new BsonDocument("files_id", new BsonDocument("$in", new BsonArray(batch)))), new BsonDocument("$group", new BsonDocument @@ -783,10 +813,10 @@ public static async Task CreateBucketAsync(MongoConnection connection, string da // NamespaceExists:集合已在(比如只建了一半的桶),补索引即可。 } } - await db.GetCollection(name + ".files").Indexes.CreateOneAsync(new CreateIndexModel( + _ = await db.GetCollection(name + ".files").Indexes.CreateOneAsync(new CreateIndexModel( new BsonDocument { { "filename", 1 }, { "uploadDate", 1 } }, new CreateIndexOptions { Name = "filename_1_uploadDate_1" }), cancellationToken: cancellationToken).ConfigureAwait(false); - await db.GetCollection(name + ".chunks").Indexes.CreateOneAsync(new CreateIndexModel( + _ = await db.GetCollection(name + ".chunks").Indexes.CreateOneAsync(new CreateIndexModel( new BsonDocument { { "files_id", 1 }, { "n", 1 } }, new CreateIndexOptions { Name = "files_id_1_n_1", Unique = true }), cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -840,21 +870,21 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation public override void Write(byte[] buffer, int offset, int count) { inner.Write(buffer, offset, count); - Count(count); + _ = Count(count); } /// public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { await inner.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).ConfigureAwait(false); - Count(count); + _ = Count(count); } /// public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) { await inner.WriteAsync(buffer, cancellationToken).ConfigureAwait(false); - Count(buffer.Length); + _ = Count(buffer.Length); } /// diff --git a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.Catalog.cs b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.Catalog.cs index 8a7044a..72a8346 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.Catalog.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.Catalog.cs @@ -89,7 +89,7 @@ public async Task GetStatsAsync(string database, string collect BsonDocument storage; try { - var pipeline = new[] + BsonDocument[] pipeline = new[] { new BsonDocument("$collStats", new BsonDocument("storageStats", new BsonDocument())) }; diff --git a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.cs b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.cs index 1820e39..fee6dc6 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnection.cs @@ -122,7 +122,7 @@ public static async Task ConnectAsync( /// 已连接的连接。 public static async Task ConnectAsync(string connectionString, CancellationToken cancellationToken) { - MongoClientSettings clientSettings = MongoClientSettings.FromConnectionString(connectionString); + var clientSettings = MongoClientSettings.FromConnectionString(connectionString); clientSettings.ServerSelectionTimeout = TimeSpan.FromSeconds(10); clientSettings.ApplicationName ??= "velashell"; var url = new MongoUrl(connectionString); @@ -151,64 +151,64 @@ internal static MongoClientSettings BuildSettings(WorkspaceConnectRequest reques switch (settings.Topology) { case MongoTopology.Uri: - { - // URI 写在"主机"那一栏里。凭据可以在 URI 里,也可以填在用户名/密码两栏 —— - // 后者优先:那两栏的密码是宿主加密落盘的,URI 里的密码是明文。 - var url = new MongoUrlBuilder(request.Host.Trim()); - if (request.Tunnel is not null) - { - // 走隧道时宿主把主机改写成了本地端点 —— 但 URI 模式下"主机"一栏是整条串, - // 宿主改写的是它解析出来的第一台。这里按宿主给的端点直连那一台。 - url.Server = new(request.Host, request.Port); - url.DirectConnection = true; - } - if (!string.IsNullOrEmpty(request.Username)) { - url.Username = request.Username; - url.Password = request.Password; + // URI 写在"主机"那一栏里。凭据可以在 URI 里,也可以填在用户名/密码两栏 —— + // 后者优先:那两栏的密码是宿主加密落盘的,URI 里的密码是明文。 + var url = new MongoUrlBuilder(request.Host.Trim()); + if (request.Tunnel is not null) + { + // 走隧道时宿主把主机改写成了本地端点 —— 但 URI 模式下"主机"一栏是整条串, + // 宿主改写的是它解析出来的第一台。这里按宿主给的端点直连那一台。 + url.Server = new(request.Host, request.Port); + url.DirectConnection = true; + } + if (!string.IsNullOrEmpty(request.Username)) + { + url.Username = request.Username; + url.Password = request.Password; + } + client = MongoClientSettings.FromUrl(url.ToMongoUrl()); + break; } - client = MongoClientSettings.FromUrl(url.ToMongoUrl()); - break; - } case MongoTopology.Srv: - { - var url = new MongoUrlBuilder - { - Scheme = ConnectionStringScheme.MongoDBPlusSrv, - Server = new(request.Host.Trim()) - }; - client = MongoClientSettings.FromUrl(url.ToMongoUrl()); - ApplyCredential(client, request, settings); - if (settings.ReplicaSet.Length > 0) { - client.ReplicaSetName = settings.ReplicaSet; + var url = new MongoUrlBuilder + { + Scheme = ConnectionStringScheme.MongoDBPlusSrv, + Server = new(request.Host.Trim()) + }; + client = MongoClientSettings.FromUrl(url.ToMongoUrl()); + ApplyCredential(client, request, settings); + if (settings.ReplicaSet.Length > 0) + { + client.ReplicaSetName = settings.ReplicaSet; + } + break; } - break; - } default: - { - client = new MongoClientSettings(); - var servers = new List { new(request.Host.Trim(), request.Port) }; - bool tunneled = request.Tunnel is not null; - if (!tunneled) { - foreach (string extra in settings.AdditionalHosts) + client = new MongoClientSettings(); + var servers = new List { new(request.Host.Trim(), request.Port) }; + bool tunneled = request.Tunnel is not null; + if (!tunneled) { - servers.Add(ParseAddress(extra)); + foreach (string extra in settings.AdditionalHosts) + { + servers.Add(ParseAddress(extra)); + } } + client.Servers = servers; + // 经隧道:本地转发只通到那一台,驱动按副本集配置去连其余成员的真实地址 —— + // 那些地址从本机根本够不着,结果是选服超时。所以隧道下强制直连。 + bool direct = tunneled || (settings.DirectConnection && servers.Count == 1); + client.DirectConnection = direct; + if (!direct && settings.ReplicaSet.Length > 0) + { + client.ReplicaSetName = settings.ReplicaSet; + } + ApplyCredential(client, request, settings); + break; } - client.Servers = servers; - // 经隧道:本地转发只通到那一台,驱动按副本集配置去连其余成员的真实地址 —— - // 那些地址从本机根本够不着,结果是选服超时。所以隧道下强制直连。 - bool direct = tunneled || (settings.DirectConnection && servers.Count == 1); - client.DirectConnection = direct; - if (!direct && settings.ReplicaSet.Length > 0) - { - client.ReplicaSetName = settings.ReplicaSet; - } - ApplyCredential(client, request, settings); - break; - } } client.ApplicationName = settings.AppName; @@ -222,7 +222,7 @@ internal static MongoClientSettings BuildSettings(WorkspaceConnectRequest reques if (tls) { client.UseTls = true; - var ssl = client.SslSettings?.Clone() ?? new SslSettings(); + SslSettings ssl = client.SslSettings?.Clone() ?? new SslSettings(); ssl.CheckCertificateRevocation = false; X509Certificate2Collection? caCertificates = LoadCaCertificates(settings.TlsCaFile); ssl.ServerCertificateValidationCallback = (_, certificate, chain, errors) => @@ -463,11 +463,11 @@ private async Task ProbePrivilegesAsync(CancellationToken canc if (resource.GetValue("anyResource", false).ToBoolean() || (resource.TryGetValue("db", out BsonValue db) && db.IsString && db.AsString.Length == 0)) { - writable.Add("*"); + _ = writable.Add("*"); } else if (resource.TryGetValue("db", out BsonValue named) && named.IsString) { - writable.Add(named.AsString); + _ = writable.Add(named.AsString); } } return new(user, roles, writable); @@ -484,7 +484,7 @@ private async Task ProbePrivilegesAsync(CancellationToken canc public async Task PingAsync(CancellationToken cancellationToken = default) { var watch = Stopwatch.StartNew(); - await Client.GetDatabase("admin").RunCommandAsync(new BsonDocument("ping", 1), cancellationToken: cancellationToken) + _ = await Client.GetDatabase("admin").RunCommandAsync(new BsonDocument("ping", 1), cancellationToken: cancellationToken) .ConfigureAwait(false); int ms = (int)Math.Max(1, watch.ElapsedMilliseconds); LatencyMs = ms; @@ -568,7 +568,7 @@ public async Task KillByCommentAsync(string comment, CancellationToken canc { if (op.AsBsonDocument.TryGetValue("opid", out BsonValue opid)) { - await RunCommandAsync("admin", new BsonDocument { { "killOp", 1 }, { "op", opid } }, cancellationToken) + _ = await RunCommandAsync("admin", new BsonDocument { { "killOp", 1 }, { "op", opid } }, cancellationToken) .ConfigureAwait(false); killed++; } diff --git a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnector.cs b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnector.cs index d5c17be..a4b6d18 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/MongoConnector.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/MongoConnector.cs @@ -79,7 +79,7 @@ public async Task ConnectAsync(MongoProfile profile, string password, { ArgumentNullException.ThrowIfNull(profile); WorkspaceConnectRequest request = profile.ToRequest(password); - MongoSettings settings = MongoSettings.From(request); + var settings = MongoSettings.From(request); IMongoTunnel? tunnel = null; if (UsesTunnel(settings)) { @@ -123,7 +123,7 @@ public Task ProbeAsync(MongoProfile profile, string password, IProg { ArgumentNullException.ThrowIfNull(profile); WorkspaceConnectRequest request = profile.ToRequest(password); - MongoSettings settings = MongoSettings.From(request); + var settings = MongoSettings.From(request); Func>? openTunnel = UsesTunnel(settings) ? ct => OpenTunnelAsync(profile, request, settings, ct) : null; diff --git a/plugins/VelaShell.Plugin.Mongo/Core/MongoProfile.cs b/plugins/VelaShell.Plugin.Mongo/Core/MongoProfile.cs index b76d1cf..49788ef 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/MongoProfile.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/MongoProfile.cs @@ -34,7 +34,7 @@ internal sealed class MongoProfile public string Username { get; set; } = ""; /// 其余设置(键见 )。 - public Dictionary Settings { get; set; } = new(StringComparer.Ordinal); + public Dictionary Settings { get; set; } = [with(StringComparer.Ordinal)]; /// 最近一次连上的时间(对象树按它排,常用的在上面)。 public DateTimeOffset? LastConnectedAt { get; set; } @@ -87,7 +87,7 @@ public void Set(string key, string? value) { if (string.IsNullOrEmpty(value)) { - Settings.Remove(key); + _ = Settings.Remove(key); } else { @@ -141,7 +141,7 @@ public async Task SaveAsync(MongoProfile profile, string? password, Cancellation } if (password.Length == 0) { - await context.Secrets.DeleteAsync(SecretName(profile.Id), cancellationToken).ConfigureAwait(false); + _ = await context.Secrets.DeleteAsync(SecretName(profile.Id), cancellationToken).ConfigureAwait(false); } else { @@ -160,7 +160,7 @@ public async Task DeleteAsync(string id, CancellationToken cancellationToken = d { await context.Storage.SetAsync(ProfilesKey, list, cancellationToken).ConfigureAwait(false); } - await context.Secrets.DeleteAsync(SecretName(id), cancellationToken).ConfigureAwait(false); + _ = await context.Secrets.DeleteAsync(SecretName(id), cancellationToken).ConfigureAwait(false); } /// 取口令;没存过是空串。 diff --git a/plugins/VelaShell.Plugin.Mongo/Core/SshTunnel.cs b/plugins/VelaShell.Plugin.Mongo/Core/SshTunnel.cs index 7c75ce5..69ed00c 100644 --- a/plugins/VelaShell.Plugin.Mongo/Core/SshTunnel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Core/SshTunnel.cs @@ -165,7 +165,7 @@ private async Task PumpAsync(int id, TcpClient client) NetworkStream local = client.GetStream(); Task up = local.CopyToAsync(remote, _lifetime.Token); Task down = remote.CopyToAsync(local, _lifetime.Token); - await Task.WhenAny(up, down).ConfigureAwait(false); + _ = await Task.WhenAny(up, down).ConfigureAwait(false); } catch (Exception ex) when (ex is not OutOfMemoryException) { @@ -177,7 +177,7 @@ private async Task PumpAsync(int id, TcpClient client) } finally { - _pumps.TryRemove(id, out _); + _ = _pumps.TryRemove(id, out _); client.Dispose(); if (remote is not null) { @@ -215,7 +215,7 @@ public async ValueTask DisposeAsync() private sealed class JumpLease : IAsyncDisposable { private static readonly Lock Gate = new(); - private static readonly Dictionary Open = new(StringComparer.Ordinal); + private static readonly Dictionary Open = [with(StringComparer.Ordinal)]; private readonly IPluginContext _context; private readonly string _savedSessionId; @@ -272,7 +272,7 @@ public async ValueTask DisposeAsync() { return; } - Open.Remove(_savedSessionId); + _ = Open.Remove(_savedSessionId); } try { diff --git a/plugins/VelaShell.Plugin.Mongo/Loc.cs b/plugins/VelaShell.Plugin.Mongo/Loc.cs index e591bf9..c23750b 100644 --- a/plugins/VelaShell.Plugin.Mongo/Loc.cs +++ b/plugins/VelaShell.Plugin.Mongo/Loc.cs @@ -18,16 +18,14 @@ namespace VelaShell.Plugin.Mongo; /// 宿主当前语言(如 zh-Hans、en)。 public sealed partial class Loc(string locale) { - private readonly bool _chinese = locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase); - /// 当前是不是中文表(少数地方要按语言挑数字或单位的写法)。 - public bool IsChinese => _chinese; + public bool IsChinese { get; } = locale.StartsWith("zh", StringComparison.OrdinalIgnoreCase); /// 取一条文案;未收录的键原样返回(方便一眼看出漏了哪条)。 /// 文案键。 /// 文案。 public string this[string key] => - (_chinese ? Tables.Chinese : Tables.English).TryGetValue(key, out string? value) ? value : key; + (IsChinese ? Tables.Chinese : Tables.English).TryGetValue(key, out string? value) ? value : key; /// 取一条文案(索引器的具名形式,便于在表达式里连用)。 /// 文案键。 diff --git a/plugins/VelaShell.Plugin.Mongo/MongoPlugin.cs b/plugins/VelaShell.Plugin.Mongo/MongoPlugin.cs index 7c0e0b6..bd37ca5 100644 --- a/plugins/VelaShell.Plugin.Mongo/MongoPlugin.cs +++ b/plugins/VelaShell.Plugin.Mongo/MongoPlugin.cs @@ -77,9 +77,7 @@ private static async Task WithinAsync(Task work, TimeSpan budget, CancellationTo { await work.WaitAsync(budget, cancellationToken).ConfigureAwait(false); } -#pragma warning disable CA1031 // 停用路径:任何一步的失败都不该让其余收尾半途而废。 catch (Exception) -#pragma warning restore CA1031 { } } diff --git a/plugins/VelaShell.Plugin.Mongo/MongoSessionRegistry.cs b/plugins/VelaShell.Plugin.Mongo/MongoSessionRegistry.cs index ab7a21d..16b2d1c 100644 --- a/plugins/VelaShell.Plugin.Mongo/MongoSessionRegistry.cs +++ b/plugins/VelaShell.Plugin.Mongo/MongoSessionRegistry.cs @@ -54,7 +54,7 @@ private void Remove(MongoSessionEntry entry) { lock (_gate) { - _entries.Remove(entry); + _ = _entries.Remove(entry); } Changed?.Invoke(); } diff --git a/plugins/VelaShell.Plugin.Mongo/README.md b/plugins/VelaShell.Plugin.Mongo/README.md index a3d6b16..3e7d76b 100644 --- a/plugins/VelaShell.Plugin.Mongo/README.md +++ b/plugins/VelaShell.Plugin.Mongo/README.md @@ -72,7 +72,10 @@ Navicat 式的对象树与大图标工具栏,补齐 Compass 的聚合管道、Sc 编辑框失焦即提交(焦点进了类型菜单、日历这些弹出层不算),解析不了的留着红框; 选中行只在 `ListBox.flat:focus-within` 时着色(`CollectionStyles`)。 - **日期的内联编辑**:`InlineValueEditor.IsDate` 时编辑框右边是日历按钮,弹 `DatePickFlyout`(月历 + 时刻框 +「现在」/「应用」); - 日期不再给"现在"那一条文字候选。 + 日期不再给"现在"那一条文字候选。设计稿没有月历这一块,外观按令牌自定:`Calendar` / `CalendarItem` / + `CalendarDayButton` / `CalendarButton` 四个**按类型作键**的主题(MongoStyles),一格 30×26、行距 28, + 今天 = `VelaAccent` 描边、选中 = `VelaAccentDim` 底;时刻框与两颗按钮是 `MongoTextBox` / `MongoOutlineButton` / `MongoPillButton`。 + 文档编辑器里的日历按钮弹的也是同一套(插件里任何 `new Calendar()` 都自动用它)。 - **筛选框的 Enter**:补全弹层开着时是「接受补全」,关着才是「查找」。筛选框在编辑区上另挂了隧道处理器, 而同一元素上的隧道处理器按**注册的逆序**调用 —— 它比 `CodeEditor` 自己的先拿到 Enter,所以要先看 `CodeEditor.IsCompletionOpen`。 - **新建索引**(设计稿 07b,`DesignIndexesView` + `DesignTabViewModel.NewIndex.cs`):Navicat 设计表的做法 —— 点「新建索引」后索引页下半部分 diff --git a/plugins/VelaShell.Plugin.Mongo/Shell/CodeExport.cs b/plugins/VelaShell.Plugin.Mongo/Shell/CodeExport.cs index 22111eb..89a1c38 100644 --- a/plugins/VelaShell.Plugin.Mongo/Shell/CodeExport.cs +++ b/plugins/VelaShell.Plugin.Mongo/Shell/CodeExport.cs @@ -114,23 +114,23 @@ private static Op Describe(ShellCommand command, string database) switch (method.Name) { case "find" or "findOne": - { - Core.FindRequest request = ShellExecutor.BuildFindRequest(command, database, 0); - return new Op { - Name = method.Name, - Database = database, - Collection = command.Collection, - Filter = request.Filter, - Projection = request.Projection, - Sort = request.Sort, - Skip = request.Skip, - Limit = method.Name == "findOne" ? 0 : request.Limit, - Hint = request.Hint, - MaxTimeMs = request.MaxTimeMs, - Count = command.Modifier("count") is not null - }; - } + Core.FindRequest request = ShellExecutor.BuildFindRequest(command, database, 0); + return new Op + { + Name = method.Name, + Database = database, + Collection = command.Collection, + Filter = request.Filter, + Projection = request.Projection, + Sort = request.Sort, + Skip = request.Skip, + Limit = method.Name == "findOne" ? 0 : request.Limit, + Hint = request.Hint, + MaxTimeMs = request.MaxTimeMs, + Count = command.Modifier("count") is not null + }; + } case "aggregate": return new Op { @@ -179,7 +179,7 @@ private static Op Describe(ShellCommand command, string database) private static string Mongosh(ShellCommand command, Op op) { var b = new StringBuilder(); - b.Append("use(").Append(BsonText.Quote(op.Database)).Append(");\n\n"); + _ = b.Append("use(").Append(BsonText.Quote(op.Database)).Append(");\n\n"); switch (command.Kind) { case ShellCommandKind.Use: @@ -187,16 +187,16 @@ private static string Mongosh(ShellCommand command, Op op) case ShellCommandKind.Show: return $"show {command.Target}"; } - b.Append("db"); + _ = b.Append("db"); if (command.Collection is { } collection) { - b.Append(System.Text.RegularExpressions.Regex.IsMatch(collection, "^[A-Za-z_$][A-Za-z0-9_$]*$") + _ = b.Append(System.Text.RegularExpressions.Regex.IsMatch(collection, "^[A-Za-z_$][A-Za-z0-9_$]*$") ? "." + collection : $".getCollection({BsonText.Quote(collection)})"); } if (command.Explain is { } verbosity) { - b.Append(".explain(").Append(BsonText.Quote(verbosity)).Append(')'); + _ = b.Append(".explain(").Append(BsonText.Quote(verbosity)).Append(')'); } if (command.Method is { } method) { @@ -206,27 +206,27 @@ private static string Mongosh(ShellCommand command, Op op) { AppendCall(b, call, multiline: false); } - b.Append(';'); + _ = b.Append(';'); return b.ToString(); } private static void AppendCall(StringBuilder b, ShellCall call, bool multiline) { - b.Append('.').Append(call.Name).Append('('); + _ = b.Append('.').Append(call.Name).Append('('); if (multiline) { - b.Append('\n'); + _ = b.Append('\n'); for (int i = 0; i < call.Arguments.Count; i++) { - b.Append(" ").Append(Indent(BsonText.Pretty(call.Arguments[i].Value), " ")); - b.Append(i < call.Arguments.Count - 1 ? ",\n" : "\n"); + _ = b.Append(" ").Append(Indent(BsonText.Pretty(call.Arguments[i].Value), " ")); + _ = b.Append(i < call.Arguments.Count - 1 ? ",\n" : "\n"); } } else { - b.Append(string.Join(", ", call.Arguments.Select(static a => BsonText.Literal(a.Value)))); + _ = b.Append(string.Join(", ", call.Arguments.Select(static a => BsonText.Literal(a.Value)))); } - b.Append(')'); + _ = b.Append(')'); } private static string Indent(string text, string indent) => text.Replace("\n", "\n" + indent, StringComparison.Ordinal); @@ -241,17 +241,17 @@ private abstract class Writer(Op op, string connectionString) protected readonly StringBuilder Body = new(); - protected readonly SortedSet Imports = new(StringComparer.Ordinal); + protected readonly SortedSet Imports = [with(StringComparer.Ordinal)]; public abstract string Write(); protected static string Escape(string text) { var b = new StringBuilder(text.Length + 2); - b.Append('"'); + _ = b.Append('"'); foreach (char c in text) { - b.Append(c switch + _ = b.Append(c switch { '"' => "\\\"", '\\' => "\\\\", @@ -261,7 +261,7 @@ protected static string Escape(string text) _ => c < ' ' ? $"\\u{(int)c:x4}" : c.ToString() }); } - b.Append('"'); + _ = b.Append('"'); return b.ToString(); } @@ -281,21 +281,21 @@ private sealed class CSharpWriter(Op op, string connectionString) : Writer(op, c { public override string Write() { - Imports.Add("MongoDB.Bson"); - Imports.Add("MongoDB.Driver"); + _ = Imports.Add("MongoDB.Bson"); + _ = Imports.Add("MongoDB.Driver"); var head = new StringBuilder(); - head.Append("var client = new MongoClient(").Append(Escape(ConnectionString)).Append(");\n"); - head.Append("var database = client.GetDatabase(").Append(Escape(Op.Database)).Append(");\n"); + _ = head.Append("var client = new MongoClient(").Append(Escape(ConnectionString)).Append(");\n"); + _ = head.Append("var database = client.GetDatabase(").Append(Escape(Op.Database)).Append(");\n"); if (Op.Collection is { } c) { - head.Append("var collection = database.GetCollection(").Append(Escape(c)).Append(");\n"); + _ = head.Append("var collection = database.GetCollection(").Append(Escape(c)).Append(");\n"); } - head.Append('\n'); + _ = head.Append('\n'); Statement(); var output = new StringBuilder(); foreach (string import in Imports) { - output.Append("using ").Append(import).Append(";\n"); + _ = output.Append("using ").Append(import).Append(";\n"); } return output.Append('\n').Append(head).Append(Body).ToString().TrimEnd() + "\n"; } @@ -307,130 +307,130 @@ private void Statement() switch (Op.Name) { case "find" or "findOne": - { - Var("filter", Op.Filter ?? []); - if (Op.Count) { - Body.Append("\nvar count = await collection.CountDocumentsAsync(filter);\n"); + Var("filter", Op.Filter ?? []); + if (Op.Count) + { + _ = Body.Append("\nvar count = await collection.CountDocumentsAsync(filter);\n"); + return; + } + if (Op.Projection is { } p) + { + Var("projection", p); + } + if (Op.Sort is { } s) + { + Var("sort", s); + } + _ = Body.Append('\n'); + var options = new List(); + if (Op.Hint is { } hint) + { + options.Add("Hint = " + Lit(hint, 1)); + } + if (Op.MaxTimeMs > 0) + { + options.Add($"MaxTime = TimeSpan.FromMilliseconds({Op.MaxTimeMs})"); + } + _ = Body.Append(Op.Name == "findOne" ? "var document = await collection.Find(filter" : "var documents = await collection.Find(filter"); + if (options.Count > 0) + { + _ = Body.Append(", new FindOptions { ").Append(string.Join(", ", options)).Append(" }"); + } + _ = Body.Append(')'); + if (Op.Projection is not null) + { + _ = Body.Append("\n .Project(projection)"); + } + if (Op.Sort is not null) + { + _ = Body.Append("\n .Sort(sort)"); + } + if (Op.Skip > 0) + { + _ = Body.Append("\n .Skip(").Append(Op.Skip).Append(')'); + } + if (Op.Limit > 0) + { + _ = Body.Append("\n .Limit(").Append(Op.Limit).Append(')'); + } + _ = Body.Append(Op.Name == "findOne" ? "\n .FirstOrDefaultAsync();\n" : "\n .ToListAsync();\n"); return; } - if (Op.Projection is { } p) - { - Var("projection", p); - } - if (Op.Sort is { } s) - { - Var("sort", s); - } - Body.Append('\n'); - var options = new List(); - if (Op.Hint is { } hint) - { - options.Add("Hint = " + Lit(hint, 1)); - } - if (Op.MaxTimeMs > 0) - { - options.Add($"MaxTime = TimeSpan.FromMilliseconds({Op.MaxTimeMs})"); - } - Body.Append(Op.Name == "findOne" ? "var document = await collection.Find(filter" : "var documents = await collection.Find(filter"); - if (options.Count > 0) - { - Body.Append(", new FindOptions { ").Append(string.Join(", ", options)).Append(" }"); - } - Body.Append(')'); - if (Op.Projection is not null) - { - Body.Append("\n .Project(projection)"); - } - if (Op.Sort is not null) - { - Body.Append("\n .Sort(sort)"); - } - if (Op.Skip > 0) - { - Body.Append("\n .Skip(").Append(Op.Skip).Append(')'); - } - if (Op.Limit > 0) - { - Body.Append("\n .Limit(").Append(Op.Limit).Append(')'); - } - Body.Append(Op.Name == "findOne" ? "\n .FirstOrDefaultAsync();\n" : "\n .ToListAsync();\n"); - return; - } case "aggregate": - Body.Append("var pipeline = new[]\n{\n"); + _ = Body.Append("var pipeline = new[]\n{\n"); for (int i = 0; i < Op.Pipeline!.Count; i++) { - Body.Append(Pad(1)).Append(Lit(Op.Pipeline[i], 1)).Append(i < Op.Pipeline.Count - 1 ? ",\n" : "\n"); + _ = Body.Append(Pad(1)).Append(Lit(Op.Pipeline[i], 1)).Append(i < Op.Pipeline.Count - 1 ? ",\n" : "\n"); } - Body.Append("};\n\nvar documents = await collection.Aggregate(pipeline).ToListAsync();\n"); + _ = Body.Append("};\n\nvar documents = await collection.Aggregate(pipeline).ToListAsync();\n"); return; case "countDocuments": Var("filter", Op.Filter!); - Body.Append("\nvar count = await collection.CountDocumentsAsync(filter);\n"); + _ = Body.Append("\nvar count = await collection.CountDocumentsAsync(filter);\n"); return; case "estimatedDocumentCount": - Body.Append("var count = await collection.EstimatedDocumentCountAsync();\n"); + _ = Body.Append("var count = await collection.EstimatedDocumentCountAsync();\n"); return; case "distinct": Var("filter", Op.Filter!); - Body.Append("\nvar values = await (await collection.DistinctAsync(").Append(Escape(Op.Field ?? "")).Append(", filter)).ToListAsync();\n"); + _ = Body.Append("\nvar values = await (await collection.DistinctAsync(").Append(Escape(Op.Field ?? "")).Append(", filter)).ToListAsync();\n"); return; case "insertOne": Var("document", Op.Document ?? new BsonDocument()); - Body.Append("\nawait collection.InsertOneAsync(document);\n"); + _ = Body.Append("\nawait collection.InsertOneAsync(document);\n"); return; case "insertMany": - Body.Append("var documents = new[]\n{\n"); + _ = Body.Append("var documents = new[]\n{\n"); BsonArray docs = Op.Document as BsonArray ?? []; for (int i = 0; i < docs.Count; i++) { - Body.Append(Pad(1)).Append(Lit(docs[i], 1)).Append(i < docs.Count - 1 ? ",\n" : "\n"); + _ = Body.Append(Pad(1)).Append(Lit(docs[i], 1)).Append(i < docs.Count - 1 ? ",\n" : "\n"); } - Body.Append("};\n\nawait collection.InsertManyAsync(documents);\n"); + _ = Body.Append("};\n\nawait collection.InsertManyAsync(documents);\n"); return; case "updateOne" or "updateMany" or "findOneAndUpdate": - { - Var("filter", Op.Filter!); - if (Op.Update is BsonArray stages) { - Body.Append("var update = Builders.Update.Pipeline(new[]\n{\n"); - for (int i = 0; i < stages.Count; i++) + Var("filter", Op.Filter!); + if (Op.Update is BsonArray stages) { - Body.Append(Pad(1)).Append(Lit(stages[i], 1)).Append(i < stages.Count - 1 ? ",\n" : "\n"); + _ = Body.Append("var update = Builders.Update.Pipeline(new[]\n{\n"); + for (int i = 0; i < stages.Count; i++) + { + _ = Body.Append(Pad(1)).Append(Lit(stages[i], 1)).Append(i < stages.Count - 1 ? ",\n" : "\n"); + } + _ = Body.Append("});\n"); } - Body.Append("});\n"); - } - else - { - Var("update", Op.Update ?? new BsonDocument()); - } - Body.Append('\n'); - if (Op.Name == "findOneAndUpdate") - { - Body.Append("var document = await collection.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions\n{\n") - .Append(Pad(1)).Append("ReturnDocument = ReturnDocument.").Append(Op.ReturnNew ? "After" : "Before").Append(",\n") - .Append(Pad(1)).Append("IsUpsert = ").Append(Op.Upsert ? "true" : "false").Append("\n});\n"); + else + { + Var("update", Op.Update ?? new BsonDocument()); + } + _ = Body.Append('\n'); + if (Op.Name == "findOneAndUpdate") + { + _ = Body.Append("var document = await collection.FindOneAndUpdateAsync(filter, update, new FindOneAndUpdateOptions\n{\n") + .Append(Pad(1)).Append("ReturnDocument = ReturnDocument.").Append(Op.ReturnNew ? "After" : "Before").Append(",\n") + .Append(Pad(1)).Append("IsUpsert = ").Append(Op.Upsert ? "true" : "false").Append("\n});\n"); + return; + } + _ = Body.Append("var result = await collection.").Append(Op.Name == "updateOne" ? "UpdateOneAsync" : "UpdateManyAsync").Append("(filter, update"); + if (Op.Upsert) + { + _ = Body.Append(", new UpdateOptions { IsUpsert = true }"); + } + _ = Body.Append(");\n"); return; } - Body.Append("var result = await collection.").Append(Op.Name == "updateOne" ? "UpdateOneAsync" : "UpdateManyAsync").Append("(filter, update"); - if (Op.Upsert) - { - Body.Append(", new UpdateOptions { IsUpsert = true }"); - } - Body.Append(");\n"); - return; - } case "replaceOne" or "findOneAndReplace": Var("filter", Op.Filter!); Var("replacement", Op.Update ?? new BsonDocument()); - Body.Append('\n').Append(Op.Name == "replaceOne" + _ = Body.Append('\n').Append(Op.Name == "replaceOne" ? $"var result = await collection.ReplaceOneAsync(filter, replacement{(Op.Upsert ? ", new ReplaceOptions { IsUpsert = true }" : "")});\n" : "var document = await collection.FindOneAndReplaceAsync(filter, replacement);\n"); return; case "deleteOne" or "deleteMany" or "findOneAndDelete": Var("filter", Op.Filter!); - Body.Append('\n').Append(Op.Name switch + _ = Body.Append('\n').Append(Op.Name switch { "deleteOne" => "var result = await collection.DeleteOneAsync(filter);\n", "deleteMany" => "var result = await collection.DeleteManyAsync(filter);\n", @@ -439,10 +439,10 @@ private void Statement() return; case "createIndex": Var("keys", Op.Document ?? new BsonDocument()); - Body.Append("\nvar name = await collection.Indexes.CreateOneAsync(new CreateIndexModel(keys"); + _ = Body.Append("\nvar name = await collection.Indexes.CreateOneAsync(new CreateIndexModel(keys"); if (Op.Command is { ElementCount: > 0 } indexOptions) { - Body.Append(", new CreateIndexOptions { ").Append(string.Join(", ", indexOptions.Elements.Select(static e => e.Name switch + _ = Body.Append(", new CreateIndexOptions { ").Append(string.Join(", ", indexOptions.Elements.Select(static e => e.Name switch { "unique" => $"Unique = {(e.Value.ToBoolean() ? "true" : "false")}", "sparse" => $"Sparse = {(e.Value.ToBoolean() ? "true" : "false")}", @@ -451,27 +451,27 @@ private void Statement() _ => $"/* {e.Name} */" }))).Append(" }"); } - Body.Append("));\n"); + _ = Body.Append("));\n"); return; case "dropIndex": - Body.Append("await collection.Indexes.DropOneAsync(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); + _ = Body.Append("await collection.Indexes.DropOneAsync(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); return; case "getIndexes": - Body.Append("var indexes = await (await collection.Indexes.ListAsync()).ToListAsync();\n"); + _ = Body.Append("var indexes = await (await collection.Indexes.ListAsync()).ToListAsync();\n"); return; case "drop": - Body.Append("await database.DropCollectionAsync(").Append(Escape(Op.Collection ?? "")).Append(");\n"); + _ = Body.Append("await database.DropCollectionAsync(").Append(Escape(Op.Collection ?? "")).Append(");\n"); return; case "stats": - Body.Append("var stats = await database.RunCommandAsync(new BsonDocument(\"collStats\", ") + _ = Body.Append("var stats = await database.RunCommandAsync(new BsonDocument(\"collStats\", ") .Append(Escape(Op.Collection ?? "")).Append("));\n"); return; case "runCommand": Var("command", Op.Command ?? []); - Body.Append("\nvar reply = await database.RunCommandAsync(command);\n"); + _ = Body.Append("\nvar reply = await database.RunCommandAsync(command);\n"); return; default: - Body.Append("// This statement has no driver equivalent here.\n"); + _ = Body.Append("// This statement has no driver equivalent here.\n"); return; } } @@ -481,43 +481,43 @@ private string Lit(BsonValue value, int level) switch (value.BsonType) { case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - if (doc.ElementCount == 0) { - return "new BsonDocument()"; - } - if (doc.ElementCount == 1 && !IsContainer(doc[0])) - { - return $"new BsonDocument({Escape(doc.GetElement(0).Name)}, {Lit(doc[0], level)})"; - } - var b = new StringBuilder("new BsonDocument\n").Append(Pad(level)).Append("{\n"); - for (int i = 0; i < doc.ElementCount; i++) - { - BsonElement e = doc.GetElement(i); - b.Append(Pad(level + 1)).Append("{ ").Append(Escape(e.Name)).Append(", ").Append(Lit(e.Value, level + 1)).Append(" }") - .Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + BsonDocument doc = value.AsBsonDocument; + if (doc.ElementCount == 0) + { + return "new BsonDocument()"; + } + if (doc.ElementCount == 1 && !IsContainer(doc[0])) + { + return $"new BsonDocument({Escape(doc.GetElement(0).Name)}, {Lit(doc[0], level)})"; + } + StringBuilder b = new StringBuilder("new BsonDocument\n").Append(Pad(level)).Append("{\n"); + for (int i = 0; i < doc.ElementCount; i++) + { + BsonElement e = doc.GetElement(i); + _ = b.Append(Pad(level + 1)).Append("{ ").Append(Escape(e.Name)).Append(", ").Append(Lit(e.Value, level + 1)).Append(" }") + .Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append('}').ToString(); } - return b.Append(Pad(level)).Append('}').ToString(); - } case BsonType.Array: - { - BsonArray array = value.AsBsonArray; - if (array.Count == 0) - { - return "new BsonArray()"; - } - if (!array.Any(IsContainer)) { - return "new BsonArray { " + string.Join(", ", array.Select(v => Lit(v, level))) + " }"; - } - var b = new StringBuilder("new BsonArray\n").Append(Pad(level)).Append("{\n"); - for (int i = 0; i < array.Count; i++) - { - b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + BsonArray array = value.AsBsonArray; + if (array.Count == 0) + { + return "new BsonArray()"; + } + if (!array.Any(IsContainer)) + { + return "new BsonArray { " + string.Join(", ", array.Select(v => Lit(v, level))) + " }"; + } + StringBuilder b = new StringBuilder("new BsonArray\n").Append(Pad(level)).Append("{\n"); + for (int i = 0; i < array.Count; i++) + { + _ = b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append('}').ToString(); } - return b.Append(Pad(level)).Append('}').ToString(); - } case BsonType.String: return Escape(value.AsString); case BsonType.Int32: @@ -535,12 +535,12 @@ private string Lit(BsonValue value, int level) case BsonType.ObjectId: return $"new ObjectId({Escape(value.AsObjectId.ToString())})"; case BsonType.DateTime: - { - DateTime at = Utc(value); - return at.Millisecond == 0 - ? $"new DateTime({at.Year}, {at.Month}, {at.Day}, {at.Hour}, {at.Minute}, {at.Second}, DateTimeKind.Utc)" - : $"new DateTime({at.Year}, {at.Month}, {at.Day}, {at.Hour}, {at.Minute}, {at.Second}, {at.Millisecond}, DateTimeKind.Utc)"; - } + { + DateTime at = Utc(value); + return at.Millisecond == 0 + ? $"new DateTime({at.Year}, {at.Month}, {at.Day}, {at.Hour}, {at.Minute}, {at.Second}, DateTimeKind.Utc)" + : $"new DateTime({at.Year}, {at.Month}, {at.Day}, {at.Hour}, {at.Minute}, {at.Second}, {at.Millisecond}, DateTimeKind.Utc)"; + } case BsonType.RegularExpression: return $"new BsonRegularExpression({Escape(value.AsBsonRegularExpression.Pattern)}, {Escape(value.AsBsonRegularExpression.Options)})"; case BsonType.Binary when value.AsBsonBinaryData.SubType is BsonBinarySubType.UuidStandard: @@ -559,18 +559,18 @@ private sealed class PythonWriter(Op op, string connectionString) : Writer(op, c { public override string Write() { - Imports.Add("from pymongo import MongoClient"); + _ = Imports.Add("from pymongo import MongoClient"); Statement(); var head = new StringBuilder(); foreach (string import in Imports) { - head.Append(import).Append('\n'); + _ = head.Append(import).Append('\n'); } - head.Append("\nclient = MongoClient(").Append(Escape(ConnectionString)).Append(")\n"); - head.Append("db = client[").Append(Escape(Op.Database)).Append("]\n"); + _ = head.Append("\nclient = MongoClient(").Append(Escape(ConnectionString)).Append(")\n"); + _ = head.Append("db = client[").Append(Escape(Op.Database)).Append("]\n"); if (Op.Collection is { } c) { - head.Append("collection = db[").Append(Escape(c)).Append("]\n"); + _ = head.Append("collection = db[").Append(Escape(c)).Append("]\n"); } return head.Append('\n').Append(Body).ToString().TrimEnd() + "\n"; } @@ -582,90 +582,90 @@ private void Statement() switch (Op.Name) { case "find" or "findOne": - { - Var("filter", Op.Filter ?? []); - if (Op.Count) - { - Body.Append("\ncount = collection.count_documents(filter)\n"); - return; - } - if (Op.Projection is { } p) - { - Var("projection", p); - } - Body.Append('\n'); - string args = Op.Projection is null ? "filter" : "filter, projection"; - if (Op.Name == "findOne") { - Body.Append("document = collection.find_one(").Append(args).Append(")\n"); + Var("filter", Op.Filter ?? []); + if (Op.Count) + { + _ = Body.Append("\ncount = collection.count_documents(filter)\n"); + return; + } + if (Op.Projection is { } p) + { + Var("projection", p); + } + _ = Body.Append('\n'); + string args = Op.Projection is null ? "filter" : "filter, projection"; + if (Op.Name == "findOne") + { + _ = Body.Append("document = collection.find_one(").Append(args).Append(")\n"); + return; + } + _ = Body.Append("cursor = collection.find(").Append(args).Append(')'); + if (Op.Sort is { } s) + { + _ = Body.Append(".sort([").Append(string.Join(", ", s.Elements.Select(e => $"({Escape(e.Name)}, {Lit(e.Value, 0)})"))).Append("])"); + } + if (Op.Skip > 0) + { + _ = Body.Append(".skip(").Append(Op.Skip).Append(')'); + } + if (Op.Limit > 0) + { + _ = Body.Append(".limit(").Append(Op.Limit).Append(')'); + } + if (Op.Hint is { } hint) + { + _ = Body.Append(".hint(").Append(hint is BsonDocument h + ? "[" + string.Join(", ", h.Elements.Select(e => $"({Escape(e.Name)}, {Lit(e.Value, 0)})")) + "]" + : Lit(hint, 0)).Append(')'); + } + if (Op.MaxTimeMs > 0) + { + _ = Body.Append(".max_time_ms(").Append(Op.MaxTimeMs).Append(')'); + } + _ = Body.Append("\ndocuments = list(cursor)\n"); return; } - Body.Append("cursor = collection.find(").Append(args).Append(')'); - if (Op.Sort is { } s) - { - Body.Append(".sort([").Append(string.Join(", ", s.Elements.Select(e => $"({Escape(e.Name)}, {Lit(e.Value, 0)})"))).Append("])"); - } - if (Op.Skip > 0) - { - Body.Append(".skip(").Append(Op.Skip).Append(')'); - } - if (Op.Limit > 0) - { - Body.Append(".limit(").Append(Op.Limit).Append(')'); - } - if (Op.Hint is { } hint) - { - Body.Append(".hint(").Append(hint is BsonDocument h - ? "[" + string.Join(", ", h.Elements.Select(e => $"({Escape(e.Name)}, {Lit(e.Value, 0)})")) + "]" - : Lit(hint, 0)).Append(')'); - } - if (Op.MaxTimeMs > 0) - { - Body.Append(".max_time_ms(").Append(Op.MaxTimeMs).Append(')'); - } - Body.Append("\ndocuments = list(cursor)\n"); - return; - } case "aggregate": Var("pipeline", Op.Pipeline!); - Body.Append("\ndocuments = list(collection.aggregate(pipeline))\n"); + _ = Body.Append("\ndocuments = list(collection.aggregate(pipeline))\n"); return; case "countDocuments": Var("filter", Op.Filter!); - Body.Append("\ncount = collection.count_documents(filter)\n"); + _ = Body.Append("\ncount = collection.count_documents(filter)\n"); return; case "estimatedDocumentCount": - Body.Append("count = collection.estimated_document_count()\n"); + _ = Body.Append("count = collection.estimated_document_count()\n"); return; case "distinct": Var("filter", Op.Filter!); - Body.Append("\nvalues = collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", filter)\n"); + _ = Body.Append("\nvalues = collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", filter)\n"); return; case "insertOne": Var("document", Op.Document ?? new BsonDocument()); - Body.Append("\nresult = collection.insert_one(document)\n"); + _ = Body.Append("\nresult = collection.insert_one(document)\n"); return; case "insertMany": Var("documents", Op.Document ?? new BsonArray()); - Body.Append("\nresult = collection.insert_many(documents)\n"); + _ = Body.Append("\nresult = collection.insert_many(documents)\n"); return; case "updateOne" or "updateMany" or "replaceOne": Var("filter", Op.Filter!); Var(Op.Name == "replaceOne" ? "replacement" : "update", Op.Update ?? new BsonDocument()); - Body.Append("\nresult = collection.").Append(Op.Name switch { "updateOne" => "update_one", "updateMany" => "update_many", _ => "replace_one" }) + _ = Body.Append("\nresult = collection.").Append(Op.Name switch { "updateOne" => "update_one", "updateMany" => "update_many", _ => "replace_one" }) .Append("(filter, ").Append(Op.Name == "replaceOne" ? "replacement" : "update").Append(Op.Upsert ? ", upsert=True" : "").Append(")\n"); return; case "findOneAndUpdate" or "findOneAndReplace": - Imports.Add("from pymongo import ReturnDocument"); + _ = Imports.Add("from pymongo import ReturnDocument"); Var("filter", Op.Filter!); Var("update", Op.Update ?? new BsonDocument()); - Body.Append("\ndocument = collection.").Append(Op.Name == "findOneAndUpdate" ? "find_one_and_update" : "find_one_and_replace") + _ = Body.Append("\ndocument = collection.").Append(Op.Name == "findOneAndUpdate" ? "find_one_and_update" : "find_one_and_replace") .Append("(filter, update, return_document=ReturnDocument.").Append(Op.ReturnNew ? "AFTER" : "BEFORE") .Append(Op.Upsert ? ", upsert=True" : "").Append(")\n"); return; case "deleteOne" or "deleteMany" or "findOneAndDelete": Var("filter", Op.Filter!); - Body.Append("\n").Append(Op.Name switch + _ = Body.Append("\n").Append(Op.Name switch { "deleteOne" => "result = collection.delete_one(filter)\n", "deleteMany" => "result = collection.delete_many(filter)\n", @@ -673,33 +673,33 @@ private void Statement() }); return; case "createIndex": - Body.Append("name = collection.create_index([") + _ = Body.Append("name = collection.create_index([") .Append(string.Join(", ", (Op.Document as BsonDocument ?? []).Elements.Select(e => $"({Escape(e.Name)}, {Lit(e.Value, 0)})"))) .Append(']'); foreach (BsonElement e in Op.Command ?? []) { - Body.Append(", ").Append(e.Name switch { "expireAfterSeconds" => "expireAfterSeconds", _ => e.Name }).Append('=').Append(Lit(e.Value, 0)); + _ = Body.Append(", ").Append(e.Name switch { "expireAfterSeconds" => "expireAfterSeconds", _ => e.Name }).Append('=').Append(Lit(e.Value, 0)); } - Body.Append(")\n"); + _ = Body.Append(")\n"); return; case "dropIndex": - Body.Append("collection.drop_index(").Append(Escape(Op.Index?.ToString() ?? "")).Append(")\n"); + _ = Body.Append("collection.drop_index(").Append(Escape(Op.Index?.ToString() ?? "")).Append(")\n"); return; case "getIndexes": - Body.Append("indexes = list(collection.list_indexes())\n"); + _ = Body.Append("indexes = list(collection.list_indexes())\n"); return; case "drop": - Body.Append("collection.drop()\n"); + _ = Body.Append("collection.drop()\n"); return; case "stats": - Body.Append("stats = db.command(\"collStats\", ").Append(Escape(Op.Collection ?? "")).Append(")\n"); + _ = Body.Append("stats = db.command(\"collStats\", ").Append(Escape(Op.Collection ?? "")).Append(")\n"); return; case "runCommand": Var("command", Op.Command ?? []); - Body.Append("\nreply = db.command(command)\n"); + _ = Body.Append("\nreply = db.command(command)\n"); return; default: - Body.Append("# This statement has no driver equivalent here.\n"); + _ = Body.Append("# This statement has no driver equivalent here.\n"); return; } } @@ -709,40 +709,40 @@ private string Lit(BsonValue value, int level) switch (value.BsonType) { case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - if (doc.ElementCount == 0) - { - return "{}"; - } - string inline = "{" + string.Join(", ", doc.Elements.Select(e => $"{Escape(e.Name)}: {Lit(e.Value, level + 1)}")) + "}"; - if (inline.Length <= 72 && !inline.Contains('\n')) { - return inline; - } - var b = new StringBuilder("{\n"); - for (int i = 0; i < doc.ElementCount; i++) - { - BsonElement e = doc.GetElement(i); - b.Append(Pad(level + 1)).Append(Escape(e.Name)).Append(": ").Append(Lit(e.Value, level + 1)).Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + BsonDocument doc = value.AsBsonDocument; + if (doc.ElementCount == 0) + { + return "{}"; + } + string inline = "{" + string.Join(", ", doc.Elements.Select(e => $"{Escape(e.Name)}: {Lit(e.Value, level + 1)}")) + "}"; + if (inline.Length <= 72 && !inline.Contains('\n')) + { + return inline; + } + var b = new StringBuilder("{\n"); + for (int i = 0; i < doc.ElementCount; i++) + { + BsonElement e = doc.GetElement(i); + _ = b.Append(Pad(level + 1)).Append(Escape(e.Name)).Append(": ").Append(Lit(e.Value, level + 1)).Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append('}').ToString(); } - return b.Append(Pad(level)).Append('}').ToString(); - } case BsonType.Array: - { - BsonArray array = value.AsBsonArray; - string inline = "[" + string.Join(", ", array.Select(v => Lit(v, level + 1))) + "]"; - if (inline.Length <= 72 && !inline.Contains('\n')) - { - return inline; - } - var b = new StringBuilder("[\n"); - for (int i = 0; i < array.Count; i++) { - b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + BsonArray array = value.AsBsonArray; + string inline = "[" + string.Join(", ", array.Select(v => Lit(v, level + 1))) + "]"; + if (inline.Length <= 72 && !inline.Contains('\n')) + { + return inline; + } + var b = new StringBuilder("[\n"); + for (int i = 0; i < array.Count; i++) + { + _ = b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append(']').ToString(); } - return b.Append(Pad(level)).Append(']').ToString(); - } case BsonType.String: return Escape(value.AsString); case BsonType.Int32 or BsonType.Int64: @@ -750,27 +750,27 @@ private string Lit(BsonValue value, int level) case BsonType.Double: return Number(value.AsDouble); case BsonType.Decimal128: - Imports.Add("from bson.decimal128 import Decimal128"); + _ = Imports.Add("from bson.decimal128 import Decimal128"); return $"Decimal128({Escape(value.AsDecimal128.ToString())})"; case BsonType.Boolean: return value.AsBoolean ? "True" : "False"; case BsonType.Null: return "None"; case BsonType.ObjectId: - Imports.Add("from bson import ObjectId"); + _ = Imports.Add("from bson import ObjectId"); return $"ObjectId({Escape(value.AsObjectId.ToString())})"; case BsonType.DateTime: - { - Imports.Add("from datetime import datetime, timezone"); - DateTime at = Utc(value); - string time = at.TimeOfDay == TimeSpan.Zero ? "" : $", {at.Hour}, {at.Minute}, {at.Second}" + (at.Millisecond > 0 ? $", {at.Millisecond * 1000}" : ""); - return $"datetime({at.Year}, {at.Month}, {at.Day}{time}, tzinfo=timezone.utc)"; - } + { + _ = Imports.Add("from datetime import datetime, timezone"); + DateTime at = Utc(value); + string time = at.TimeOfDay == TimeSpan.Zero ? "" : $", {at.Hour}, {at.Minute}, {at.Second}" + (at.Millisecond > 0 ? $", {at.Millisecond * 1000}" : ""); + return $"datetime({at.Year}, {at.Month}, {at.Day}{time}, tzinfo=timezone.utc)"; + } case BsonType.RegularExpression: - Imports.Add("from bson.regex import Regex"); + _ = Imports.Add("from bson.regex import Regex"); return $"Regex({Escape(value.AsBsonRegularExpression.Pattern)}, {Escape(value.AsBsonRegularExpression.Options)})"; case BsonType.Binary when value.AsBsonBinaryData.SubType is BsonBinarySubType.UuidStandard: - Imports.Add("import uuid"); + _ = Imports.Add("import uuid"); return $"uuid.UUID({Escape(value.AsBsonBinaryData.ToGuid().ToString())})"; default: return Escape(value.ToString() ?? ""); @@ -784,14 +784,14 @@ private sealed class NodeWriter(Op op, string connectionString) : Writer(op, con { public override string Write() { - Imports.Add("MongoClient"); + _ = Imports.Add("MongoClient"); Statement(); - var head = new StringBuilder("const { ").Append(string.Join(", ", Imports)).Append(" } = require(\"mongodb\");\n\n"); - head.Append("const client = new MongoClient(").Append(Escape(ConnectionString)).Append(");\n"); - head.Append("const db = client.db(").Append(Escape(Op.Database)).Append(");\n"); + StringBuilder head = new StringBuilder("const { ").Append(string.Join(", ", Imports)).Append(" } = require(\"mongodb\");\n\n"); + _ = head.Append("const client = new MongoClient(").Append(Escape(ConnectionString)).Append(");\n"); + _ = head.Append("const db = client.db(").Append(Escape(Op.Database)).Append(");\n"); if (Op.Collection is { } c) { - head.Append("const collection = db.collection(").Append(Escape(c)).Append(");\n"); + _ = head.Append("const collection = db.collection(").Append(Escape(c)).Append(");\n"); } return head.Append('\n').Append(Body).ToString().TrimEnd() + "\n"; } @@ -803,122 +803,122 @@ private void Statement() switch (Op.Name) { case "find" or "findOne": - { - Var("filter", Op.Filter ?? []); - if (Op.Count) - { - Body.Append("\nconst count = await collection.countDocuments(filter);\n"); - return; - } - var options = new List(); - if (Op.Projection is { } p) - { - options.Add("projection: " + Lit(p, 1)); - } - if (Op.Hint is { } hint) - { - options.Add("hint: " + Lit(hint, 1)); - } - if (Op.MaxTimeMs > 0) - { - options.Add($"maxTimeMS: {Op.MaxTimeMs}"); - } - string optionText = options.Count == 0 ? "" : ", { " + string.Join(", ", options) + " }"; - Body.Append('\n'); - if (Op.Name == "findOne") { - Body.Append("const document = await collection.findOne(filter").Append(optionText).Append(");\n"); + Var("filter", Op.Filter ?? []); + if (Op.Count) + { + _ = Body.Append("\nconst count = await collection.countDocuments(filter);\n"); + return; + } + var options = new List(); + if (Op.Projection is { } p) + { + options.Add("projection: " + Lit(p, 1)); + } + if (Op.Hint is { } hint) + { + options.Add("hint: " + Lit(hint, 1)); + } + if (Op.MaxTimeMs > 0) + { + options.Add($"maxTimeMS: {Op.MaxTimeMs}"); + } + string optionText = options.Count == 0 ? "" : ", { " + string.Join(", ", options) + " }"; + _ = Body.Append('\n'); + if (Op.Name == "findOne") + { + _ = Body.Append("const document = await collection.findOne(filter").Append(optionText).Append(");\n"); + return; + } + _ = Body.Append("const documents = await collection\n .find(filter").Append(optionText).Append(')'); + if (Op.Sort is { } s) + { + _ = Body.Append("\n .sort(").Append(Lit(s, 1)).Append(')'); + } + if (Op.Skip > 0) + { + _ = Body.Append("\n .skip(").Append(Op.Skip).Append(')'); + } + if (Op.Limit > 0) + { + _ = Body.Append("\n .limit(").Append(Op.Limit).Append(')'); + } + _ = Body.Append("\n .toArray();\n"); return; } - Body.Append("const documents = await collection\n .find(filter").Append(optionText).Append(')'); - if (Op.Sort is { } s) - { - Body.Append("\n .sort(").Append(Lit(s, 1)).Append(')'); - } - if (Op.Skip > 0) - { - Body.Append("\n .skip(").Append(Op.Skip).Append(')'); - } - if (Op.Limit > 0) - { - Body.Append("\n .limit(").Append(Op.Limit).Append(')'); - } - Body.Append("\n .toArray();\n"); - return; - } case "aggregate": Var("pipeline", Op.Pipeline!); - Body.Append("\nconst documents = await collection.aggregate(pipeline).toArray();\n"); + _ = Body.Append("\nconst documents = await collection.aggregate(pipeline).toArray();\n"); return; case "countDocuments": Var("filter", Op.Filter!); - Body.Append("\nconst count = await collection.countDocuments(filter);\n"); + _ = Body.Append("\nconst count = await collection.countDocuments(filter);\n"); return; case "estimatedDocumentCount": - Body.Append("const count = await collection.estimatedDocumentCount();\n"); + _ = Body.Append("const count = await collection.estimatedDocumentCount();\n"); return; case "distinct": Var("filter", Op.Filter!); - Body.Append("\nconst values = await collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", filter);\n"); + _ = Body.Append("\nconst values = await collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", filter);\n"); return; case "insertOne": Var("document", Op.Document ?? new BsonDocument()); - Body.Append("\nconst result = await collection.insertOne(document);\n"); + _ = Body.Append("\nconst result = await collection.insertOne(document);\n"); return; case "insertMany": Var("documents", Op.Document ?? new BsonArray()); - Body.Append("\nconst result = await collection.insertMany(documents);\n"); + _ = Body.Append("\nconst result = await collection.insertMany(documents);\n"); return; case "updateOne" or "updateMany" or "replaceOne" or "findOneAndUpdate" or "findOneAndReplace": - { - Var("filter", Op.Filter!); - string second = Op.Name is "replaceOne" or "findOneAndReplace" ? "replacement" : "update"; - Var(second, Op.Update ?? new BsonDocument()); - var options = new List(); - if (Op.Upsert) { - options.Add("upsert: true"); - } - if (Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal)) - { - options.Add($"returnDocument: \"{(Op.ReturnNew ? "after" : "before")}\""); + Var("filter", Op.Filter!); + string second = Op.Name is "replaceOne" or "findOneAndReplace" ? "replacement" : "update"; + Var(second, Op.Update ?? new BsonDocument()); + var options = new List(); + if (Op.Upsert) + { + options.Add("upsert: true"); + } + if (Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal)) + { + options.Add($"returnDocument: \"{(Op.ReturnNew ? "after" : "before")}\""); + } + _ = Body.Append('\n').Append(Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal) ? "const document" : "const result") + .Append(" = await collection.").Append(Op.Name).Append("(filter, ").Append(second) + .Append(options.Count > 0 ? ", { " + string.Join(", ", options) + " }" : "").Append(");\n"); + return; } - Body.Append('\n').Append(Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal) ? "const document" : "const result") - .Append(" = await collection.").Append(Op.Name).Append("(filter, ").Append(second) - .Append(options.Count > 0 ? ", { " + string.Join(", ", options) + " }" : "").Append(");\n"); - return; - } case "deleteOne" or "deleteMany" or "findOneAndDelete": Var("filter", Op.Filter!); - Body.Append('\n').Append(Op.Name == "findOneAndDelete" ? "const document" : "const result") + _ = Body.Append('\n').Append(Op.Name == "findOneAndDelete" ? "const document" : "const result") .Append(" = await collection.").Append(Op.Name).Append("(filter);\n"); return; case "createIndex": - Body.Append("const name = await collection.createIndex(").Append(Lit(Op.Document ?? new BsonDocument(), 0)); + _ = Body.Append("const name = await collection.createIndex(").Append(Lit(Op.Document ?? new BsonDocument(), 0)); if (Op.Command is { ElementCount: > 0 } options2) { - Body.Append(", ").Append(Lit(options2, 0)); + _ = Body.Append(", ").Append(Lit(options2, 0)); } - Body.Append(");\n"); + _ = Body.Append(");\n"); return; case "dropIndex": - Body.Append("await collection.dropIndex(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); + _ = Body.Append("await collection.dropIndex(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); return; case "getIndexes": - Body.Append("const indexes = await collection.indexes();\n"); + _ = Body.Append("const indexes = await collection.indexes();\n"); return; case "drop": - Body.Append("await collection.drop();\n"); + _ = Body.Append("await collection.drop();\n"); return; case "stats": - Body.Append("const stats = await db.command({ collStats: ").Append(Escape(Op.Collection ?? "")).Append(" });\n"); + _ = Body.Append("const stats = await db.command({ collStats: ").Append(Escape(Op.Collection ?? "")).Append(" });\n"); return; case "runCommand": Var("command", Op.Command ?? []); - Body.Append("\nconst reply = await db.command(command);\n"); + _ = Body.Append("\nconst reply = await db.command(command);\n"); return; default: - Body.Append("// This statement has no driver equivalent here.\n"); + _ = Body.Append("// This statement has no driver equivalent here.\n"); return; } } @@ -928,65 +928,65 @@ private string Lit(BsonValue value, int level) switch (value.BsonType) { case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - if (doc.ElementCount == 0) - { - return "{}"; - } - string inline = "{ " + string.Join(", ", doc.Elements.Select(e => $"{Key(e.Name)}: {Lit(e.Value, level + 1)}")) + " }"; - if (inline.Length <= 72 && !inline.Contains('\n')) - { - return inline; - } - var b = new StringBuilder("{\n"); - for (int i = 0; i < doc.ElementCount; i++) { - BsonElement e = doc.GetElement(i); - b.Append(Pad(level + 1)).Append(Key(e.Name)).Append(": ").Append(Lit(e.Value, level + 1)).Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + BsonDocument doc = value.AsBsonDocument; + if (doc.ElementCount == 0) + { + return "{}"; + } + string inline = "{ " + string.Join(", ", doc.Elements.Select(e => $"{Key(e.Name)}: {Lit(e.Value, level + 1)}")) + " }"; + if (inline.Length <= 72 && !inline.Contains('\n')) + { + return inline; + } + var b = new StringBuilder("{\n"); + for (int i = 0; i < doc.ElementCount; i++) + { + BsonElement e = doc.GetElement(i); + _ = b.Append(Pad(level + 1)).Append(Key(e.Name)).Append(": ").Append(Lit(e.Value, level + 1)).Append(i < doc.ElementCount - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append('}').ToString(); } - return b.Append(Pad(level)).Append('}').ToString(); - } case BsonType.Array: - { - BsonArray array = value.AsBsonArray; - string inline = "[" + string.Join(", ", array.Select(v => Lit(v, level + 1))) + "]"; - if (inline.Length <= 72 && !inline.Contains('\n')) { - return inline; - } - var b = new StringBuilder("[\n"); - for (int i = 0; i < array.Count; i++) - { - b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + BsonArray array = value.AsBsonArray; + string inline = "[" + string.Join(", ", array.Select(v => Lit(v, level + 1))) + "]"; + if (inline.Length <= 72 && !inline.Contains('\n')) + { + return inline; + } + var b = new StringBuilder("[\n"); + for (int i = 0; i < array.Count; i++) + { + _ = b.Append(Pad(level + 1)).Append(Lit(array[i], level + 1)).Append(i < array.Count - 1 ? ",\n" : "\n"); + } + return b.Append(Pad(level)).Append(']').ToString(); } - return b.Append(Pad(level)).Append(']').ToString(); - } case BsonType.String: return Escape(value.AsString); case BsonType.Int32: return value.AsInt32.ToString(CultureInfo.InvariantCulture); case BsonType.Int64: - Imports.Add("Long"); + _ = Imports.Add("Long"); return $"Long.fromString({Escape(value.AsInt64.ToString(CultureInfo.InvariantCulture))})"; case BsonType.Double: return value.AsDouble.ToString("R", CultureInfo.InvariantCulture); case BsonType.Decimal128: - Imports.Add("Decimal128"); + _ = Imports.Add("Decimal128"); return $"Decimal128.fromString({Escape(value.AsDecimal128.ToString())})"; case BsonType.Boolean: return value.AsBoolean ? "true" : "false"; case BsonType.Null: return "null"; case BsonType.ObjectId: - Imports.Add("ObjectId"); + _ = Imports.Add("ObjectId"); return $"new ObjectId({Escape(value.AsObjectId.ToString())})"; case BsonType.DateTime: return $"new Date({Escape(IsoString(value))})"; case BsonType.RegularExpression: return $"/{value.AsBsonRegularExpression.Pattern.Replace("/", "\\/", StringComparison.Ordinal)}/{value.AsBsonRegularExpression.Options}"; case BsonType.Binary when value.AsBsonBinaryData.SubType is BsonBinarySubType.UuidStandard: - Imports.Add("UUID"); + _ = Imports.Add("UUID"); return $"new UUID({Escape(value.AsBsonBinaryData.ToGuid().ToString())})"; default: return Escape(value.ToString() ?? ""); @@ -1003,25 +1003,25 @@ private sealed class JavaWriter(Op op, string connectionString) : Writer(op, con { public override string Write() { - Imports.Add("com.mongodb.client.MongoClient"); - Imports.Add("com.mongodb.client.MongoClients"); - Imports.Add("com.mongodb.client.MongoDatabase"); - Imports.Add("org.bson.Document"); + _ = Imports.Add("com.mongodb.client.MongoClient"); + _ = Imports.Add("com.mongodb.client.MongoClients"); + _ = Imports.Add("com.mongodb.client.MongoDatabase"); + _ = Imports.Add("org.bson.Document"); if (Op.Collection is not null) { - Imports.Add("com.mongodb.client.MongoCollection"); + _ = Imports.Add("com.mongodb.client.MongoCollection"); } Statement(); var head = new StringBuilder(); foreach (string import in Imports) { - head.Append("import ").Append(import).Append(";\n"); + _ = head.Append("import ").Append(import).Append(";\n"); } - head.Append("\nMongoClient client = MongoClients.create(").Append(Escape(ConnectionString)).Append(");\n"); - head.Append("MongoDatabase database = client.getDatabase(").Append(Escape(Op.Database)).Append(");\n"); + _ = head.Append("\nMongoClient client = MongoClients.create(").Append(Escape(ConnectionString)).Append(");\n"); + _ = head.Append("MongoDatabase database = client.getDatabase(").Append(Escape(Op.Database)).Append(");\n"); if (Op.Collection is { } c) { - head.Append("MongoCollection collection = database.getCollection(").Append(Escape(c)).Append(");\n"); + _ = head.Append("MongoCollection collection = database.getCollection(").Append(Escape(c)).Append(");\n"); } return head.Append('\n').Append(Body).ToString().TrimEnd() + "\n"; } @@ -1033,145 +1033,145 @@ private void Statement() switch (Op.Name) { case "find" or "findOne": - { - Var("filter", Op.Filter ?? []); - if (Op.Count) - { - Body.Append("\nlong count = collection.countDocuments(filter);\n"); - return; - } - if (Op.Projection is { } p) - { - Var("projection", p); - } - if (Op.Sort is { } s) { - Var("sort", s); - } - Body.Append('\n'); - if (Op.Name == "findOne") - { - Body.Append("Document document = collection.find(filter)").Append(Op.Projection is null ? "" : ".projection(projection)").Append(".first();\n"); + Var("filter", Op.Filter ?? []); + if (Op.Count) + { + _ = Body.Append("\nlong count = collection.countDocuments(filter);\n"); + return; + } + if (Op.Projection is { } p) + { + Var("projection", p); + } + if (Op.Sort is { } s) + { + Var("sort", s); + } + _ = Body.Append('\n'); + if (Op.Name == "findOne") + { + _ = Body.Append("Document document = collection.find(filter)").Append(Op.Projection is null ? "" : ".projection(projection)").Append(".first();\n"); + return; + } + _ = Imports.Add("java.util.ArrayList"); + _ = Imports.Add("java.util.List"); + _ = Body.Append("List documents = collection.find(filter)"); + if (Op.Projection is not null) + { + _ = Body.Append("\n .projection(projection)"); + } + if (Op.Sort is not null) + { + _ = Body.Append("\n .sort(sort)"); + } + if (Op.Skip > 0) + { + _ = Body.Append("\n .skip(").Append(Op.Skip).Append(')'); + } + if (Op.Limit > 0) + { + _ = Body.Append("\n .limit(").Append(Op.Limit).Append(')'); + } + if (Op.Hint is { } hint) + { + _ = Body.Append(hint is BsonString ? "\n .hintString(" : "\n .hint(").Append(Lit(hint, 1)).Append(')'); + } + if (Op.MaxTimeMs > 0) + { + _ = Imports.Add("java.util.concurrent.TimeUnit"); + _ = Body.Append("\n .maxTime(").Append(Op.MaxTimeMs).Append(", TimeUnit.MILLISECONDS)"); + } + _ = Body.Append("\n .into(new ArrayList<>());\n"); return; } - Imports.Add("java.util.ArrayList"); - Imports.Add("java.util.List"); - Body.Append("List documents = collection.find(filter)"); - if (Op.Projection is not null) - { - Body.Append("\n .projection(projection)"); - } - if (Op.Sort is not null) - { - Body.Append("\n .sort(sort)"); - } - if (Op.Skip > 0) - { - Body.Append("\n .skip(").Append(Op.Skip).Append(')'); - } - if (Op.Limit > 0) - { - Body.Append("\n .limit(").Append(Op.Limit).Append(')'); - } - if (Op.Hint is { } hint) - { - Body.Append(hint is BsonString ? "\n .hintString(" : "\n .hint(").Append(Lit(hint, 1)).Append(')'); - } - if (Op.MaxTimeMs > 0) - { - Imports.Add("java.util.concurrent.TimeUnit"); - Body.Append("\n .maxTime(").Append(Op.MaxTimeMs).Append(", TimeUnit.MILLISECONDS)"); - } - Body.Append("\n .into(new ArrayList<>());\n"); - return; - } case "aggregate": - Imports.Add("java.util.ArrayList"); - Imports.Add("java.util.Arrays"); - Imports.Add("java.util.List"); - Body.Append("List pipeline = Arrays.asList(\n"); + _ = Imports.Add("java.util.ArrayList"); + _ = Imports.Add("java.util.Arrays"); + _ = Imports.Add("java.util.List"); + _ = Body.Append("List pipeline = Arrays.asList(\n"); for (int i = 0; i < Op.Pipeline!.Count; i++) { - Body.Append(Pad(1)).Append(Lit(Op.Pipeline[i], 1)).Append(i < Op.Pipeline.Count - 1 ? ",\n" : "\n"); + _ = Body.Append(Pad(1)).Append(Lit(Op.Pipeline[i], 1)).Append(i < Op.Pipeline.Count - 1 ? ",\n" : "\n"); } - Body.Append(");\n\nList documents = collection.aggregate(pipeline).into(new ArrayList<>());\n"); + _ = Body.Append(");\n\nList documents = collection.aggregate(pipeline).into(new ArrayList<>());\n"); return; case "countDocuments": Var("filter", Op.Filter!); - Body.Append("\nlong count = collection.countDocuments(filter);\n"); + _ = Body.Append("\nlong count = collection.countDocuments(filter);\n"); return; case "estimatedDocumentCount": - Body.Append("long count = collection.estimatedDocumentCount();\n"); + _ = Body.Append("long count = collection.estimatedDocumentCount();\n"); return; case "distinct": - Imports.Add("java.util.ArrayList"); - Imports.Add("java.util.List"); - Imports.Add("org.bson.BsonValue"); + _ = Imports.Add("java.util.ArrayList"); + _ = Imports.Add("java.util.List"); + _ = Imports.Add("org.bson.BsonValue"); Var("filter", Op.Filter!); - Body.Append("\nList values = collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", BsonValue.class).filter(filter).into(new ArrayList<>());\n"); + _ = Body.Append("\nList values = collection.distinct(").Append(Escape(Op.Field ?? "")).Append(", BsonValue.class).filter(filter).into(new ArrayList<>());\n"); return; case "insertOne": Var("document", Op.Document ?? new BsonDocument()); - Body.Append("\ncollection.insertOne(document);\n"); + _ = Body.Append("\ncollection.insertOne(document);\n"); return; case "insertMany": - { - Imports.Add("java.util.Arrays"); - Imports.Add("java.util.List"); - BsonArray docs = Op.Document as BsonArray ?? []; - Body.Append("List documents = Arrays.asList(\n"); - for (int i = 0; i < docs.Count; i++) { - Body.Append(Pad(1)).Append(Lit(docs[i], 1)).Append(i < docs.Count - 1 ? ",\n" : "\n"); + _ = Imports.Add("java.util.Arrays"); + _ = Imports.Add("java.util.List"); + BsonArray docs = Op.Document as BsonArray ?? []; + _ = Body.Append("List documents = Arrays.asList(\n"); + for (int i = 0; i < docs.Count; i++) + { + _ = Body.Append(Pad(1)).Append(Lit(docs[i], 1)).Append(i < docs.Count - 1 ? ",\n" : "\n"); + } + _ = Body.Append(");\n\ncollection.insertMany(documents);\n"); + return; } - Body.Append(");\n\ncollection.insertMany(documents);\n"); - return; - } case "updateOne" or "updateMany" or "replaceOne" or "findOneAndUpdate" or "findOneAndReplace": - { - Var("filter", Op.Filter!); - string second = Op.Name is "replaceOne" or "findOneAndReplace" ? "replacement" : "update"; - Var(second, Op.Update ?? new BsonDocument()); - Body.Append('\n'); - if (Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal)) { - string optionsType = Op.Name == "findOneAndUpdate" ? "FindOneAndUpdateOptions" : "FindOneAndReplaceOptions"; - Imports.Add("com.mongodb.client.model." + optionsType); - Imports.Add("com.mongodb.client.model.ReturnDocument"); - Body.Append("Document document = collection.").Append(Op.Name).Append("(filter, ").Append(second).Append(", new ").Append(optionsType) - .Append("()\n .returnDocument(ReturnDocument.").Append(Op.ReturnNew ? "AFTER" : "BEFORE").Append(")\n .upsert(") - .Append(Op.Upsert ? "true" : "false").Append("));\n"); + Var("filter", Op.Filter!); + string second = Op.Name is "replaceOne" or "findOneAndReplace" ? "replacement" : "update"; + Var(second, Op.Update ?? new BsonDocument()); + _ = Body.Append('\n'); + if (Op.Name.StartsWith("findOneAnd", StringComparison.Ordinal)) + { + string optionsType = Op.Name == "findOneAndUpdate" ? "FindOneAndUpdateOptions" : "FindOneAndReplaceOptions"; + _ = Imports.Add("com.mongodb.client.model." + optionsType); + _ = Imports.Add("com.mongodb.client.model.ReturnDocument"); + _ = Body.Append("Document document = collection.").Append(Op.Name).Append("(filter, ").Append(second).Append(", new ").Append(optionsType) + .Append("()\n .returnDocument(ReturnDocument.").Append(Op.ReturnNew ? "AFTER" : "BEFORE").Append(")\n .upsert(") + .Append(Op.Upsert ? "true" : "false").Append("));\n"); + return; + } + string optionsName = Op.Name == "replaceOne" ? "ReplaceOptions" : "UpdateOptions"; + if (Op.Upsert) + { + _ = Imports.Add("com.mongodb.client.model." + optionsName); + } + _ = Body.Append(Op.Name == "replaceOne" ? "UpdateResult" : "UpdateResult").Append(" result = collection.").Append(Op.Name).Append("(filter, ").Append(second) + .Append(Op.Upsert ? $", new {optionsName}().upsert(true)" : "").Append(");\n"); + _ = Imports.Add("com.mongodb.client.result.UpdateResult"); return; } - string optionsName = Op.Name == "replaceOne" ? "ReplaceOptions" : "UpdateOptions"; - if (Op.Upsert) - { - Imports.Add("com.mongodb.client.model." + optionsName); - } - Body.Append(Op.Name == "replaceOne" ? "UpdateResult" : "UpdateResult").Append(" result = collection.").Append(Op.Name).Append("(filter, ").Append(second) - .Append(Op.Upsert ? $", new {optionsName}().upsert(true)" : "").Append(");\n"); - Imports.Add("com.mongodb.client.result.UpdateResult"); - return; - } case "deleteOne" or "deleteMany": - Imports.Add("com.mongodb.client.result.DeleteResult"); + _ = Imports.Add("com.mongodb.client.result.DeleteResult"); Var("filter", Op.Filter!); - Body.Append("\nDeleteResult result = collection.").Append(Op.Name).Append("(filter);\n"); + _ = Body.Append("\nDeleteResult result = collection.").Append(Op.Name).Append("(filter);\n"); return; case "findOneAndDelete": Var("filter", Op.Filter!); - Body.Append("\nDocument document = collection.findOneAndDelete(filter);\n"); + _ = Body.Append("\nDocument document = collection.findOneAndDelete(filter);\n"); return; case "createIndex": Var("keys", Op.Document ?? new BsonDocument()); - Body.Append("\nString name = collection.createIndex(keys"); + _ = Body.Append("\nString name = collection.createIndex(keys"); if (Op.Command is { ElementCount: > 0 } options) { - Imports.Add("com.mongodb.client.model.IndexOptions"); - Body.Append(", new IndexOptions()"); + _ = Imports.Add("com.mongodb.client.model.IndexOptions"); + _ = Body.Append(", new IndexOptions()"); foreach (BsonElement e in options) { - Body.Append(e.Name switch + _ = Body.Append(e.Name switch { "unique" => $".unique({(e.Value.ToBoolean() ? "true" : "false")})", "sparse" => $".sparse({(e.Value.ToBoolean() ? "true" : "false")})", @@ -1180,28 +1180,28 @@ private void Statement() }); } } - Body.Append(");\n"); + _ = Body.Append(");\n"); return; case "dropIndex": - Body.Append("collection.dropIndex(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); + _ = Body.Append("collection.dropIndex(").Append(Escape(Op.Index?.ToString() ?? "")).Append(");\n"); return; case "getIndexes": - Imports.Add("java.util.ArrayList"); - Imports.Add("java.util.List"); - Body.Append("List indexes = collection.listIndexes().into(new ArrayList<>());\n"); + _ = Imports.Add("java.util.ArrayList"); + _ = Imports.Add("java.util.List"); + _ = Body.Append("List indexes = collection.listIndexes().into(new ArrayList<>());\n"); return; case "drop": - Body.Append("collection.drop();\n"); + _ = Body.Append("collection.drop();\n"); return; case "stats": - Body.Append("Document stats = database.runCommand(new Document(\"collStats\", ").Append(Escape(Op.Collection ?? "")).Append("));\n"); + _ = Body.Append("Document stats = database.runCommand(new Document(\"collStats\", ").Append(Escape(Op.Collection ?? "")).Append("));\n"); return; case "runCommand": Var("command", Op.Command ?? []); - Body.Append("\nDocument reply = database.runCommand(command);\n"); + _ = Body.Append("\nDocument reply = database.runCommand(command);\n"); return; default: - Body.Append("// This statement has no driver equivalent here.\n"); + _ = Body.Append("// This statement has no driver equivalent here.\n"); return; } } @@ -1211,22 +1211,22 @@ private string Lit(BsonValue value, int level) switch (value.BsonType) { case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - if (doc.ElementCount == 0) - { - return "new Document()"; - } - var b = new StringBuilder("new Document(").Append(Escape(doc.GetElement(0).Name)).Append(", ").Append(Lit(doc[0], level + 1)).Append(')'); - for (int i = 1; i < doc.ElementCount; i++) { - BsonElement e = doc.GetElement(i); - b.Append('\n').Append(Pad(level + 1)).Append(".append(").Append(Escape(e.Name)).Append(", ").Append(Lit(e.Value, level + 1)).Append(')'); + BsonDocument doc = value.AsBsonDocument; + if (doc.ElementCount == 0) + { + return "new Document()"; + } + StringBuilder b = new StringBuilder("new Document(").Append(Escape(doc.GetElement(0).Name)).Append(", ").Append(Lit(doc[0], level + 1)).Append(')'); + for (int i = 1; i < doc.ElementCount; i++) + { + BsonElement e = doc.GetElement(i); + _ = b.Append('\n').Append(Pad(level + 1)).Append(".append(").Append(Escape(e.Name)).Append(", ").Append(Lit(e.Value, level + 1)).Append(')'); + } + return b.ToString(); } - return b.ToString(); - } case BsonType.Array: - Imports.Add("java.util.Arrays"); + _ = Imports.Add("java.util.Arrays"); return "Arrays.asList(" + string.Join(", ", value.AsBsonArray.Select(v => Lit(v, level + 1))) + ")"; case BsonType.String: return Escape(value.AsString); @@ -1237,25 +1237,25 @@ private string Lit(BsonValue value, int level) case BsonType.Double: return Number(value.AsDouble); case BsonType.Decimal128: - Imports.Add("java.math.BigDecimal"); - Imports.Add("org.bson.types.Decimal128"); + _ = Imports.Add("java.math.BigDecimal"); + _ = Imports.Add("org.bson.types.Decimal128"); return $"new Decimal128(new BigDecimal({Escape(value.AsDecimal128.ToString())}))"; case BsonType.Boolean: return value.AsBoolean ? "true" : "false"; case BsonType.Null: return "null"; case BsonType.ObjectId: - Imports.Add("org.bson.types.ObjectId"); + _ = Imports.Add("org.bson.types.ObjectId"); return $"new ObjectId({Escape(value.AsObjectId.ToString())})"; case BsonType.DateTime: - Imports.Add("java.time.Instant"); - Imports.Add("java.util.Date"); + _ = Imports.Add("java.time.Instant"); + _ = Imports.Add("java.util.Date"); return $"Date.from(Instant.parse({Escape(IsoString(value))}))"; case BsonType.RegularExpression: - Imports.Add("org.bson.BsonRegularExpression"); + _ = Imports.Add("org.bson.BsonRegularExpression"); return $"new BsonRegularExpression({Escape(value.AsBsonRegularExpression.Pattern)}, {Escape(value.AsBsonRegularExpression.Options)})"; case BsonType.Binary when value.AsBsonBinaryData.SubType is BsonBinarySubType.UuidStandard: - Imports.Add("java.util.UUID"); + _ = Imports.Add("java.util.UUID"); return $"UUID.fromString({Escape(value.AsBsonBinaryData.ToGuid().ToString())})"; default: return Escape(value.ToString() ?? ""); diff --git a/plugins/VelaShell.Plugin.Mongo/Shell/ShellCompletion.cs b/plugins/VelaShell.Plugin.Mongo/Shell/ShellCompletion.cs index a41bfce..bda39ea 100644 --- a/plugins/VelaShell.Plugin.Mongo/Shell/ShellCompletion.cs +++ b/plugins/VelaShell.Plugin.Mongo/Shell/ShellCompletion.cs @@ -630,24 +630,24 @@ internal static (string? Collection, string? Method, string? Database) Head(stri shape = new PipelineShape([.. group.Names], "$group"); break; case "$project" when element.Value is BsonDocument project: - { - bool inclusion = project.Elements.Any(static e => e.Name != "_id" && !(e.Value.IsNumeric && e.Value.ToDouble() == 0) - && !(e.Value is BsonBoolean { Value: false })); - if (inclusion) { - List fields = [.. project.Elements.Where(static e => !(e.Value.IsNumeric && e.Value.ToDouble() == 0) && e.Value is not BsonBoolean { Value: false }).Select(static e => e.Name)]; - if (!project.Contains("_id")) + bool inclusion = project.Elements.Any(static e => e.Name != "_id" && !(e.Value.IsNumeric && e.Value.ToDouble() == 0) + && !(e.Value is BsonBoolean { Value: false })); + if (inclusion) { - fields.Insert(0, "_id"); + List fields = [.. project.Elements.Where(static e => !(e.Value.IsNumeric && e.Value.ToDouble() == 0) && e.Value is not BsonBoolean { Value: false }).Select(static e => e.Name)]; + if (!project.Contains("_id")) + { + fields.Insert(0, "_id"); + } + shape = new PipelineShape(fields, "$project"); } - shape = new PipelineShape(fields, "$project"); - } - else if (shape is not null) - { - shape = shape with { Fields = [.. shape.Fields.Where(f => !project.Contains(f))] }; + else if (shape is not null) + { + shape = shape with { Fields = [.. shape.Fields.Where(f => !project.Contains(f))] }; + } + break; } - break; - } case "$addFields" or "$set" when element.Value is BsonDocument added && shape is not null: shape = new PipelineShape([.. shape.Fields, .. added.Names.Where(n => !shape.Fields.Contains(n))], shape.Source); break; diff --git a/plugins/VelaShell.Plugin.Mongo/Shell/ShellExecutor.cs b/plugins/VelaShell.Plugin.Mongo/Shell/ShellExecutor.cs index 5f6d902..f99edc1 100644 --- a/plugins/VelaShell.Plugin.Mongo/Shell/ShellExecutor.cs +++ b/plugins/VelaShell.Plugin.Mongo/Shell/ShellExecutor.cs @@ -155,7 +155,7 @@ internal sealed class ShellExecutor(MongoConnection connection, IShellGuard guar public async Task ExecuteAsync(ShellCommand command, ShellSession session, ShellRunOptions options, CancellationToken cancellationToken = default) { - Stopwatch watch = Stopwatch.StartNew(); + var watch = Stopwatch.StartNew(); ShellResult result = command.Kind switch { ShellCommandKind.Use => Use(command, session), @@ -206,91 +206,91 @@ private async Task DatabaseAsync(ShellCommand command, string datab switch (method.Name) { case "getCollectionNames": - { - IReadOnlyList collections = await connection.ListCollectionsAsync(database, cancellationToken).ConfigureAwait(false); - return Documents(command, database, null, [.. collections.Select(static c => new BsonDocument("name", c.Name))], false); - } + { + IReadOnlyList collections = await connection.ListCollectionsAsync(database, cancellationToken).ConfigureAwait(false); + return Documents(command, database, null, [.. collections.Select(static c => new BsonDocument("name", c.Name))], false); + } case "getCollectionInfos": - { - var listOptions = new ListCollectionsOptions { Filter = method.Document(0) ?? [] }; - using IAsyncCursor cursor = await db.ListCollectionsAsync(listOptions, cancellationToken).ConfigureAwait(false); - return Documents(command, database, null, await cursor.ToListAsync(cancellationToken).ConfigureAwait(false), false); - } + { + var listOptions = new ListCollectionsOptions { Filter = method.Document(0) ?? [] }; + using IAsyncCursor cursor = await db.ListCollectionsAsync(listOptions, cancellationToken).ConfigureAwait(false); + return Documents(command, database, null, await cursor.ToListAsync(cancellationToken).ConfigureAwait(false), false); + } case "stats": - { - var stats = new BsonDocument("dbStats", 1); - if (method.Arg(0) is { IsNumeric: true } scale) { - stats["scale"] = scale; + var stats = new BsonDocument("dbStats", 1); + if (method.Arg(0) is { IsNumeric: true } scale) + { + stats["scale"] = scale; + } + return Single(command, database, null, await connection.RunCommandAsync(database, stats, cancellationToken).ConfigureAwait(false)); } - return Single(command, database, null, await connection.RunCommandAsync(database, stats, cancellationToken).ConfigureAwait(false)); - } case "runCommand" or "adminCommand": - { - string target = method.Name == "adminCommand" ? "admin" : database; - BsonDocument body = method.Arg(0) switch - { - BsonDocument d => d, - BsonString s => new BsonDocument(s.Value, 1), - _ => throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name)) - }; - if (body.ElementCount > 0 && ShellParser.WriteCommands.Contains(body.GetElement(0).Name)) { - if (!guard.EnsureWritable(target)) + string target = method.Name == "adminCommand" ? "admin" : database; + BsonDocument body = method.Arg(0) switch { - return Blocked(command, database); - } - if (body.GetElement(0).Name.Equals("dropDatabase", StringComparison.OrdinalIgnoreCase) - && !await ConfirmDropDatabaseAsync(target).ConfigureAwait(false)) + BsonDocument d => d, + BsonString s => new BsonDocument(s.Value, 1), + _ => throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name)) + }; + if (body.ElementCount > 0 && ShellParser.WriteCommands.Contains(body.GetElement(0).Name)) { - return Declined(command, target); + if (!guard.EnsureWritable(target)) + { + return Blocked(command, database); + } + if (body.GetElement(0).Name.Equals("dropDatabase", StringComparison.OrdinalIgnoreCase) + && !await ConfirmDropDatabaseAsync(target).ConfigureAwait(false)) + { + return Declined(command, target); + } } + BsonDocument reply = await connection.RunCommandAsync(target, body, cancellationToken).ConfigureAwait(false); + bool catalog = body.ElementCount > 0 && body.GetElement(0).Name is "create" or "drop" or "renameCollection" or "dropDatabase"; + return Single(command, target, null, reply) with { ChangesCatalog = catalog }; } - BsonDocument reply = await connection.RunCommandAsync(target, body, cancellationToken).ConfigureAwait(false); - bool catalog = body.ElementCount > 0 && body.GetElement(0).Name is "create" or "drop" or "renameCollection" or "dropDatabase"; - return Single(command, target, null, reply) with { ChangesCatalog = catalog }; - } case "createCollection" or "createView": - { - if (method.Arg(0) is not BsonString name) - { - throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name)); - } - if (!guard.EnsureWritable(database)) - { - return Blocked(command, database); - } - var create = new BsonDocument("create", name.Value); - if (method.Name == "createView") - { - create["viewOn"] = method.Arg(1) as BsonString ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", "viewOn")); - create["pipeline"] = method.Arg(2) as BsonArray ?? []; - create.Merge(method.Document(3) ?? [], overwriteExistingElements: false); - } - else { - create.Merge(method.Document(1) ?? [], overwriteExistingElements: false); + if (method.Arg(0) is not BsonString name) + { + throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name)); + } + if (!guard.EnsureWritable(database)) + { + return Blocked(command, database); + } + var create = new BsonDocument("create", name.Value); + if (method.Name == "createView") + { + create["viewOn"] = method.Arg(1) as BsonString ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", "viewOn")); + create["pipeline"] = method.Arg(2) as BsonArray ?? []; + _ = create.Merge(method.Document(3) ?? [], overwriteExistingElements: false); + } + else + { + _ = create.Merge(method.Document(1) ?? [], overwriteExistingElements: false); + } + BsonDocument reply = await connection.RunCommandAsync(database, create, cancellationToken).ConfigureAwait(false); + return Single(command, database, name.Value, reply) with { ChangesCatalog = true }; } - BsonDocument reply = await connection.RunCommandAsync(database, create, cancellationToken).ConfigureAwait(false); - return Single(command, database, name.Value, reply) with { ChangesCatalog = true }; - } case "dropDatabase": - { - if (guard.DisableDropDatabase) - { - throw new ShellExecutionException(loc["Query_ErrDropDatabaseDisabled"]); - } - if (!guard.EnsureWritable(database)) - { - return Blocked(command, database); - } - if (!await ConfirmDropDatabaseAsync(database).ConfigureAwait(false)) { - return Declined(command, database); + if (guard.DisableDropDatabase) + { + throw new ShellExecutionException(loc["Query_ErrDropDatabaseDisabled"]); + } + if (!guard.EnsureWritable(database)) + { + return Blocked(command, database); + } + if (!await ConfirmDropDatabaseAsync(database).ConfigureAwait(false)) + { + return Declined(command, database); + } + BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument("dropDatabase", 1), cancellationToken).ConfigureAwait(false); + return Single(command, database, null, reply) with { ChangesCatalog = true }; } - BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument("dropDatabase", 1), cancellationToken).ConfigureAwait(false); - return Single(command, database, null, reply) with { ChangesCatalog = true }; - } case "getName": return Single(command, database, null, new BsonDocument("name", database)); case "version": @@ -302,28 +302,28 @@ private async Task DatabaseAsync(ShellCommand command, string datab return Single(command, database, null, await connection.RunCommandAsync(database, new BsonDocument("profile", -1), cancellationToken).ConfigureAwait(false)); case "currentOp": - { - var current = new BsonDocument("currentOp", 1); - current.Merge(method.Document(0) ?? [], overwriteExistingElements: true); - BsonDocument reply = await connection.RunCommandAsync("admin", current, cancellationToken).ConfigureAwait(false); - return Documents(command, database, null, Docs(reply.GetValue("inprog", new BsonArray())), false); - } + { + var current = new BsonDocument("currentOp", 1); + _ = current.Merge(method.Document(0) ?? [], overwriteExistingElements: true); + BsonDocument reply = await connection.RunCommandAsync("admin", current, cancellationToken).ConfigureAwait(false); + return Documents(command, database, null, Docs(reply.GetValue("inprog", new BsonArray())), false); + } case "getUsers" or "getRoles": - { - string commandName = method.Name == "getUsers" ? "usersInfo" : "rolesInfo"; - BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument(commandName, 1), cancellationToken).ConfigureAwait(false); - return Documents(command, database, null, Docs(reply.GetValue(method.Name == "getUsers" ? "users" : "roles", new BsonArray())), false); - } + { + string commandName = method.Name == "getUsers" ? "usersInfo" : "rolesInfo"; + BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument(commandName, 1), cancellationToken).ConfigureAwait(false); + return Documents(command, database, null, Docs(reply.GetValue(method.Name == "getUsers" ? "users" : "roles", new BsonArray())), false); + } case "aggregate": - { - BsonArray pipeline = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsPipeline", "aggregate")); - var aggregateOptions = new AggregateOptions { MaxTime = MaxTime(options.MaxTimeMs), Comment = Comment(options) }; - using IAsyncCursor cursor = await db.AggregateAsync( - PipelineDefinition.Create(pipeline.Select(static s => s.AsBsonDocument)), - aggregateOptions, cancellationToken).ConfigureAwait(false); - (List docs, bool truncated) = await DrainAsync(cursor, options.MaxDocuments, cancellationToken).ConfigureAwait(false); - return Documents(command, database, null, docs, truncated) with { Pipeline = pipeline }; - } + { + BsonArray pipeline = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsPipeline", "aggregate")); + var aggregateOptions = new AggregateOptions { MaxTime = MaxTime(options.MaxTimeMs), Comment = Comment(options) }; + using IAsyncCursor cursor = await db.AggregateAsync( + PipelineDefinition.Create(pipeline.Select(static s => s.AsBsonDocument)), + aggregateOptions, cancellationToken).ConfigureAwait(false); + (List docs, bool truncated) = await DrainAsync(cursor, options.MaxDocuments, cancellationToken).ConfigureAwait(false); + return Documents(command, database, null, docs, truncated) with { Pipeline = pipeline }; + } default: throw new ShellExecutionException(loc.Format("Query_ErrUnsupportedMethod", "db." + method.Name)); } @@ -350,152 +350,152 @@ private async Task CollectionAsync(ShellCommand command, string dat return await FindAsync(command, collection, database, maxTime, comment, options, cancellationToken).ConfigureAwait(false); case "aggregate": - { - BsonArray pipeline = Pipeline(method); - bool writes = pipeline.Any(static s => s is BsonDocument d && (d.Contains("$out") || d.Contains("$merge"))); - if (writes && !guard.EnsureWritable(database)) { - return Blocked(command, database); + BsonArray pipeline = Pipeline(method); + bool writes = pipeline.Any(static s => s is BsonDocument d && (d.Contains("$out") || d.Contains("$merge"))); + if (writes && !guard.EnsureWritable(database)) + { + return Blocked(command, database); + } + BsonDocument settings = method.Document(1) ?? []; + var aggregateOptions = new AggregateOptions + { + AllowDiskUse = settings.TryGetValue("allowDiskUse", out BsonValue disk) ? disk.ToBoolean() + : command.Modifier("allowDiskUse") is not null ? true : null, + MaxTime = settings.TryGetValue("maxTimeMS", out BsonValue max) && max.IsNumeric ? MaxTime(max.ToInt32()) : maxTime, + Hint = settings.GetValue("hint", null) ?? command.Modifier("hint")?.Arg(0), + Collation = Collation(settings.GetValue("collation", null) ?? command.Modifier("collation")?.Arg(0)), + Comment = comment, + BatchSize = settings.TryGetValue("batchSize", out BsonValue batch) && batch.IsNumeric ? batch.ToInt32() : null + }; + using IAsyncCursor cursor = await collection.AggregateAsync( + PipelineDefinition.Create(pipeline.Select(static s => s.AsBsonDocument)), + aggregateOptions, cancellationToken).ConfigureAwait(false); + (List docs, bool truncated) = await DrainAsync(cursor, options.MaxDocuments, cancellationToken).ConfigureAwait(false); + return Documents(command, database, name, docs, truncated) with { Pipeline = pipeline, ChangesCatalog = writes }; } - BsonDocument settings = method.Document(1) ?? []; - var aggregateOptions = new AggregateOptions - { - AllowDiskUse = settings.TryGetValue("allowDiskUse", out BsonValue disk) ? disk.ToBoolean() - : command.Modifier("allowDiskUse") is not null ? true : null, - MaxTime = settings.TryGetValue("maxTimeMS", out BsonValue max) && max.IsNumeric ? MaxTime(max.ToInt32()) : maxTime, - Hint = settings.GetValue("hint", null) ?? command.Modifier("hint")?.Arg(0), - Collation = Collation(settings.GetValue("collation", null) ?? command.Modifier("collation")?.Arg(0)), - Comment = comment, - BatchSize = settings.TryGetValue("batchSize", out BsonValue batch) && batch.IsNumeric ? batch.ToInt32() : null - }; - using IAsyncCursor cursor = await collection.AggregateAsync( - PipelineDefinition.Create(pipeline.Select(static s => s.AsBsonDocument)), - aggregateOptions, cancellationToken).ConfigureAwait(false); - (List docs, bool truncated) = await DrainAsync(cursor, options.MaxDocuments, cancellationToken).ConfigureAwait(false); - return Documents(command, database, name, docs, truncated) with { Pipeline = pipeline, ChangesCatalog = writes }; - } case "countDocuments": - { - BsonDocument settings = method.Document(1) ?? []; - var countOptions = new CountOptions { - Skip = settings.TryGetValue("skip", out BsonValue skip) && skip.IsNumeric ? skip.ToInt64() : null, - Limit = settings.TryGetValue("limit", out BsonValue limit) && limit.IsNumeric ? limit.ToInt64() : null, - Hint = settings.GetValue("hint", null), - MaxTime = maxTime, - Comment = comment - }; - long count = await collection.CountDocumentsAsync(Filter(method, 0), countOptions, cancellationToken).ConfigureAwait(false); - return Single(command, database, name, new BsonDocument("count", count)); - } + BsonDocument settings = method.Document(1) ?? []; + var countOptions = new CountOptions + { + Skip = settings.TryGetValue("skip", out BsonValue skip) && skip.IsNumeric ? skip.ToInt64() : null, + Limit = settings.TryGetValue("limit", out BsonValue limit) && limit.IsNumeric ? limit.ToInt64() : null, + Hint = settings.GetValue("hint", null), + MaxTime = maxTime, + Comment = comment + }; + long count = await collection.CountDocumentsAsync(Filter(method, 0), countOptions, cancellationToken).ConfigureAwait(false); + return Single(command, database, name, new BsonDocument("count", count)); + } case "estimatedDocumentCount": - { - long count = await collection.EstimatedDocumentCountAsync( - new EstimatedDocumentCountOptions { MaxTime = maxTime, Comment = comment }, cancellationToken).ConfigureAwait(false); - return Single(command, database, name, new BsonDocument("count", count)); - } + { + long count = await collection.EstimatedDocumentCountAsync( + new EstimatedDocumentCountOptions { MaxTime = maxTime, Comment = comment }, cancellationToken).ConfigureAwait(false); + return Single(command, database, name, new BsonDocument("count", count)); + } case "distinct": - { - string field = method.Arg(0) is BsonString f ? f.Value : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", "distinct")); - using IAsyncCursor cursor = await collection.DistinctAsync( - new StringFieldDefinition(field), Filter(method, 1), - new DistinctOptions { MaxTime = maxTime, Comment = comment }, cancellationToken).ConfigureAwait(false); - List values = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false); - bool truncated = values.Count > options.MaxDocuments; - return Documents(command, database, name, - [.. values.Take(options.MaxDocuments).Select(static v => new BsonDocument("value", v))], truncated); - } + { + string field = method.Arg(0) is BsonString f ? f.Value : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", "distinct")); + using IAsyncCursor cursor = await collection.DistinctAsync( + new StringFieldDefinition(field), Filter(method, 1), + new DistinctOptions { MaxTime = maxTime, Comment = comment }, cancellationToken).ConfigureAwait(false); + List values = await cursor.ToListAsync(cancellationToken).ConfigureAwait(false); + bool truncated = values.Count > options.MaxDocuments; + return Documents(command, database, name, + [.. values.Take(options.MaxDocuments).Select(static v => new BsonDocument("value", v))], truncated); + } case "insertOne" or "insert" when method.Arg(0) is BsonDocument document: - { - await collection.InsertOneAsync(document, new InsertOneOptions { Comment = comment }, cancellationToken).ConfigureAwait(false); - return Write(command, database, name, new BsonDocument { { "acknowledged", true }, { "insertedId", document.GetValue("_id", BsonNull.Value) } }, - loc.Format("Query_MsgInserted", 1)); - } + { + await collection.InsertOneAsync(document, new InsertOneOptions { Comment = comment }, cancellationToken).ConfigureAwait(false); + return Write(command, database, name, new BsonDocument { { "acknowledged", true }, { "insertedId", document.GetValue("_id", BsonNull.Value) } }, + loc.Format("Query_MsgInserted", 1)); + } case "insertOne": throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name)); case "insertMany" or "insert": - { - BsonArray array = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsArray", method.Name)); - List documents = [.. array.Select(static d => d.AsBsonDocument)]; - bool ordered = method.Document(1)?.GetValue("ordered", true).ToBoolean() ?? true; - await collection.InsertManyAsync(documents, new InsertManyOptions { IsOrdered = ordered, Comment = comment }, cancellationToken) - .ConfigureAwait(false); - return Write(command, database, name, - new BsonDocument { { "acknowledged", true }, { "insertedIds", new BsonArray(documents.Select(static d => d.GetValue("_id", BsonNull.Value))) } }, - loc.Format("Query_MsgInserted", documents.Count)); - } + { + BsonArray array = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsArray", method.Name)); + List documents = [.. array.Select(static d => d.AsBsonDocument)]; + bool ordered = method.Document(1)?.GetValue("ordered", true).ToBoolean() ?? true; + await collection.InsertManyAsync(documents, new InsertManyOptions { IsOrdered = ordered, Comment = comment }, cancellationToken) + .ConfigureAwait(false); + return Write(command, database, name, + new BsonDocument { { "acknowledged", true }, { "insertedIds", new BsonArray(documents.Select(static d => d.GetValue("_id", BsonNull.Value))) } }, + loc.Format("Query_MsgInserted", documents.Count)); + } case "updateOne" or "updateMany" or "update": - { - BsonDocument filter = Filter(method, 0); - BsonDocument settings = method.Document(2) ?? []; - bool many = method.Name == "updateMany" || method.Name == "update" && settings.GetValue("multi", false).ToBoolean(); - if (many && !await ConfirmAsync(ShellConfirmKind.UpdateMany, collection, database, name, filter, cancellationToken).ConfigureAwait(false)) { - return Declined(command, database, name); + BsonDocument filter = Filter(method, 0); + BsonDocument settings = method.Document(2) ?? []; + bool many = method.Name == "updateMany" || method.Name == "update" && settings.GetValue("multi", false).ToBoolean(); + if (many && !await ConfirmAsync(ShellConfirmKind.UpdateMany, collection, database, name, filter, cancellationToken).ConfigureAwait(false)) + { + return Declined(command, database, name); + } + UpdateDefinition update = Update(method.Arg(1), method.Name); + var updateOptions = new UpdateOptions + { + IsUpsert = settings.GetValue("upsert", false).ToBoolean(), + ArrayFilters = settings.GetValue("arrayFilters", null) is BsonArray filters + ? [.. filters.Select(static f => new BsonDocumentArrayFilterDefinition(f.AsBsonDocument))] + : null, + Hint = settings.GetValue("hint", null), + Collation = Collation(settings.GetValue("collation", null)), + Comment = comment + }; + UpdateResult result = many + ? await collection.UpdateManyAsync(filter, update, updateOptions, cancellationToken).ConfigureAwait(false) + : await collection.UpdateOneAsync(filter, update, updateOptions, cancellationToken).ConfigureAwait(false); + return Write(command, database, name, UpdateReply(result), loc.Format("Query_MsgUpdated", result.MatchedCount, result.ModifiedCount)); } - UpdateDefinition update = Update(method.Arg(1), method.Name); - var updateOptions = new UpdateOptions - { - IsUpsert = settings.GetValue("upsert", false).ToBoolean(), - ArrayFilters = settings.GetValue("arrayFilters", null) is BsonArray filters - ? [.. filters.Select(static f => new BsonDocumentArrayFilterDefinition(f.AsBsonDocument))] - : null, - Hint = settings.GetValue("hint", null), - Collation = Collation(settings.GetValue("collation", null)), - Comment = comment - }; - UpdateResult result = many - ? await collection.UpdateManyAsync(filter, update, updateOptions, cancellationToken).ConfigureAwait(false) - : await collection.UpdateOneAsync(filter, update, updateOptions, cancellationToken).ConfigureAwait(false); - return Write(command, database, name, UpdateReply(result), loc.Format("Query_MsgUpdated", result.MatchedCount, result.ModifiedCount)); - } case "replaceOne": - { - BsonDocument replacement = method.Arg(1) as BsonDocument ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", "replaceOne")); - BsonDocument settings = method.Document(2) ?? []; - ReplaceOneResult result = await collection.ReplaceOneAsync(Filter(method, 0), replacement, new ReplaceOptions { - IsUpsert = settings.GetValue("upsert", false).ToBoolean(), - Hint = settings.GetValue("hint", null), - Collation = Collation(settings.GetValue("collation", null)), - Comment = comment - }, cancellationToken).ConfigureAwait(false); - var reply = new BsonDocument + BsonDocument replacement = method.Arg(1) as BsonDocument ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", "replaceOne")); + BsonDocument settings = method.Document(2) ?? []; + ReplaceOneResult result = await collection.ReplaceOneAsync(Filter(method, 0), replacement, new ReplaceOptions + { + IsUpsert = settings.GetValue("upsert", false).ToBoolean(), + Hint = settings.GetValue("hint", null), + Collation = Collation(settings.GetValue("collation", null)), + Comment = comment + }, cancellationToken).ConfigureAwait(false); + var reply = new BsonDocument { { "acknowledged", result.IsAcknowledged }, { "matchedCount", result.IsAcknowledged ? result.MatchedCount : 0 }, { "modifiedCount", result.IsAcknowledged && result.IsModifiedCountAvailable ? result.ModifiedCount : 0 }, { "upsertedId", result.IsAcknowledged ? result.UpsertedId ?? BsonNull.Value : BsonNull.Value } }; - return Write(command, database, name, reply, loc.Format("Query_MsgUpdated", reply["matchedCount"], reply["modifiedCount"])); - } + return Write(command, database, name, reply, loc.Format("Query_MsgUpdated", reply["matchedCount"], reply["modifiedCount"])); + } case "deleteOne" or "deleteMany" or "remove": - { - BsonDocument filter = Filter(method, 0); - bool many = method.Name == "deleteMany" || method.Name == "remove" && method.Arg(1) is not BsonBoolean { Value: true } - && method.Document(1)?.GetValue("justOne", false).ToBoolean() != true; - if (many && !await ConfirmAsync(ShellConfirmKind.DeleteMany, collection, database, name, filter, cancellationToken).ConfigureAwait(false)) { - return Declined(command, database, name); + BsonDocument filter = Filter(method, 0); + bool many = method.Name == "deleteMany" || method.Name == "remove" && method.Arg(1) is not BsonBoolean { Value: true } + && method.Document(1)?.GetValue("justOne", false).ToBoolean() != true; + if (many && !await ConfirmAsync(ShellConfirmKind.DeleteMany, collection, database, name, filter, cancellationToken).ConfigureAwait(false)) + { + return Declined(command, database, name); + } + BsonDocument settings = method.Document(1) ?? []; + var deleteOptions = new DeleteOptions { Hint = settings.GetValue("hint", null), Collation = Collation(settings.GetValue("collation", null)), Comment = comment }; + DeleteResult result = many + ? await collection.DeleteManyAsync(filter, deleteOptions, cancellationToken).ConfigureAwait(false) + : await collection.DeleteOneAsync(filter, deleteOptions, cancellationToken).ConfigureAwait(false); + long deleted = result.IsAcknowledged ? result.DeletedCount : 0; + return Write(command, database, name, new BsonDocument { { "acknowledged", result.IsAcknowledged }, { "deletedCount", deleted } }, + loc.Format("Query_MsgDeleted", deleted)); } - BsonDocument settings = method.Document(1) ?? []; - var deleteOptions = new DeleteOptions { Hint = settings.GetValue("hint", null), Collation = Collation(settings.GetValue("collation", null)), Comment = comment }; - DeleteResult result = many - ? await collection.DeleteManyAsync(filter, deleteOptions, cancellationToken).ConfigureAwait(false) - : await collection.DeleteOneAsync(filter, deleteOptions, cancellationToken).ConfigureAwait(false); - long deleted = result.IsAcknowledged ? result.DeletedCount : 0; - return Write(command, database, name, new BsonDocument { { "acknowledged", result.IsAcknowledged }, { "deletedCount", deleted } }, - loc.Format("Query_MsgDeleted", deleted)); - } case "findOneAndUpdate" or "findOneAndReplace" or "findOneAndDelete": return await FindOneAndAsync(command, collection, database, name, maxTime, comment, cancellationToken).ConfigureAwait(false); @@ -504,75 +504,75 @@ private async Task CollectionAsync(ShellCommand command, string dat return await BulkWriteAsync(command, collection, database, name, comment, cancellationToken).ConfigureAwait(false); case "createIndex" or "createIndexes": - { - IEnumerable keys = method.Name == "createIndexes" - ? (method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsArray", method.Name))).Select(static k => k.AsBsonDocument) - : [method.Document(0) ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name))]; - BsonDocument indexOptions = method.Document(1) ?? []; - var indexes = new BsonArray(); - foreach (BsonDocument key in keys) - { - var spec = new BsonDocument { { "key", key }, { "name", indexOptions.GetValue("name", IndexName(key)) } }; - spec.Merge(indexOptions, overwriteExistingElements: false); - indexes.Add(spec); - } - var createIndexes = new BsonDocument { { "createIndexes", name }, { "indexes", indexes } }; - if (comment is not null) { - createIndexes["comment"] = comment; + IEnumerable keys = method.Name == "createIndexes" + ? (method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsArray", method.Name))).Select(static k => k.AsBsonDocument) + : [method.Document(0) ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name))]; + BsonDocument indexOptions = method.Document(1) ?? []; + var indexes = new BsonArray(); + foreach (BsonDocument key in keys) + { + var spec = new BsonDocument { { "key", key }, { "name", indexOptions.GetValue("name", IndexName(key)) } }; + _ = spec.Merge(indexOptions, overwriteExistingElements: false); + _ = indexes.Add(spec); + } + var createIndexes = new BsonDocument { { "createIndexes", name }, { "indexes", indexes } }; + if (comment is not null) + { + createIndexes["comment"] = comment; + } + BsonDocument reply = await connection.RunCommandAsync(database, createIndexes, cancellationToken).ConfigureAwait(false); + return Write(command, database, name, reply, loc.Format("Query_MsgIndexCreated", string.Join(", ", indexes.Select(static i => i["name"].ToString())))); } - BsonDocument reply = await connection.RunCommandAsync(database, createIndexes, cancellationToken).ConfigureAwait(false); - return Write(command, database, name, reply, loc.Format("Query_MsgIndexCreated", string.Join(", ", indexes.Select(static i => i["name"].ToString())))); - } case "dropIndex" or "dropIndexes": - { - BsonValue index = method.Arg(0) ?? (method.Name == "dropIndexes" ? "*" : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name))); - if (guard.ConfirmWrites && !await guard.ConfirmAsync(new ShellConfirmation(ShellConfirmKind.DropIndex, database, name, null, null, - index.IsString ? index.AsString : index.ToString())).ConfigureAwait(true)) { - return Declined(command, database, name); + BsonValue index = method.Arg(0) ?? (method.Name == "dropIndexes" ? "*" : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name))); + if (guard.ConfirmWrites && !await guard.ConfirmAsync(new ShellConfirmation(ShellConfirmKind.DropIndex, database, name, null, null, + index.IsString ? index.AsString : index.ToString())).ConfigureAwait(true)) + { + return Declined(command, database, name); + } + BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument { { "dropIndexes", name }, { "index", index } }, cancellationToken) + .ConfigureAwait(false); + return Write(command, database, name, reply, loc.Format("Query_MsgIndexDropped", index.IsString ? index.AsString : index.ToString())); } - BsonDocument reply = await connection.RunCommandAsync(database, new BsonDocument { { "dropIndexes", name }, { "index", index } }, cancellationToken) - .ConfigureAwait(false); - return Write(command, database, name, reply, loc.Format("Query_MsgIndexDropped", index.IsString ? index.AsString : index.ToString())); - } case "getIndexes": - { - IReadOnlyList indexes = await connection.ListIndexesAsync(database, name, cancellationToken).ConfigureAwait(false); - return Documents(command, database, name, [.. indexes], false); - } + { + IReadOnlyList indexes = await connection.ListIndexesAsync(database, name, cancellationToken).ConfigureAwait(false); + return Documents(command, database, name, [.. indexes], false); + } case "stats": - { - CollectionStats stats = await connection.GetStatsAsync(database, name, cancellationToken).ConfigureAwait(false); - return Single(command, database, name, stats.Raw); - } + { + CollectionStats stats = await connection.GetStatsAsync(database, name, cancellationToken).ConfigureAwait(false); + return Single(command, database, name, stats.Raw); + } case "drop": - { - if (guard.ConfirmWrites && !await guard.ConfirmAsync(new ShellConfirmation(ShellConfirmKind.Drop, database, name, null, - await connection.EstimatedCountAsync(database, name, cancellationToken).ConfigureAwait(true))).ConfigureAwait(true)) { - return Declined(command, database, name); + if (guard.ConfirmWrites && !await guard.ConfirmAsync(new ShellConfirmation(ShellConfirmKind.Drop, database, name, null, + await connection.EstimatedCountAsync(database, name, cancellationToken).ConfigureAwait(true))).ConfigureAwait(true)) + { + return Declined(command, database, name); + } + await connection.Database(database).DropCollectionAsync(name, cancellationToken).ConfigureAwait(false); + return Write(command, database, name, new BsonDocument("dropped", true), loc.Format("Query_MsgDropped", name)) with { ChangesCatalog = true }; } - await connection.Database(database).DropCollectionAsync(name, cancellationToken).ConfigureAwait(false); - return Write(command, database, name, new BsonDocument("dropped", true), loc.Format("Query_MsgDropped", name)) with { ChangesCatalog = true }; - } case "renameCollection": - { - string target = method.Arg(0) is BsonString t ? t.Value : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name)); - bool dropTarget = method.Arg(1) is BsonBoolean { Value: true }; - BsonDocument reply = await connection.RunCommandAsync("admin", new BsonDocument + { + string target = method.Arg(0) is BsonString t ? t.Value : throw new ShellExecutionException(loc.Format("Query_ErrNeedsName", method.Name)); + bool dropTarget = method.Arg(1) is BsonBoolean { Value: true }; + BsonDocument reply = await connection.RunCommandAsync("admin", new BsonDocument { { "renameCollection", $"{database}.{name}" }, { "to", $"{database}.{target}" }, { "dropTarget", dropTarget } }, cancellationToken).ConfigureAwait(false); - return Write(command, database, name, reply, loc.Format("Query_MsgRenamed", name, target)) with { ChangesCatalog = true }; - } + return Write(command, database, name, reply, loc.Format("Query_MsgRenamed", name, target)) with { ChangesCatalog = true }; + } default: throw new ShellExecutionException(loc.Format("Query_ErrUnsupportedMethod", method.Name)); @@ -656,13 +656,22 @@ private async Task FindOneAndAsync(ShellCommand command, IMongoColl BsonDocument settings = method.Document(method.Name == "findOneAndDelete" ? 1 : 2) ?? []; bool returnNew = settings.GetValue("returnNewDocument", false).ToBoolean() || settings.GetValue("returnDocument", "before").ToString() == "after"; - BsonDocument? projection = settings.GetValue("projection", null) as BsonDocument; - BsonDocument? sort = settings.GetValue("sort", null) as BsonDocument; - BsonDocument? document; - switch (method.Name) + var projection = settings.GetValue("projection", null) as BsonDocument; + var sort = settings.GetValue("sort", null) as BsonDocument; + BsonDocument? document = method.Name switch { - case "findOneAndUpdate": - document = await collection.FindOneAndUpdateAsync(filter, Update(method.Arg(1), method.Name), new FindOneAndUpdateOptions + "findOneAndUpdate" => await collection.FindOneAndUpdateAsync(filter, Update(method.Arg(1), method.Name), new FindOneAndUpdateOptions + { + ReturnDocument = returnNew ? ReturnDocument.After : ReturnDocument.Before, + IsUpsert = settings.GetValue("upsert", false).ToBoolean(), + Projection = projection is null ? null : new BsonDocumentProjectionDefinition(projection), + Sort = sort, + MaxTime = maxTime, + Comment = comment + }, cancellationToken).ConfigureAwait(false), + "findOneAndReplace" => await collection.FindOneAndReplaceAsync(filter, + method.Arg(1) as BsonDocument ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name)), + new FindOneAndReplaceOptions { ReturnDocument = returnNew ? ReturnDocument.After : ReturnDocument.Before, IsUpsert = settings.GetValue("upsert", false).ToBoolean(), @@ -670,31 +679,15 @@ private async Task FindOneAndAsync(ShellCommand command, IMongoColl Sort = sort, MaxTime = maxTime, Comment = comment - }, cancellationToken).ConfigureAwait(false); - break; - case "findOneAndReplace": - document = await collection.FindOneAndReplaceAsync(filter, - method.Arg(1) as BsonDocument ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsDocument", method.Name)), - new FindOneAndReplaceOptions - { - ReturnDocument = returnNew ? ReturnDocument.After : ReturnDocument.Before, - IsUpsert = settings.GetValue("upsert", false).ToBoolean(), - Projection = projection is null ? null : new BsonDocumentProjectionDefinition(projection), - Sort = sort, - MaxTime = maxTime, - Comment = comment - }, cancellationToken).ConfigureAwait(false); - break; - default: - document = await collection.FindOneAndDeleteAsync(filter, new FindOneAndDeleteOptions - { - Projection = projection is null ? null : new BsonDocumentProjectionDefinition(projection), - Sort = sort, - MaxTime = maxTime, - Comment = comment - }, cancellationToken).ConfigureAwait(false); - break; - } + }, cancellationToken).ConfigureAwait(false), + _ => await collection.FindOneAndDeleteAsync(filter, new FindOneAndDeleteOptions + { + Projection = projection is null ? null : new BsonDocumentProjectionDefinition(projection), + Sort = sort, + MaxTime = maxTime, + Comment = comment + }, cancellationToken).ConfigureAwait(false), + }; return Documents(command, database, name, document is null ? [] : [document], false) with { Message = document is null ? loc["Query_MsgNoMatch"] : loc.Format("Query_MsgDocs", 1) @@ -772,7 +765,7 @@ private async Task BulkWriteAsync(ShellCommand command, IMongoColle public async Task ExplainAsync(ShellCommand command, string database, string verbosity, ShellRunOptions options, BsonValue? hint, CancellationToken cancellationToken = default) { - Stopwatch watch = Stopwatch.StartNew(); + var watch = Stopwatch.StartNew(); BsonDocument inner = BuildExplainTarget(command, database, options, hint, loc); BsonDocument explain = await connection.RunCommandAsync(command.Database ?? database, new BsonDocument { { "explain", inner }, { "verbosity", verbosity } }, cancellationToken).ConfigureAwait(false); @@ -803,126 +796,126 @@ internal static BsonDocument BuildExplainTarget(ShellCommand command, string dat switch (method.Name) { case "find" or "findOne": - { - FindRequest request = BuildFindRequest(command, database, options.MaxTimeMs); - if (command.Modifier("count") is not null) { - target = new BsonDocument { { "count", name }, { "query", request.Filter } }; + FindRequest request = BuildFindRequest(command, database, options.MaxTimeMs); + if (command.Modifier("count") is not null) + { + target = new BsonDocument { { "count", name }, { "query", request.Filter } }; + break; + } + target = new BsonDocument { { "find", name }, { "filter", request.Filter } }; + if (request.Projection is { } projection) + { + target["projection"] = projection; + } + if (request.Sort is { } sort) + { + target["sort"] = sort; + } + if (request.Skip > 0) + { + target["skip"] = request.Skip; + } + if (request.Limit > 0) + { + target["limit"] = request.Limit; + } + if (method.Name == "findOne") + { + target["singleBatch"] = true; + } + if ((hint ?? request.Hint) is { } h) + { + target["hint"] = h; + } + if (request.Collation is { } collation) + { + target["collation"] = collation; + } break; } - target = new BsonDocument { { "find", name }, { "filter", request.Filter } }; - if (request.Projection is { } projection) - { - target["projection"] = projection; - } - if (request.Sort is { } sort) - { - target["sort"] = sort; - } - if (request.Skip > 0) - { - target["skip"] = request.Skip; - } - if (request.Limit > 0) - { - target["limit"] = request.Limit; - } - if (method.Name == "findOne") - { - target["singleBatch"] = true; - } - if ((hint ?? request.Hint) is { } h) - { - target["hint"] = h; - } - if (request.Collation is { } collation) - { - target["collation"] = collation; - } - break; - } case "aggregate": - { - BsonArray pipeline = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsPipeline", "aggregate")); - target = new BsonDocument { { "aggregate", name }, { "pipeline", pipeline }, { "cursor", new BsonDocument() } }; - BsonDocument settings = method.Document(1) ?? []; - if ((hint ?? settings.GetValue("hint", null) ?? command.Modifier("hint")?.Arg(0)) is { } h) - { - target["hint"] = h; - } - if (settings.GetValue("allowDiskUse", null) is { } disk) { - target["allowDiskUse"] = disk; + BsonArray pipeline = method.Arg(0) as BsonArray ?? throw new ShellExecutionException(loc.Format("Query_ErrNeedsPipeline", "aggregate")); + target = new BsonDocument { { "aggregate", name }, { "pipeline", pipeline }, { "cursor", new BsonDocument() } }; + BsonDocument settings = method.Document(1) ?? []; + if ((hint ?? settings.GetValue("hint", null) ?? command.Modifier("hint")?.Arg(0)) is { } h) + { + target["hint"] = h; + } + if (settings.GetValue("allowDiskUse", null) is { } disk) + { + target["allowDiskUse"] = disk; + } + break; } - break; - } case "countDocuments": - { - var pipeline = new BsonArray { new BsonDocument("$match", method.Document(0) ?? []) }; - BsonDocument settings = method.Document(1) ?? []; - if (settings.GetValue("skip", null) is { IsNumeric: true } skip) - { - pipeline.Add(new BsonDocument("$skip", skip)); - } - if (settings.GetValue("limit", null) is { IsNumeric: true } limit) - { - pipeline.Add(new BsonDocument("$limit", limit)); - } - pipeline.Add(new BsonDocument("$group", new BsonDocument { { "_id", 1 }, { "n", new BsonDocument("$sum", 1) } })); - target = new BsonDocument { { "aggregate", name }, { "pipeline", pipeline }, { "cursor", new BsonDocument() } }; - if ((hint ?? settings.GetValue("hint", null)) is { } h) { - target["hint"] = h; + var pipeline = new BsonArray { new BsonDocument("$match", method.Document(0) ?? []) }; + BsonDocument settings = method.Document(1) ?? []; + if (settings.GetValue("skip", null) is { IsNumeric: true } skip) + { + _ = pipeline.Add(new BsonDocument("$skip", skip)); + } + if (settings.GetValue("limit", null) is { IsNumeric: true } limit) + { + _ = pipeline.Add(new BsonDocument("$limit", limit)); + } + _ = pipeline.Add(new BsonDocument("$group", new BsonDocument { { "_id", 1 }, { "n", new BsonDocument("$sum", 1) } })); + target = new BsonDocument { { "aggregate", name }, { "pipeline", pipeline }, { "cursor", new BsonDocument() } }; + if ((hint ?? settings.GetValue("hint", null)) is { } h) + { + target["hint"] = h; + } + break; } - break; - } case "distinct": target = new BsonDocument { { "distinct", name }, { "key", method.Arg(0) ?? "" }, { "query", method.Document(1) ?? [] } }; break; case "updateOne" or "updateMany" or "replaceOne": - { - var update = new BsonDocument + { + var update = new BsonDocument { { "q", method.Document(0) ?? [] }, { "u", method.Arg(1) ?? new BsonDocument() }, { "multi", method.Name == "updateMany" }, { "upsert", method.Document(2)?.GetValue("upsert", false) ?? false } }; - if (hint is not null) - { - update["hint"] = hint; + if (hint is not null) + { + update["hint"] = hint; + } + target = new BsonDocument { { "update", name }, { "updates", new BsonArray { update } } }; + break; } - target = new BsonDocument { { "update", name }, { "updates", new BsonArray { update } } }; - break; - } case "deleteOne" or "deleteMany": - { - var delete = new BsonDocument { { "q", method.Document(0) ?? [] }, { "limit", method.Name == "deleteOne" ? 1 : 0 } }; - if (hint is not null) { - delete["hint"] = hint; + var delete = new BsonDocument { { "q", method.Document(0) ?? [] }, { "limit", method.Name == "deleteOne" ? 1 : 0 } }; + if (hint is not null) + { + delete["hint"] = hint; + } + target = new BsonDocument { { "delete", name }, { "deletes", new BsonArray { delete } } }; + break; } - target = new BsonDocument { { "delete", name }, { "deletes", new BsonArray { delete } } }; - break; - } case "findOneAndUpdate" or "findOneAndReplace" or "findOneAndDelete": - { - BsonDocument settings = method.Document(method.Name == "findOneAndDelete" ? 1 : 2) ?? []; - target = new BsonDocument { { "findAndModify", name }, { "query", method.Document(0) ?? [] } }; - if (method.Name == "findOneAndDelete") - { - target["remove"] = true; - } - else - { - target["update"] = method.Arg(1) ?? new BsonDocument(); - } - if (settings.GetValue("sort", null) is BsonDocument sort) { - target["sort"] = sort; + BsonDocument settings = method.Document(method.Name == "findOneAndDelete" ? 1 : 2) ?? []; + target = new BsonDocument { { "findAndModify", name }, { "query", method.Document(0) ?? [] } }; + if (method.Name == "findOneAndDelete") + { + target["remove"] = true; + } + else + { + target["update"] = method.Arg(1) ?? new BsonDocument(); + } + if (settings.GetValue("sort", null) is BsonDocument sort) + { + target["sort"] = sort; + } + break; } - break; - } default: throw new ShellExecutionException(loc.Format("Query_ErrExplainUnsupported", method.Name)); } diff --git a/plugins/VelaShell.Plugin.Mongo/Shell/ShellFormatter.cs b/plugins/VelaShell.Plugin.Mongo/Shell/ShellFormatter.cs index 27bdd7d..4a684f7 100644 --- a/plugins/VelaShell.Plugin.Mongo/Shell/ShellFormatter.cs +++ b/plugins/VelaShell.Plugin.Mongo/Shell/ShellFormatter.cs @@ -32,7 +32,7 @@ public static string Format(string text) string line = raw.Trim(); if (inBlockComment) { - output.Append(new string(' ', levels.Count * Unit.Length)).Append(line).Append('\n'); + _ = output.Append(new string(' ', levels.Count * Unit.Length)).Append(line).Append('\n'); if (line.Contains("*/", StringComparison.Ordinal)) { inBlockComment = false; @@ -43,7 +43,7 @@ public static string Format(string text) { if (++blankRun <= 1) { - output.Append('\n'); + _ = output.Append('\n'); } continue; } @@ -62,7 +62,7 @@ public static string Format(string text) } } int indent = temp.Count + (spaced.StartsWith('.') ? 1 : 0); - output.Append(string.Concat(Enumerable.Repeat(Unit, indent))).Append(spaced).Append('\n'); + _ = output.Append(string.Concat(Enumerable.Repeat(Unit, indent))).Append(spaced).Append('\n'); // 再按本行的括号更新层级:一行里新开的括号(不论几个)只算一级缩进。 int pending = 0; @@ -114,7 +114,7 @@ private static string Space(string line, out bool opensBlockComment, out IReadOn if (c is '"' or '\'') { int end = ShellScript.SkipString(line, i); - b.Append(line, i, end - i); + _ = b.Append(line, i, end - i); i = end; previous = c; sawContent = true; @@ -125,16 +125,16 @@ private static string Space(string line, out bool opensBlockComment, out IReadOn TrimSpace(b); if (b.Length > 0) { - b.Append(' '); + _ = b.Append(' '); } - b.Append(line, i, line.Length - i); + _ = b.Append(line, i, line.Length - i); break; } if (c == '/' && i + 1 < line.Length && line[i + 1] == '*') { int close = line.IndexOf("*/", i + 2, StringComparison.Ordinal); int end = close < 0 ? line.Length : close + 2; - b.Append(line, i, end - i); + _ = b.Append(line, i, end - i); opensBlockComment = close < 0; i = end; continue; @@ -142,7 +142,7 @@ private static string Space(string line, out bool opensBlockComment, out IReadOn if (c == '/' && ShellScript.IsRegexStart(previous)) { int end = ShellScript.SkipRegex(line, i); - b.Append(line, i, end - i); + _ = b.Append(line, i, end - i); i = end; previous = '/'; sawContent = true; @@ -152,7 +152,7 @@ private static string Space(string line, out bool opensBlockComment, out IReadOn { if (b.Length > 0 && b[^1] != ' ') { - b.Append(' '); + _ = b.Append(' '); } i++; continue; @@ -160,46 +160,46 @@ private static string Space(string line, out bool opensBlockComment, out IReadOn switch (c) { case '{': - b.Append('{'); + _ = b.Append('{'); // { 后一个空格(空对象 {} 除外)。 if (NextSignificant(line, i + 1) is var n && n != '}' && n != '\0') { - b.Append(' '); + _ = b.Append(' '); } break; case '}': TrimSpace(b); if (b.Length > 0 && b[^1] != '{' && b[^1] is not ('(' or '[')) { - b.Append(' '); + _ = b.Append(' '); } - b.Append('}'); + _ = b.Append('}'); break; case ':': TrimSpace(b); - b.Append(':'); + _ = b.Append(':'); if (i + 1 < line.Length) { - b.Append(' '); + _ = b.Append(' '); } break; case ',': TrimSpace(b); - b.Append(','); + _ = b.Append(','); if (i + 1 < line.Length && NextSignificant(line, i + 1) is not (']' or ')' or '\0')) { - b.Append(' '); + _ = b.Append(' '); } break; case ')' or ']': TrimSpace(b); - b.Append(c); + _ = b.Append(c); break; case '(' or '[': - b.Append(c); + _ = b.Append(c); break; default: - b.Append(c); + _ = b.Append(c); break; } if (c is '(' or '[' or '{' or ')' or ']' or '}') diff --git a/plugins/VelaShell.Plugin.Mongo/Staging/StagedChanges.cs b/plugins/VelaShell.Plugin.Mongo/Staging/StagedChanges.cs index 8e63e36..48711ca 100644 --- a/plugins/VelaShell.Plugin.Mongo/Staging/StagedChanges.cs +++ b/plugins/VelaShell.Plugin.Mongo/Staging/StagedChanges.cs @@ -78,11 +78,11 @@ public BsonDocument Apply() { if (change.Value is null) { - BsonPath.Unset(current, change.Path); + _ = BsonPath.Unset(current, change.Path); } else { - BsonPath.Set(current, change.Path, change.Value.DeepClone()); + _ = BsonPath.Set(current, change.Path, change.Value.DeepClone()); } } return current; @@ -133,7 +133,7 @@ public void Set(string path, BsonValue? value) Put(ancestor.Path, rebuilt, ancestorIndex); return; } - _changes.RemoveAll(c => IsAncestor(path, c.Path)); + _ = _changes.RemoveAll(c => IsAncestor(path, c.Path)); Put(path, value, -1); } @@ -180,7 +180,7 @@ public StagedEdit Clone() private void Put(string path, BsonValue? value, int index) { BsonValue? original = BsonPath.Get(Original, path); - _changes.RemoveAll(c => c.Path == path); + _ = _changes.RemoveAll(c => c.Path == path); if (SameValue(original, value)) { return; @@ -203,11 +203,11 @@ private static BsonValue Rebuild(BsonValue? ancestor, string relative, BsonValue var holder = new BsonDocument("v", start); if (value is null) { - BsonPath.Unset(holder, "v." + relative); + _ = BsonPath.Unset(holder, "v." + relative); } else { - BsonPath.Set(holder, "v." + relative, value.DeepClone()); + _ = BsonPath.Set(holder, "v." + relative, value.DeepClone()); } return holder["v"]; } diff --git a/plugins/VelaShell.Plugin.Mongo/Staging/StagingArea.cs b/plugins/VelaShell.Plugin.Mongo/Staging/StagingArea.cs index 5e1b52d..9549422 100644 --- a/plugins/VelaShell.Plugin.Mongo/Staging/StagingArea.cs +++ b/plugins/VelaShell.Plugin.Mongo/Staging/StagingArea.cs @@ -125,11 +125,11 @@ public void SetInsertField(StagedInsert insert, string path, BsonValue? value) { if (value is null) { - Bson.BsonPath.Unset(insert.Document, path); + _ = Bson.BsonPath.Unset(insert.Document, path); } else { - Bson.BsonPath.Set(insert.Document, path, value); + _ = Bson.BsonPath.Set(insert.Document, path, value); } Raise(); } @@ -163,7 +163,7 @@ public void MarkDeleted(BsonDocument original) } if (_edits.Remove(id, out StagedEdit? edit)) { - _editOrder.Remove(edit); + _ = _editOrder.Remove(edit); } var staged = new StagedDelete(id, (BsonDocument)(edit?.Original ?? original).DeepClone()); _deletes[id] = staged; @@ -176,7 +176,7 @@ public void UnmarkDeleted(BsonValue id) { if (_deletes.Remove(id, out StagedDelete? staged)) { - _deleteOrder.Remove(staged); + _ = _deleteOrder.Remove(staged); Raise(); } } @@ -187,12 +187,12 @@ public void Discard(BsonValue id) bool changed = false; if (_edits.Remove(id, out StagedEdit? edit)) { - _editOrder.Remove(edit); + _ = _editOrder.Remove(edit); changed = true; } if (_deletes.Remove(id, out StagedDelete? staged)) { - _deleteOrder.Remove(staged); + _ = _deleteOrder.Remove(staged); changed = true; } if (changed) @@ -235,18 +235,18 @@ internal void Remove(IEnumerable edits, IEnumerable in { if (_edits.Remove(edit.Id)) { - _editOrder.Remove(edit); + _ = _editOrder.Remove(edit); } } foreach (StagedInsert insert in inserts) { - _inserts.Remove(insert); + _ = _inserts.Remove(insert); } foreach (StagedDelete delete in deletes) { if (_deletes.Remove(delete.Id)) { - _deleteOrder.Remove(delete); + _ = _deleteOrder.Remove(delete); } } Raise(); @@ -287,7 +287,7 @@ private void Prune(StagedEdit edit) { if (edit.IsEmpty && _edits.Remove(edit.Id)) { - _editOrder.Remove(edit); + _ = _editOrder.Remove(edit); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/BsonDump.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/BsonDump.cs index b8d645f..2b861b7 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/BsonDump.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/BsonDump.cs @@ -36,7 +36,7 @@ public static BsonDocument Metadata(CollectionInfo info, IReadOnlyList /// 打开一个写出流(gzip 时套一层压缩)。 public static Stream OpenWrite(string path, bool gzip) { - Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _ = Directory.CreateDirectory(Path.GetDirectoryName(path)!); Stream file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20); return gzip ? new GZipStream(file, CompressionLevel.Fastest, leaveOpen: false) : file; } @@ -110,7 +110,7 @@ public static int Write(Stream stream, BsonDocument document) } int length = BinaryPrimitives.ReadInt32LittleEndian(prefix); // BSON 单文档上限 16 MB,留点余量;小于 5 字节的不可能是文档。 - if (length < 5 || length > 48 * 1024 * 1024) + if (length is < 5 or > (48 * 1024 * 1024)) { throw new InvalidDataException($"Invalid BSON document length {length}."); } @@ -144,7 +144,7 @@ public static long Count(string path, CancellationToken cancellationToken = defa { break; } - stream.Seek(length - 4, SeekOrigin.Current); + _ = stream.Seek(length - 4, SeekOrigin.Current); count++; } return count; diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/CellFormatter.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/CellFormatter.cs index 92a140e..04aa035 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/CellFormatter.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/CellFormatter.cs @@ -19,47 +19,31 @@ internal static class CellFormatter /// null 写成空(否则写 null)。 public static string Format(BsonValue? value, CellConversion conversion, bool nullAsEmpty) { - switch (BsonKinds.Of(value)) + return BsonKinds.Of(value) switch { - case BsonKind.Missing: - return ""; - case BsonKind.Null: - return nullAsEmpty ? "" : "null"; - case BsonKind.ObjectId: - return value!.AsObjectId.ToString(); - case BsonKind.String: - return value!.AsString; - case BsonKind.Int32: - return conversion == CellConversion.Fixed2 - ? value!.AsInt32.ToString("0.00", CultureInfo.InvariantCulture) - : value!.AsInt32.ToString(CultureInfo.InvariantCulture); - case BsonKind.Int64: - return conversion == CellConversion.Fixed2 - ? value!.AsInt64.ToString("0.00", CultureInfo.InvariantCulture) - : value!.AsInt64.ToString(CultureInfo.InvariantCulture); - case BsonKind.Double: - return conversion == CellConversion.Fixed2 && double.IsFinite(value!.AsDouble) - ? value.AsDouble.ToString("0.00", CultureInfo.InvariantCulture) - : BsonText.FormatDouble(value!.AsDouble, forceDecimalPoint: false); - case BsonKind.Decimal128: - return FormatDecimal(value!.AsDecimal128, conversion == CellConversion.Fixed2); - case BsonKind.Boolean: - return value!.AsBoolean ? "true" : "false"; - case BsonKind.Date: - return conversion == CellConversion.IsoTime ? BsonText.IsoDate(value!) : LocalDate(value!); - case BsonKind.Object or BsonKind.Array: - return BsonText.Compact(value!, EjsonMode.Relaxed); - case BsonKind.Uuid: - return value!.AsBsonBinaryData.ToGuid().ToString(); - case BsonKind.Binary: - return Convert.ToBase64String(value!.AsBsonBinaryData.Bytes); - case BsonKind.Regex: - return value!.AsBsonRegularExpression.ToString(); - case BsonKind.Timestamp: - return $"Timestamp({value!.AsBsonTimestamp.Timestamp}, {value.AsBsonTimestamp.Increment})"; - default: - return BsonText.Literal(value!); - } + BsonKind.Missing => "", + BsonKind.Null => nullAsEmpty ? "" : "null", + BsonKind.ObjectId => value!.AsObjectId.ToString(), + BsonKind.String => value!.AsString, + BsonKind.Int32 => conversion == CellConversion.Fixed2 + ? value!.AsInt32.ToString("0.00", CultureInfo.InvariantCulture) + : value!.AsInt32.ToString(CultureInfo.InvariantCulture), + BsonKind.Int64 => conversion == CellConversion.Fixed2 + ? value!.AsInt64.ToString("0.00", CultureInfo.InvariantCulture) + : value!.AsInt64.ToString(CultureInfo.InvariantCulture), + BsonKind.Double => conversion == CellConversion.Fixed2 && double.IsFinite(value!.AsDouble) + ? value.AsDouble.ToString("0.00", CultureInfo.InvariantCulture) + : BsonText.FormatDouble(value!.AsDouble, forceDecimalPoint: false), + BsonKind.Decimal128 => FormatDecimal(value!.AsDecimal128, conversion == CellConversion.Fixed2), + BsonKind.Boolean => value!.AsBoolean ? "true" : "false", + BsonKind.Date => conversion == CellConversion.IsoTime ? BsonText.IsoDate(value!) : LocalDate(value!), + BsonKind.Object or BsonKind.Array => BsonText.Compact(value!, EjsonMode.Relaxed), + BsonKind.Uuid => value!.AsBsonBinaryData.ToGuid().ToString(), + BsonKind.Binary => Convert.ToBase64String(value!.AsBsonBinaryData.Bytes), + BsonKind.Regex => value!.AsBsonRegularExpression.ToString(), + BsonKind.Timestamp => $"Timestamp({value!.AsBsonTimestamp.Timestamp}, {value.AsBsonTimestamp.Increment})", + _ => BsonText.Literal(value!), + }; } /// Decimal128 → 文本; 时四舍五入到两位(远离零)。 @@ -79,7 +63,7 @@ public static string FormatDecimal(Decimal128 value, bool fixed2) public static string LocalDate(BsonValue value) { long ms = value.AsBsonDateTime.MillisecondsSinceEpoch; - if (ms < -62135596800000L || ms > 253402300799999L) + if (ms is < -62135596800000L or > 253402300799999L) { return ms.ToString(CultureInfo.InvariantCulture); } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/CollectionCopier.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/CollectionCopier.cs index 50b43a0..ec2cc92 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/CollectionCopier.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/CollectionCopier.cs @@ -107,7 +107,7 @@ public void Resume() paused = _paused; _paused = null; } - paused?.TrySetResult(); + _ = (paused?.TrySetResult()); } /// 暂停中就等到恢复(或取消)。 @@ -165,14 +165,14 @@ public static async Task CopyAsync( await CopyViewAsync(target, targetDb, item, cancellationToken).ConfigureAwait(false); return new(0, 0, 0); case XferObjectKind.Bucket: - { - // 先 chunks 后 files:读者从 files 找文件,files 晚到就不会看见"有文件没数据"的半截桶。 - XferOutcome chunks = await CopyCollectionAsync(source, sourceDb, target, targetDb, $"{item.Name}.chunks", - new BsonDocument(), item.Action, options, gate, onDocuments, log, loc, cancellationToken).ConfigureAwait(false); - XferOutcome files = await CopyCollectionAsync(source, sourceDb, target, targetDb, $"{item.Name}.files", - new BsonDocument(), item.Action, options, gate, onDocuments, log, loc, cancellationToken).ConfigureAwait(false); - return new(chunks.Copied + files.Copied, chunks.Failed + files.Failed, chunks.Indexes + files.Indexes); - } + { + // 先 chunks 后 files:读者从 files 找文件,files 晚到就不会看见"有文件没数据"的半截桶。 + XferOutcome chunks = await CopyCollectionAsync(source, sourceDb, target, targetDb, $"{item.Name}.chunks", + [], item.Action, options, gate, onDocuments, log, loc, cancellationToken).ConfigureAwait(false); + XferOutcome files = await CopyCollectionAsync(source, sourceDb, target, targetDb, $"{item.Name}.files", + [], item.Action, options, gate, onDocuments, log, loc, cancellationToken).ConfigureAwait(false); + return new(chunks.Copied + files.Copied, chunks.Failed + files.Failed, chunks.Indexes + files.Indexes); + } default: return await CopyCollectionAsync(source, sourceDb, target, targetDb, item.Name, item.Info?.Options ?? [], item.Action, options, gate, onDocuments, log, loc, cancellationToken).ConfigureAwait(false); @@ -239,14 +239,14 @@ async Task InsertAsync(List documents) try { await to.InsertManyAsync(documents, new InsertManyOptions { IsOrdered = false }, cancellationToken).ConfigureAwait(false); - Interlocked.Add(ref copied, documents.Count); + _ = Interlocked.Add(ref copied, documents.Count); onDocuments(documents.Count); } catch (MongoBulkWriteException ex) { long bad = ex.WriteErrors.Count; - Interlocked.Add(ref copied, documents.Count - bad); - Interlocked.Add(ref failed, bad); + _ = Interlocked.Add(ref copied, documents.Count - bad); + _ = Interlocked.Add(ref failed, bad); onDocuments(documents.Count); } catch (Exception ex) when (ex is MongoException or TimeoutException) @@ -258,7 +258,7 @@ async Task InsertAsync(List documents) } finally { - slots.Release(); + _ = slots.Release(); } } @@ -282,8 +282,8 @@ async Task InsertAsync(List documents) await gate.WaitAsync(cancellationToken).ConfigureAwait(false); await slots.WaitAsync(cancellationToken).ConfigureAwait(false); inflight.Add(InsertAsync(buffer)); - buffer = new List(options.BatchSize); - inflight.RemoveAll(static t => t.IsCompleted); + buffer = [with(options.BatchSize)]; + _ = inflight.RemoveAll(static t => t.IsCompleted); ThrowIfFailed(errors); } } @@ -349,30 +349,30 @@ internal static BsonDocument CreateCommand(string name, BsonDocument sourceOptio case "viewOn" or "pipeline": continue; case "timeseries" when element.Value is BsonDocument series: - { - BsonDocument copy = series.DeepClone().AsBsonDocument; - if (copy.Contains("granularity")) { - copy.Remove("bucketMaxSpanSeconds"); - copy.Remove("bucketRoundingSeconds"); + BsonDocument copy = series.DeepClone().AsBsonDocument; + if (copy.Contains("granularity")) + { + copy.Remove("bucketMaxSpanSeconds"); + copy.Remove("bucketRoundingSeconds"); + } + command[element.Name] = copy; + continue; } - command[element.Name] = copy; - continue; - } case "clusteredIndex" when element.Value is BsonDocument clustered: - { - var copy = new BsonDocument + { + var copy = new BsonDocument { { "key", clustered.GetValue("key", new BsonDocument("_id", 1)) }, { "unique", clustered.GetValue("unique", true) } }; - if (clustered.TryGetValue("name", out BsonValue clusteredName)) - { - copy["name"] = clusteredName; + if (clustered.TryGetValue("name", out BsonValue clusteredName)) + { + copy["name"] = clusteredName; + } + command[element.Name] = copy; + continue; } - command[element.Name] = copy; - continue; - } default: command[element.Name] = element.Value; continue; @@ -407,7 +407,7 @@ private static async Task CopyViewAsync(MongoConnection target, string database, { command["collation"] = collation; } - await target.RunCommandAsync(database, command, cancellationToken).ConfigureAwait(false); + _ = await target.RunCommandAsync(database, command, cancellationToken).ConfigureAwait(false); } private static async Task CopyIndexesAsync( @@ -431,7 +431,7 @@ private static async Task CopyIndexesAsync( BsonDocument spec = index.DeepClone().AsBsonDocument; spec.Remove("ns"); spec.Remove("v"); - specs.Add(spec); + _ = specs.Add(spec); } if (specs.Count == 0) { @@ -439,7 +439,7 @@ private static async Task CopyIndexesAsync( } try { - await target.RunCommandAsync(targetDb, new BsonDocument { { "createIndexes", name }, { "indexes", specs } }, cancellationToken) + _ = await target.RunCommandAsync(targetDb, new BsonDocument { { "createIndexes", name }, { "indexes", specs } }, cancellationToken) .ConfigureAwait(false); return specs.Count; } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/Csv.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/Csv.cs index 4b006d4..c1cafde 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/Csv.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/Csv.cs @@ -136,7 +136,7 @@ private bool ReadRecord(List fields, out bool blank) return false; } RecordLine = _line; - _field.Clear(); + _ = _field.Clear(); bool quoted = false; bool fieldStart = true; bool sawQuote = false; @@ -157,7 +157,7 @@ private bool ReadRecord(List fields, out bool blank) if (Peek() == '"') { _position++; - _field.Append('"'); + _ = _field.Append('"'); } else { @@ -170,7 +170,7 @@ private bool ReadRecord(List fields, out bool blank) { _line++; } - _field.Append(c); + _ = _field.Append(c); } continue; } @@ -184,11 +184,11 @@ private bool ReadRecord(List fields, out bool blank) if (c == _delimiter) { fields.Add(_field.ToString()); - _field.Clear(); + _ = _field.Clear(); fieldStart = true; continue; } - if (c == '\r' || c == '\n') + if (c is '\r' or '\n') { if (c == '\r' && Peek() == '\n') { @@ -200,7 +200,7 @@ private bool ReadRecord(List fields, out bool blank) return true; } fieldStart = false; - _field.Append(c); + _ = _field.Append(c); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ExportRunner.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ExportRunner.cs index 0d57a6d..e6b2ecc 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ExportRunner.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ExportRunner.cs @@ -101,7 +101,7 @@ public static async Task RunAsync( } else if (job.TargetIsFolder) { - Directory.CreateDirectory(job.Target); + _ = Directory.CreateDirectory(job.Target); foreach (ExportSource source in job.Sources) { string path = Path.Combine(job.Target, TransferText.SafeFileName(source.Info.Name) + ExportJob.Extension(job.Format, job.Json)); @@ -138,7 +138,7 @@ private static async Task WriteFileAsync( CancellationToken cancellationToken) { string directory = Path.GetDirectoryName(Path.GetFullPath(path))!; - Directory.CreateDirectory(directory); + _ = Directory.CreateDirectory(directory); string part = path + ".part"; created.Add(part); var counting = new CountingStream(new FileStream(part, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 20)); @@ -159,7 +159,7 @@ await StreamAsync(connection.Collection(job.Database, source.Info.Name), job, wr } state.Detach(); File.Move(part, path, overwrite: true); - created.Remove(part); + _ = created.Remove(part); } private static async Task DumpAsync( diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ExportWriters.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ExportWriters.cs index 1a2931c..b913471 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ExportWriters.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ExportWriters.cs @@ -325,14 +325,14 @@ public override long Position public override void Write(byte[] buffer, int offset, int count) { inner.Write(buffer, offset, count); - Interlocked.Add(ref _written, count); + _ = Interlocked.Add(ref _written, count); } /// public override void Write(ReadOnlySpan buffer) { inner.Write(buffer); - Interlocked.Add(ref _written, buffer.Length); + _ = Interlocked.Add(ref _written, buffer.Length); } /// diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportPlan.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportPlan.cs index 73144bc..9d5b196 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportPlan.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportPlan.cs @@ -175,7 +175,7 @@ public ConvertedRow Convert(ImportRecord record) { var issues = new List(); var values = new BsonValue?[_included.Length]; - var raw = new string?[_included.Length]; + string?[] raw = new string?[_included.Length]; if (record.Error is { } parseError) { issues.Add(new(ImportIssueKind.Parse, true, "", -1, parseError)); @@ -261,7 +261,7 @@ private void Apply(BsonDocument document, ImportColumn column, int index, Conver } } values[index] = value; - BsonPath.Set(document, target, value); + _ = BsonPath.Set(document, target, value); } private void Check(BsonDocument document, List issues) @@ -315,8 +315,8 @@ public static IReadOnlyList Group(IEnumerable ro { foreach (ImportIssue issue in row.Issues.DistinctBy(static i => (i.Kind, i.Field, i.IsError))) { - var key = (issue.Kind, issue.Field, issue.IsError); - if (groups.TryGetValue(key, out var existing)) + (ImportIssueKind Kind, string Field, bool IsError) key = (issue.Kind, issue.Field, issue.IsError); + if (groups.TryGetValue(key, out (int Rows, long First, string Detail) existing)) { groups[key] = (existing.Rows + 1, existing.First, existing.Detail); } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportReaders.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportReaders.cs index a44d0e1..0345c97 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportReaders.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportReaders.cs @@ -99,27 +99,27 @@ public long CountRecords(CancellationToken cancellationToken) case ImportFormat.Bson: return BsonDump.Count(Path, cancellationToken); case ImportFormat.Json: - { - using FileStream stream = File.OpenRead(Path); - using StreamReader reader = TextEncodings.OpenReader(stream, Encoding); - var scanner = new JsonDocumentScanner(reader); - long count = 0; - while (scanner.Skip()) { - if ((++count & 0xFFF) == 0) + using FileStream stream = File.OpenRead(Path); + using StreamReader reader = TextEncodings.OpenReader(stream, Encoding); + var scanner = new JsonDocumentScanner(reader); + long count = 0; + while (scanner.Skip()) { - cancellationToken.ThrowIfCancellationRequested(); + if ((++count & 0xFFF) == 0) + { + cancellationToken.ThrowIfCancellationRequested(); + } } + return count; } - return count; - } default: - { - using FileStream stream = File.OpenRead(Path); - using StreamReader reader = TextEncodings.OpenReader(stream, Encoding); - long records = CsvReader.CountRecords(reader, cancellationToken); - return HasHeader ? Math.Max(0, records - 1) : records; - } + { + using FileStream stream = File.OpenRead(Path); + using StreamReader reader = TextEncodings.OpenReader(stream, Encoding); + long records = CsvReader.CountRecords(reader, cancellationToken); + return HasHeader ? Math.Max(0, records - 1) : records; + } } } } @@ -148,7 +148,7 @@ internal sealed class CsvImportReader : IImportReader private readonly StreamReader _text; private readonly CsvReader _csv; private string[]? _pending; - private long _pendingLine; + private readonly long _pendingLine; /// 构造(读掉表头)。 public CsvImportReader(ImportSource source) @@ -300,7 +300,7 @@ private bool Scan(bool capture, out string json, out long line) { json = ""; line = 0; - _current.Clear(); + _ = _current.Clear(); int depth = 0; bool inString = false; bool escape = false; @@ -330,14 +330,14 @@ private bool Scan(bool capture, out string json, out long line) line = _line; if (capture) { - _current.Append(c); + _ = _current.Append(c); } } continue; } if (capture) { - _current.Append(c); + _ = _current.Append(c); } if (inString) { diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportRunner.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportRunner.cs index 46eecd3..095faed 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportRunner.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportRunner.cs @@ -278,7 +278,7 @@ public void Error(long line, IReadOnlyList reasons, ImportRecord record, if (_errors is null) { ErrorPath = Path.Combine(job.ReportDirectory, $"{Path.GetFileNameWithoutExtension(job.Source.Path)}.errors-{_stamp}.jsonl"); - Directory.CreateDirectory(job.ReportDirectory); + _ = Directory.CreateDirectory(job.ReportDirectory); _errors = new StreamWriter(ErrorPath, append: false, new UTF8Encoding(false)); } var entry = new BsonDocument @@ -311,7 +311,7 @@ public void Backup(BsonDocument document) if (_backup is null) { BackupPath = Path.Combine(job.ReportDirectory, $"{job.Database}.{job.Collection}.backup-{_stamp}.jsonl"); - Directory.CreateDirectory(job.ReportDirectory); + _ = Directory.CreateDirectory(job.ReportDirectory); _backup = new StreamWriter(BackupPath, append: false, new UTF8Encoding(false)); } // Canonical:备份是要能原样恢复的,类型一个都不能丢(Int64 / Decimal128 / 日期)。 diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportValues.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportValues.cs index b792843..71da425 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/ImportValues.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/ImportValues.cs @@ -101,19 +101,19 @@ public static ConvertOutcome FromText(string text, BsonKind kind, string? dateFo ? ConvertOutcome.Ok(new BsonDateTime(utc)) : ConvertOutcome.Fail(ImportIssueKind.BadDate, GuessDateFormat(trimmed, includeTime: false) ?? trimmed); case BsonKind.ObjectId: - { - string hex = Unwrap(trimmed, "ObjectId"); - return ObjectId.TryParse(hex, out ObjectId id) - ? ConvertOutcome.Ok(id) - : ConvertOutcome.Fail(ImportIssueKind.BadValue, trimmed); - } + { + string hex = Unwrap(trimmed, "ObjectId"); + return ObjectId.TryParse(hex, out ObjectId id) + ? ConvertOutcome.Ok(id) + : ConvertOutcome.Fail(ImportIssueKind.BadValue, trimmed); + } case BsonKind.Uuid: - { - string guid = Unwrap(trimmed, "UUID"); - return Guid.TryParse(guid, out Guid g) - ? ConvertOutcome.Ok(new BsonBinaryData(g, GuidRepresentation.Standard)) - : ConvertOutcome.Fail(ImportIssueKind.BadValue, trimmed); - } + { + string guid = Unwrap(trimmed, "UUID"); + return Guid.TryParse(guid, out Guid g) + ? ConvertOutcome.Ok(new BsonBinaryData(g, GuidRepresentation.Standard)) + : ConvertOutcome.Fail(ImportIssueKind.BadValue, trimmed); + } case BsonKind.Object or BsonKind.Array: try { @@ -248,7 +248,7 @@ internal static string CleanNumber(string text, out bool grouping) case '¥' or '¥' or '$' or '€' or '£': break; default: - builder.Append(c); + _ = builder.Append(c); break; } } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/SchemaSampler.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/SchemaSampler.cs index bb7521b..99f6f7e 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/SchemaSampler.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/SchemaSampler.cs @@ -98,7 +98,7 @@ public static async Task> SampleAsync( } catch (MongoCommandException) { - return await collection.Find(filter ?? new BsonDocument()).Limit(size).ToListAsync(cancellationToken).ConfigureAwait(false); + return await collection.Find(filter ?? []).Limit(size).ToListAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/TransferModels.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/TransferModels.cs index 6070815..c7b2c42 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/TransferModels.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/TransferModels.cs @@ -243,7 +243,7 @@ public static string LocalOffset() public static string SafeFileName(string name) { char[] invalid = Path.GetInvalidFileNameChars(); - var chars = name.Select(c => invalid.Contains(c) ? '_' : c).ToArray(); + char[] chars = name.Select(c => invalid.Contains(c) ? '_' : c).ToArray(); string safe = new string(chars).Trim(); return safe.Length == 0 ? "_" : safe; } diff --git a/plugins/VelaShell.Plugin.Mongo/Transfer/XlsxWriter.cs b/plugins/VelaShell.Plugin.Mongo/Transfer/XlsxWriter.cs index a8bb4a6..9b54c49 100644 --- a/plugins/VelaShell.Plugin.Mongo/Transfer/XlsxWriter.cs +++ b/plugins/VelaShell.Plugin.Mongo/Transfer/XlsxWriter.cs @@ -59,7 +59,6 @@ internal sealed class XlsxWriter : IDisposable private readonly bool _freezeHeader; private Stream? _sheetStream; private XmlWriter? _sheet; - private int _row; private bool _completed; /// 构造。 @@ -72,7 +71,7 @@ public XlsxWriter(Stream output, bool freezeHeader) } /// 当前表已写的行数。 - public int RowsInSheet => _row; + public int RowsInSheet { get; private set; } /// 已建的表数。 public int SheetCount => _sheets.Count; @@ -127,7 +126,7 @@ public void BeginSheet(string name, IReadOnlyList? columnWidths = null) _sheet.WriteEndElement(); } _sheet.WriteStartElement("sheetData", MainNs); - _row = 0; + RowsInSheet = 0; } /// 写一行。 @@ -139,12 +138,12 @@ public void WriteRow(IReadOnlyList cells, bool header = false) { throw new InvalidOperationException("BeginSheet first."); } - if (_row >= MaxRows) + if (RowsInSheet >= MaxRows) { throw new InvalidOperationException("The sheet is full."); } - _row++; - string rowRef = _row.ToString(CultureInfo.InvariantCulture); + RowsInSheet++; + string rowRef = RowsInSheet.ToString(CultureInfo.InvariantCulture); _sheet.WriteStartElement("row", MainNs); _sheet.WriteAttributeString("r", rowRef); for (int i = 0; i < cells.Count; i++) @@ -284,7 +283,7 @@ public static string ColumnName(int index) while (n > 0) { int rem = (n - 1) % 26; - name.Insert(0, (char)('A' + rem)); + _ = name.Insert(0, (char)('A' + rem)); n = (n - 1) / 26; } return name.ToString(); @@ -293,7 +292,7 @@ public static string ColumnName(int index) /// 工作表名:去掉 []:*?/\,最长 31 个字符,不能为空。 public static string SanitizeSheetName(string name) { - var chars = name.Where(static c => c is not ('[' or ']' or ':' or '*' or '?' or '/' or '\\') && !char.IsControl(c)).ToArray(); + char[] chars = name.Where(static c => c is not ('[' or ']' or ':' or '*' or '?' or '/' or '\\') && !char.IsControl(c)).ToArray(); string clean = new string(chars).Trim('\'').Trim(); if (clean.Length == 0) { @@ -345,19 +344,19 @@ private static string Clean(string text) char c = text[i]; if (char.IsHighSurrogate(c) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1])) { - builder.Append(c).Append(text[i + 1]); + _ = builder.Append(c).Append(text[i + 1]); i++; } else if (IsXmlChar(c) && !char.IsSurrogate(c)) { - builder.Append(c); + _ = builder.Append(c); } } return builder.ToString(); } private static bool IsXmlChar(char c) => - c == '\t' || c == '\n' || c == '\r' || (c >= 0x20 && c <= 0xFFFD && c is not '￾' and not '￿'); + c is '\t' or '\n' or '\r' or >= (char)0x20 and <= (char)0xFFFD and not '￾' and not '￿'; private static XmlWriterSettings XmlSettings() => new() { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/AddStagedFieldDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/AddStagedFieldDialogViewModel.cs index c43f459..36b4e6b 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/AddStagedFieldDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/AddStagedFieldDialogViewModel.cs @@ -27,10 +27,6 @@ internal sealed class AddStagedFieldDialogViewModel : DialogViewModel, IViewFact private readonly BsonDocument _document; private readonly string _parent; private readonly Func _stage; - private string _name = ""; - private ValueKindChoice _kind = ValueKindChoice.All[0]; - private string _valueText = ""; - private string? _error; /// 构造。 /// 外壳服务。 @@ -57,15 +53,15 @@ public AddStagedFieldDialogViewModel(IMongoWorkspace workspace, BsonDocument doc /// 字段名。 public string Name { - get => _name; + get; set { - if (SetProperty(ref _name, value ?? "")) + if (SetProperty(ref field, value ?? "")) { Error = null; } } - } + } = ""; /// 可选类型。 public IReadOnlyList Kinds => ValueKindChoice.All; @@ -73,40 +69,40 @@ public string Name /// 选中的类型。 public ValueKindChoice Kind { - get => _kind; + get; set { - if (SetProperty(ref _kind, value ?? ValueKindChoice.All[0])) + if (SetProperty(ref field, value ?? ValueKindChoice.All[0])) { Error = null; RaisePropertyChanged(nameof(ValueHint)); } } - } + } = ValueKindChoice.All[0]; /// 值(按类型解析;空 = 该类型的空值)。 public string ValueText { - get => _valueText; + get; set { - if (SetProperty(ref _valueText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { Error = null; } } - } + } = ""; /// 值框的占位提示。 - public string ValueHint => Loc.Format("Cw_AddFieldValueHint", BsonText.Literal(BsonEdit.Empty(_kind.Kind))); + public string ValueHint => Loc.Format("Cw_AddFieldValueHint", BsonText.Literal(BsonEdit.Empty(Kind.Kind))); /// 错误。 public string? Error { - get => _error; + get; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } @@ -114,7 +110,7 @@ private set } /// 有错。 - public bool HasError => _error is not null; + public bool HasError => Error is not null; /// 添加。 public RelayCommand ConfirmCommand { get; } @@ -124,7 +120,7 @@ private set private void Confirm() { - string name = _name.Trim(); + string name = Name.Trim(); if (name.Length == 0 || name.StartsWith('$') || name.Contains('.')) { Error = Loc["Cw_AddFieldBadName"]; @@ -137,11 +133,11 @@ private void Confirm() return; } BsonValue value; - if (_valueText.Length == 0) + if (ValueText.Length == 0) { - value = BsonEdit.Empty(_kind.Kind); + value = BsonEdit.Empty(Kind.Kind); } - else if (!BsonEdit.TryParse(_valueText, _kind.Kind, out value, out string? error)) + else if (!BsonEdit.TryParse(ValueText, Kind.Kind, out value, out string? error)) { Error = Loc[error ?? "Edit_BadLiteral"]; return; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionMenus.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionMenus.cs index c1906fc..0863198 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionMenus.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionMenus.cs @@ -134,7 +134,7 @@ public static void Open(Control target, IEnumerable items) private static Control Header(string label, string? hint, bool danger) { - var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto"), MinWidth = 190 }; + var grid = new Grid { ColumnDefinitions = [with("*,Auto")], MinWidth = 190 }; var text = new TextBlock { Text = label, VerticalAlignment = VerticalAlignment.Center }; if (danger) { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionSample.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionSample.cs index d995852..76f2393 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionSample.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionSample.cs @@ -23,7 +23,7 @@ internal sealed record CollectionSampleField(string Path, BsonKind Kind, int Cou internal sealed class CollectionSample { private readonly IReadOnlyList _documents; - private readonly Dictionary> _valueCache = new(StringComparer.Ordinal); + private readonly Dictionary> _valueCache = [with(StringComparer.Ordinal)]; private CollectionSample(IReadOnlyList documents) { @@ -65,7 +65,7 @@ private CollectionSample(IReadOnlyList documents) /// 抽一批。视图与时序集合同样支持 $sample。 public static async Task LoadAsync(MongoConnection connection, string database, string collection, int size, CancellationToken cancellationToken) { - var pipeline = new[] { new BsonDocument("$sample", new BsonDocument("size", Math.Clamp(size, 50, 5000))) }; + BsonDocument[] pipeline = new[] { new BsonDocument("$sample", new BsonDocument("size", Math.Clamp(size, 50, 5000))) }; using IAsyncCursor cursor = await connection.Collection(database, collection) .AggregateAsync(pipeline, new AggregateOptions { MaxTime = TimeSpan.FromSeconds(10) }, cancellationToken) .ConfigureAwait(false); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Drill.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Drill.cs index 2f31cba..e91daf0 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Drill.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Drill.cs @@ -15,30 +15,28 @@ namespace VelaShell.Plugin.Mongo.Ui; /// internal sealed partial class CollectionTabViewModel { - private GridDrill? _drill; private ObservableCollection _drillRows = []; private CollectionRow? _drillSelected; private IReadOnlyList _drillSelectedRows = []; private readonly List _drillHistory = []; - private IReadOnlyList _drillCrumbs = []; private bool _swappingGrid; /// 正在看某个对象 / 数组的子表。 - public bool IsDrilled => _drill is not null; + public bool IsDrilled => Drill is not null; /// 当前这一层;没钻入为 。 - public GridDrill? Drill => _drill; + public GridDrill? Drill { get; private set; } /// 网格的列。 - public IReadOnlyList GridColumns => _drill?.Columns ?? _columns; + public IReadOnlyList GridColumns => Drill?.Columns ?? _columns; /// 网格的行。 - public ObservableCollection GridRows => _drill is null ? _rows : _drillRows; + public ObservableCollection GridRows => Drill is null ? _rows : _drillRows; /// 网格选中的行(钻入时是子表里的那一行;检查器仍看它所属的文档)。 public CollectionRow? GridSelectedRow { - get => _drill is null ? _selectedRow : _drillSelected; + get => Drill is null ? _selectedRow : _drillSelected; set { // 换数据源的那一刻 ListBox 会把选中清成 null 推回来 —— 那不是用户的选择。 @@ -46,7 +44,7 @@ public CollectionRow? GridSelectedRow { return; } - if (_drill is null) + if (Drill is null) { SelectedRow = value; return; @@ -65,14 +63,14 @@ public CollectionRow? GridSelectedRow /// 网格里多选的行(由视图在选中变化时写入)。 public IReadOnlyList GridSelectedRows { - get => _drill is null ? SelectedRows : _drillSelectedRows.Count > 0 ? _drillSelectedRows : _drillSelected is { } one ? [one] : []; + get => Drill is null ? SelectedRows : _drillSelectedRows.Count > 0 ? _drillSelectedRows : _drillSelected is { } one ? [one] : []; set { if (_swappingGrid) { return; } - if (_drill is null) + if (Drill is null) { SelectedRows = value; } @@ -84,14 +82,10 @@ public IReadOnlyList GridSelectedRows } /// 面包屑:文档 #3 › items › [2] › attrs。 - public IReadOnlyList DrillCrumbs - { - get => _drillCrumbs; - private set => SetProperty(ref _drillCrumbs, value); - } + public IReadOnlyList DrillCrumbs { get; private set => SetProperty(ref field, value); } = []; /// 面包屑右侧的小字(数组 · 4 项)。 - public string DrillSummary => _drill?.Summary(Loc) ?? ""; + public string DrillSummary => Drill?.Summary(Loc) ?? ""; /// 面包屑左侧的「‹」。 public RelayCommand DrillBackCommand { get; private set; } = null!; @@ -101,22 +95,22 @@ public IReadOnlyList DrillCrumbs private void InitializeDrillCommands() { - DrillBackCommand = new(DrillBack, () => _drill is not null); + DrillBackCommand = new(DrillBack, () => Drill is not null); DrillToCommand = new(crumb => { - if (_drill is not { } drill || crumb.IsLast) + if (Drill is not { } drill || crumb.IsLast) { return; } // 往回跳:历史里比目标更深的都不要了。 - _drillHistory.RemoveAll(p => p.Length >= crumb.Path.Length); + _ = _drillHistory.RemoveAll(p => p.Length >= crumb.Path.Length); if (crumb.Path.Length == 0) { ExitDrill(drill.Path); } else { - Show(drill.Root, crumb.Path, focusFrom: drill.Path); + _ = Show(drill.Root, crumb.Path, focusFrom: drill.Path); } }); } @@ -132,7 +126,7 @@ internal bool DrillInto(CollectionCell cell) return false; } CancelGridEdits(); - if (_drill is { } current) + if (Drill is { } current) { _drillHistory.Add(current.Path); } @@ -142,7 +136,7 @@ internal bool DrillInto(CollectionCell cell) /// 回上一层(Backspace / Alt+← / 「‹」);已在最外层时什么也不做。 internal void DrillBack() { - if (_drill is not { } drill) + if (Drill is not { } drill) { return; } @@ -162,7 +156,7 @@ internal void DrillBack() /// 退出钻入,回到本页文档;选中回到那份文档、当前列回到钻进去时的那一列。 internal void ExitDrill(string? focusFrom = null) { - if (_drill is not { } drill) + if (Drill is not { } drill) { return; } @@ -171,7 +165,7 @@ internal void ExitDrill(string? focusFrom = null) _swappingGrid = true; try { - _drill = null; + Drill = null; _drillRows = []; _drillSelected = null; _drillSelectedRows = []; @@ -199,19 +193,19 @@ internal void ExitDrill(string? focusFrom = null) /// private bool Show(CollectionRow root, string path, string? focusFrom) { - IReadOnlyList? previous = _drill is { } old && old.Path == path ? old.Columns : null; - GridDrill? drill = GridDrill.Create(this, root, path, previous, out List rows); + IReadOnlyList? previous = Drill is { } old && old.Path == path ? old.Columns : null; + var drill = GridDrill.Create(this, root, path, previous, out List rows); if (drill is null) { return false; } - int keepIndex = _drill is { } same && same.Path == path ? (_drillSelected?.Number ?? 0) : 0; + int keepIndex = Drill is { } same && same.Path == path ? (_drillSelected?.Number ?? 0) : 0; string? keepColumn = _currentColumn?.Name; _swappingGrid = true; try { - _drill = drill; - _drillRows = new ObservableCollection(rows); + Drill = drill; + _drillRows = [with(rows)]; _drillSelected = null; _drillSelectedRows = []; RaiseGridChanged(); @@ -263,7 +257,7 @@ private bool Show(CollectionRow root, string path, string? focusFrom) /// 暂存区变了:子表按所属文档的新版本重建(元素可能多了、少了),停在原来的下标上。 private void RefreshDrill() { - if (_drill is not { } drill) + if (Drill is not { } drill) { return; } @@ -273,7 +267,7 @@ private void RefreshDrill() return; } // 形状没变(元素数、列都一样)就原地重算各行 —— 改一格不该把子表整个换掉,选中、焦点、滚动位置都留着。 - GridDrill? fresh = GridDrill.Create(this, drill.Root, drill.Path, drill.Columns, out _); + var fresh = GridDrill.Create(this, drill.Root, drill.Path, drill.Columns, out _); if (fresh is not null && fresh.IsArray == drill.IsArray && fresh.Count == drill.Count && fresh.Columns.Select(static c => (c.Name, c.IsElementValue)).SequenceEqual(drill.Columns.Select(static c => (c.Name, c.IsElementValue)))) { @@ -302,7 +296,7 @@ private void RefreshDrill() /// 重查 / 翻页之后:按 _id(新增行按暂存键)找回那份文档,接着看同一层;找不到就退出。 private void ReattachDrill() { - if (_drill is not { } drill) + if (Drill is not { } drill) { return; } @@ -325,7 +319,7 @@ private void AddDrillRow(GridDrill drill) if (drill.IsArray) { InsertArrayElement(drill.Root, drill.Path); - if (_drill is { } now && _drillRows.LastOrDefault() is { } last) + if (Drill is { } now && _drillRows.LastOrDefault() is { } last) { GridSelectedRow = last; _currentColumn = now.Columns.FirstOrDefault(static c => c.IsElementValue) ?? _currentColumn; @@ -340,7 +334,7 @@ private void AddDrillRow(GridDrill drill) /// 工具栏「删除」、底栏「−」、Del:网格钻入时删子表里的元素,否则暂存删除选中的文档。 private void DeleteGridSelection() { - if (_drill is { } drill && _viewMode == CollectionViewMode.Grid) + if (Drill is { } drill && _viewMode == CollectionViewMode.Grid) { DeleteDrillRows(drill); } @@ -378,7 +372,7 @@ private void DeleteDrillRows(GridDrill drill) { copy.RemoveAt(index); } - Stage(drill.Root, drill.Path, copy); + _ = Stage(drill.Root, drill.Path, copy); } private void RaiseGridChanged() @@ -392,7 +386,7 @@ private void RaiseGridChanged() private List BuildCrumbs() { - if (_drill is not { } drill) + if (Drill is not { } drill) { return []; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Edit.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Edit.cs index 4588525..7468db2 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Edit.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Edit.cs @@ -3,8 +3,8 @@ using MongoDB.Bson; using MongoDB.Driver; using VelaShell.Plugin.Mongo.Bson; -using VelaShell.Plugin.Mongo.Staging; using VelaShell.Plugin.Mongo.Core; +using VelaShell.Plugin.Mongo.Staging; namespace VelaShell.Plugin.Mongo.Ui; @@ -322,7 +322,7 @@ internal void OpenInEditor(CollectionRow row) Workspace.Toast(new() { Title = Loc["Cw_ViewReadOnly"], Kind = ToastKind.Warning }); return; } - BsonDocument document = (BsonDocument)row.Document.DeepClone(); + var document = (BsonDocument)row.Document.DeepClone(); Workspace.ShowDialog(new DocumentEditorDialogViewModel(Workspace, Info, document, async saved => { DiscardDocument(row); @@ -439,7 +439,7 @@ private IReadOnlyList EnumChoices(string path) { if (BsonPath.Get(doc, path) is BsonString s) { - distinct.Add(s.Value); + _ = distinct.Add(s.Value); if (distinct.Count > 8) { return []; @@ -535,10 +535,10 @@ internal void DeleteField(CollectionRow row, string path) { var copy = (BsonArray)array.DeepClone(); copy.RemoveAt(index); - Stage(row, path[..dot], copy); + _ = Stage(row, path[..dot], copy); return; } - Stage(row, path, null); + _ = Stage(row, path, null); } /// 在数组末尾插入一个元素(类型照第一项;文档元素给同样的键、空值)。 @@ -553,7 +553,7 @@ internal void InsertArrayElement(CollectionRow row, string arrayPath) : array[0] is BsonDocument template ? new BsonDocument(template.Elements.Select(static e => new BsonElement(e.Name, BsonEdit.Empty(BsonKinds.Of(e.Value))))) : BsonEdit.Empty(BsonKinds.Of(array[0])); - Stage(row, $"{arrayPath}.{array.Count.ToString(CultureInfo.InvariantCulture)}", element); + _ = Stage(row, $"{arrayPath}.{array.Count.ToString(CultureInfo.InvariantCulture)}", element); } /// 添加字段(弹一个小对话框问名字、类型、值)。 @@ -776,13 +776,13 @@ private void DiscardAll() private async Task UnlockAsync() { if (!await Workspace.ConfirmAsync(new() - { - Title = Loc["State_UnlockTitle"], - Message = Loc.Format("State_UnlockBody", Workspace.ConnectionName), - ConfirmLabel = Loc["State_UnlockConfirm"], - IconKey = "Mongo.lock-open", - Danger = Workspace.Guard.IsProduction - }).ConfigureAwait(true)) + { + Title = Loc["State_UnlockTitle"], + Message = Loc.Format("State_UnlockBody", Workspace.ConnectionName), + ConfirmLabel = Loc["State_UnlockConfirm"], + IconKey = "Mongo.lock-open", + Danger = Workspace.Guard.IsProduction + }).ConfigureAwait(true)) { return; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Query.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Query.cs index bd11299..e0ed2f1 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Query.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Query.cs @@ -1,6 +1,6 @@ -using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; +using System.Text; using Avalonia.Threading; using MongoDB.Bson; using MongoDB.Driver; @@ -28,32 +28,16 @@ internal sealed partial class CollectionTabViewModel private static readonly TimeSpan LongQueryThreshold = TimeSpan.FromMilliseconds(700); private string _filterText; - private string? _filterError; - private IReadOnlyList _filterDiagnostics = []; - private string _projectionText = ""; - private bool _projectionInvalid; - private string _sortText = ""; - private bool _sortInvalid; - private string _skipText = "0"; private string _limitText; - private bool _skipInvalid; - private bool _limitInvalid; - private IReadOnlyList _hintChoices = []; private string _selectedHint = ""; - private bool _isFavorite; private IReadOnlyList _favorites = []; private IReadOnlyList _history = []; private int _pageIndex; - private string _pageText = "1"; private CancellationTokenSource? _queryCts; private string? _queryComment; private Stopwatch? _queryWatch; private DispatcherTimer? _longQueryTimer; - private bool _isLongQuery; - private string _longQueryText = ""; private long? _opid; - private string _lastRunFilter = ""; - private FindRequest? _lastRequest; private int _countVersion; /// 查找(Ctrl+Enter)。 @@ -119,10 +103,10 @@ private void InitializeQueryCommands() FirstPageCommand = new(() => GoToPageAsync(0), () => _pageIndex > 0); PreviousPageCommand = new(() => GoToPageAsync(_pageIndex - 1), () => _pageIndex > 0); NextPageCommand = new(() => GoToPageAsync(_pageIndex + 1), () => _pageIndex < PageCount - 1); - LastPageCommand = new(() => GoToPageAsync(PageCount - 1), () => _totalCount is not null && _pageIndex < PageCount - 1); + LastPageCommand = new(() => GoToPageAsync(PageCount - 1), () => TotalCount is not null && _pageIndex < PageCount - 1); GoToPageCommand = new(() => { - if (int.TryParse(_pageText.Replace(",", "", StringComparison.Ordinal), NumberStyles.Integer, CultureInfo.InvariantCulture, out int page)) + if (int.TryParse(PageText.Replace(",", "", StringComparison.Ordinal), NumberStyles.Integer, CultureInfo.InvariantCulture, out int page)) { return GoToPageAsync(Math.Clamp(page - 1, 0, Math.Max(0, PageCount - 1))); } @@ -164,10 +148,9 @@ public void ApplyFilter(string filter) /// 筛选的语法错误(标红 + 提示);没有为 。 public string? FilterError { - get => _filterError; - private set + get; private set { - if (SetProperty(ref _filterError, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasFilterError)); } @@ -175,76 +158,57 @@ private set } /// 有语法错误。 - public bool HasFilterError => _filterError is not null; + public bool HasFilterError => FilterError is not null; /// 编辑器上的诊断(红色波浪线)。 - public IReadOnlyList FilterDiagnostics - { - get => _filterDiagnostics; - private set => SetProperty(ref _filterDiagnostics, value); - } + public IReadOnlyList FilterDiagnostics { get; private set => SetProperty(ref field, value); } = []; /// 投影。 public string ProjectionText { - get => _projectionText; - set + get; set { - if (SetProperty(ref _projectionText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { - ProjectionInvalid = !ShellJson.TryParseDocument(_projectionText, out _, out _); + ProjectionInvalid = !ShellJson.TryParseDocument(field, out _, out _); RaisePropertyChanged(nameof(EchoText)); } } - } + } = ""; /// 投影写错了。 - public bool ProjectionInvalid - { - get => _projectionInvalid; - private set => SetProperty(ref _projectionInvalid, value); - } + public bool ProjectionInvalid { get; private set => SetProperty(ref field, value); } /// 排序。 public string SortText { - get => _sortText; - set + get; set { - if (SetProperty(ref _sortText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { - SortInvalid = !ShellJson.TryParseDocument(_sortText, out _, out _); + SortInvalid = !ShellJson.TryParseDocument(field, out _, out _); RaisePropertyChanged(nameof(EchoText)); } } - } + } = ""; /// 排序写错了。 - public bool SortInvalid - { - get => _sortInvalid; - private set => SetProperty(ref _sortInvalid, value); - } + public bool SortInvalid { get; private set => SetProperty(ref field, value); } /// 跳过。 public string SkipText { - get => _skipText; - set + get; set { - if (SetProperty(ref _skipText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { - SkipInvalid = ParseCount(_skipText, 0) is null; + SkipInvalid = ParseCount(field, 0) is null; } } - } + } = "0"; /// 跳过写错了。 - public bool SkipInvalid - { - get => _skipInvalid; - private set => SetProperty(ref _skipInvalid, value); - } + public bool SkipInvalid { get; private set => SetProperty(ref field, value); } /// 限制(每页行数)。 public string LimitText @@ -260,18 +224,10 @@ public string LimitText } /// 限制写错了。 - public bool LimitInvalid - { - get => _limitInvalid; - private set => SetProperty(ref _limitInvalid, value); - } + public bool LimitInvalid { get; private set => SetProperty(ref field, value); } /// 索引提示的选项(第一项「自动」)。 - public IReadOnlyList HintChoices - { - get => _hintChoices.Count == 0 ? [Loc["Cw_HintAuto"]] : _hintChoices; - private set => SetProperty(ref _hintChoices, value); - } + public IReadOnlyList HintChoices { get => field.Count == 0 ? [Loc["Cw_HintAuto"]] : field; private set => SetProperty(ref field, value); } = []; /// 选中的索引提示。 public string SelectedHint @@ -281,11 +237,7 @@ public string SelectedHint } /// 当前筛选是不是收藏(星标填色)。 - public bool IsFavorite - { - get => _isFavorite; - private set => SetProperty(ref _isFavorite, value); - } + public bool IsFavorite { get; private set => SetProperty(ref field, value); } /// 历史下拉的内容:收藏在前,历史在后。 public IReadOnlyList HistoryItems => @@ -298,42 +250,38 @@ public bool IsFavorite public bool HasHistory => _favorites.Count > 0 || _history.Count > 0; /// 筛选非空(决定空态是「空集合」还是「无结果」)。 - public bool HasActiveFilter => _lastRunFilter.Length > 0 && _lastRunFilter != "{}"; + public bool HasActiveFilter => LastRunFilter.Length > 0 && LastRunFilter != "{}"; /// 最近一次执行的筛选(无结果卡片里回显)。 - public string LastRunFilter => _lastRunFilter; + public string LastRunFilter { get; private set; } = ""; /// 当前查询(导出向导「导出当前查询结果」用)。 - public FindRequest? CurrentRequest => _lastRequest; + public FindRequest? CurrentRequest { get; private set; } /// 解析好的排序(列头的箭头用);写错为 。 - internal BsonDocument? ParsedSort => ShellJson.TryParseDocument(_sortText, out BsonDocument doc, out _) ? doc : null; + internal BsonDocument? ParsedSort => ShellJson.TryParseDocument(SortText, out BsonDocument doc, out _) ? doc : null; // ── 分页 ───────────────────────────────────────────────────────────────── /// 总数(有筛选用 countDocuments,无筛选用 estimatedDocumentCount);没数完为 。 - public long? TotalCount => _totalCount; + public long? TotalCount { get; private set; } /// 每页行数。 public int PageSize => ParseCount(_limitText, 1) ?? Workspace.Connection.Settings.PageSize; /// 基础跳过数。 - public int BaseSkip => ParseCount(_skipText, 0) ?? 0; + public int BaseSkip => ParseCount(SkipText, 0) ?? 0; /// 总页数(总数未知时至少当前页 + 1)。 - public int PageCount => _totalCount is { } total + public int PageCount => TotalCount is { } total ? (int)Math.Max(1, Math.Ceiling(Math.Max(0, total - BaseSkip) / (double)PageSize)) : _pageIndex + (_documents.Count >= PageSize ? 2 : 1); /// 页码框。 - public string PageText - { - get => _pageText; - set => SetProperty(ref _pageText, value ?? ""); - } + public string PageText { get; set => SetProperty(ref field, value ?? ""); } = "1"; /// / 25,699 页。 - public string PageCountText => _totalCount is null ? Loc["Cw_PageCountUnknown"] : Loc.Format("Cw_PageCount", BsonText.Grouped(PageCount)); + public string PageCountText => TotalCount is null ? Loc["Cw_PageCountUnknown"] : Loc.Format("Cw_PageCount", BsonText.Grouped(PageCount)); /// 1–50 / 1,284,902。 public string RangeText @@ -341,7 +289,7 @@ public string RangeText get { long first = BaseSkip + (long)_pageIndex * PageSize; - string total = _totalCount is { } t ? BsonText.Grouped(t) : "…"; + string total = TotalCount is { } t ? BsonText.Grouped(t) : "…"; return _documents.Count == 0 ? $"0 / {total}" : $"{BsonText.Grouped(first + 1)}–{BsonText.Grouped(first + _documents.Count)} / {total}"; @@ -354,21 +302,13 @@ public string RangeText // ── 长查询 ─────────────────────────────────────────────────────────────── /// 长查询卡片(设计稿 22「正在查询 shop.orders…」)。 - public bool IsLongQuery - { - get => _isLongQuery; - private set => SetProperty(ref _isLongQuery, value); - } + public bool IsLongQuery { get; private set => SetProperty(ref field, value); } /// 长查询卡片标题。 public string LongQueryTitle => Loc.Format("State_LongQueryTitle", Info.Namespace); /// 已运行 6.2 s · 服务端 opid 8812045。 - public string LongQueryText - { - get => _longQueryText; - private set => SetProperty(ref _longQueryText, value); - } + public string LongQueryText { get; private set => SetProperty(ref field, value); } = ""; // ── 执行 ───────────────────────────────────────────────────────────────── @@ -422,8 +362,8 @@ public async Task RunQueryAsync(bool resetPage) } _elapsed = watch.Elapsed; _loadedOnce = true; - _lastRequest = request; - _lastRunFilter = _filterText.Trim(); + CurrentRequest = request; + LastRunFilter = _filterText.Trim(); ApplyResults(documents, request.Skip + 1); RaisePropertiesChanged(nameof(Elapsed), nameof(LastRunFilter), nameof(HasActiveFilter), nameof(CurrentRequest), nameof(EchoText), nameof(RangeText), nameof(PageCountText)); @@ -432,7 +372,7 @@ public async Task RunQueryAsync(bool resetPage) _ = ExplainPlanAsync(request); if (HasActiveFilter) { - _ = RememberFilterAsync(_lastRunFilter); + _ = RememberFilterAsync(LastRunFilter); } } catch (OperationCanceledException) @@ -465,13 +405,13 @@ private bool TryBuildRequest(int pageIndex, out FindRequest request) { request = null!; ValidateFilter(); - if (_filterError is not null || _projectionInvalid || _sortInvalid || _skipInvalid || _limitInvalid) + if (FilterError is not null || ProjectionInvalid || SortInvalid || SkipInvalid || LimitInvalid) { return false; } BsonDocument filter = ShellJson.ParseDocument(_filterText); - BsonDocument projection = ShellJson.ParseDocument(_projectionText); - BsonDocument sort = ShellJson.ParseDocument(_sortText); + BsonDocument projection = ShellJson.ParseDocument(ProjectionText); + BsonDocument sort = ShellJson.ParseDocument(SortText); request = new FindRequest { Database = Database, @@ -566,7 +506,7 @@ private void RaisePagingCanExecute() private async Task CountAsync(FindRequest request) { int version = ++_countVersion; - _totalCount = null; + TotalCount = null; RaisePropertiesChanged(nameof(TotalCount), nameof(PageCountText), nameof(RangeText)); try { @@ -581,7 +521,7 @@ private async Task CountAsync(FindRequest request) { return; } - _totalCount = count; + TotalCount = count; } catch (Exception ex) when (ex is MongoException or TimeoutException or OperationCanceledException) { @@ -648,7 +588,7 @@ private void OnLongQueryTick() { return; } - if (!_isLongQuery) + if (!IsLongQuery) { IsLongQuery = true; _ = FindOpidAsync(_queryComment); @@ -768,30 +708,30 @@ private async Task LoadIndexesAsync() internal string BuildStatement(bool abbreviate, int? skipOverride = null) { string filter = string.IsNullOrWhiteSpace(_filterText) ? "{}" : abbreviate ? "{…}" : _filterText.Trim(); - string projection = string.IsNullOrWhiteSpace(_projectionText) ? "" : ", " + _projectionText.Trim(); - var text = new System.Text.StringBuilder() + string projection = string.IsNullOrWhiteSpace(ProjectionText) ? "" : ", " + ProjectionText.Trim(); + StringBuilder text = new System.Text.StringBuilder() .Append("db.").Append(MongoWorkspaceViewModel.ShellCollectionRef(CollectionName)) .Append(".find(").Append(filter).Append(projection).Append(')'); - if (!string.IsNullOrWhiteSpace(_sortText)) + if (!string.IsNullOrWhiteSpace(SortText)) { - text.Append(".sort(").Append(_sortText.Trim()).Append(')'); + _ = text.Append(".sort(").Append(SortText.Trim()).Append(')'); } if (_selectedHint.Length > 0) { - text.Append(".hint(").Append(BsonText.Quote(_selectedHint)).Append(')'); + _ = text.Append(".hint(").Append(BsonText.Quote(_selectedHint)).Append(')'); } int skip = skipOverride ?? (BaseSkip + _pageIndex * PageSize); if (skip > 0) { - text.Append(".skip(").Append(skip.ToString(CultureInfo.InvariantCulture)).Append(')'); + _ = text.Append(".skip(").Append(skip.ToString(CultureInfo.InvariantCulture)).Append(')'); } - text.Append(".limit(").Append(PageSize.ToString(CultureInfo.InvariantCulture)).Append(')'); + _ = text.Append(".limit(").Append(PageSize.ToString(CultureInfo.InvariantCulture)).Append(')'); return text.ToString(); } private void OpenExplain() { - if (_filterError is not null) + if (FilterError is not null) { return; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Views.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Views.cs index dc50639..f617a79 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Views.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.Views.cs @@ -5,22 +5,18 @@ namespace VelaShell.Plugin.Mongo.Ui; /// 树视图与 JSON 卡片:同一页文档的另外两种画法。 internal sealed partial class CollectionTabViewModel { - private readonly HashSet _treeExpanded = new(StringComparer.Ordinal); - private ObservableCollection _treeRows = []; + private readonly HashSet _treeExpanded = [with(StringComparer.Ordinal)]; private DocTreeRow? _selectedTreeRow; - private ObservableCollection _cards = []; private JsonCardViewModel? _focusedCard; - private bool _inlineTypeHints = true; - private bool _formatOnSave = true; // ── 树 ─────────────────────────────────────────────────────────────────── /// 树视图的可见行。 public ObservableCollection TreeRows { - get => _treeRows; - private set => SetProperty(ref _treeRows, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 树视图选中的行。 public DocTreeRow? SelectedTreeRow @@ -45,7 +41,7 @@ internal void ToggleTree(DocTreeRow row) string key = DocTreeBuilder.ExpansionKey(row.Row, row.Path); if (!_treeExpanded.Remove(key)) { - _treeExpanded.Add(key); + _ = _treeExpanded.Add(key); } RebuildTree(); } @@ -53,11 +49,11 @@ internal void ToggleTree(DocTreeRow row) /// 展开一份文档的某个路径(及其祖先)—— 截图与"定位到字段"用。 internal void ExpandTreePath(CollectionRow row, string path) { - _treeExpanded.Add(DocTreeBuilder.ExpansionKey(row, "")); + _ = _treeExpanded.Add(DocTreeBuilder.ExpansionKey(row, "")); string[] segments = path.Split('.', StringSplitOptions.RemoveEmptyEntries); for (int i = 1; i <= segments.Length; i++) { - _treeExpanded.Add(DocTreeBuilder.ExpansionKey(row, string.Join('.', segments[..i]))); + _ = _treeExpanded.Add(DocTreeBuilder.ExpansionKey(row, string.Join('.', segments[..i]))); } RebuildTree(); } @@ -71,7 +67,7 @@ internal void RebuildTree() } (CollectionRow? keepRow, string? keepPath) = (_selectedTreeRow?.Row, _selectedTreeRow?.Path); List rows = DocTreeBuilder.Build(_rows, _treeExpanded, Staging, Loc); - TreeRows = new ObservableCollection(rows); + TreeRows = [with(rows)]; _selectedTreeRow = rows.FirstOrDefault(r => ReferenceEquals(r.Row, keepRow) && r.Path == keepPath) ?? rows.FirstOrDefault(r => ReferenceEquals(r.Row, _selectedRow) && r.IsDocument); RaisePropertyChanged(nameof(SelectedTreeRow)); @@ -82,9 +78,9 @@ internal void RebuildTree() /// JSON 视图的卡片(一份文档一张)。 public ObservableCollection Cards { - get => _cards; - private set => SetProperty(ref _cards, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 大纲面板看的那张卡片(编辑中的优先,否则跟着选中行)。 public JsonCardViewModel? FocusedCard @@ -110,22 +106,22 @@ public JsonCardViewModel? FocusedCard /// 行内类型提示(编辑底栏显示光标所在字段的类型)。 public bool InlineTypeHints { - get => _inlineTypeHints; + get; set { - if (SetProperty(ref _inlineTypeHints, value)) + if (SetProperty(ref field, value)) { _focusedCard?.UpdateCaret(); } } - } + } = true; /// 保存前自动格式化(「更新文档」先把文本重排成标准缩进)。 public bool FormatOnSave { - get => _formatOnSave; - set => SetProperty(ref _formatOnSave, value); - } + get; + set => SetProperty(ref field, value); + } = true; /// 重建卡片(只在 JSON 视图可见时)。正在编辑的卡片保留,免得刷新把用户敲了一半的文本冲掉。 internal void RebuildCards() @@ -134,7 +130,7 @@ internal void RebuildCards() { return; } - Dictionary editing = _cards.Where(static c => c.IsEditing).ToDictionary(static c => c.Row); + var editing = Cards.Where(static c => c.IsEditing).ToDictionary(static c => c.Row); var cards = new List(_rows.Count); foreach (CollectionRow row in _rows) { @@ -142,7 +138,7 @@ internal void RebuildCards() ?? editing.Values.FirstOrDefault(c => c.Row.Id is { } id && id.Equals(row.Id)); cards.Add(kept is not null && ReferenceEquals(kept.Row, row) ? kept : new JsonCardViewModel(this, row)); } - Cards = new ObservableCollection(cards); + Cards = [with(cards)]; SyncCardSelection(); } @@ -153,8 +149,8 @@ private void SyncCardSelection() { return; } - JsonCardViewModel? editing = _cards.FirstOrDefault(static c => c.IsEditing); - JsonCardViewModel? target = editing ?? _cards.FirstOrDefault(c => ReferenceEquals(c.Row, _selectedRow)) ?? _cards.FirstOrDefault(); + JsonCardViewModel? editing = Cards.FirstOrDefault(static c => c.IsEditing); + JsonCardViewModel? target = editing ?? Cards.FirstOrDefault(c => ReferenceEquals(c.Row, _selectedRow)) ?? Cards.FirstOrDefault(); if (!ReferenceEquals(target, _focusedCard)) { _focusedCard = target; @@ -168,7 +164,7 @@ internal void OnCardEditingChanged(JsonCardViewModel card) { if (card.IsEditing) { - foreach (JsonCardViewModel other in _cards.Where(c => c.IsEditing && !ReferenceEquals(c, card)).ToList()) + foreach (JsonCardViewModel other in Cards.Where(c => c.IsEditing && !ReferenceEquals(c, card)).ToList()) { other.CancelEdit(); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.cs index 275af8f..c4a2c9b 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/CollectionTabViewModel.cs @@ -21,23 +21,13 @@ namespace VelaShell.Plugin.Mongo.Ui; /// internal sealed partial class CollectionTabViewModel : WorkspaceTab { - private CollectionInfo _info; - private CollectionViewMode _viewMode = CollectionViewMode.Grid; - private IReadOnlyList _columns = []; - private ObservableCollection _rows = []; private List _documents = []; private CollectionRow? _selectedRow; private IReadOnlyList _selectedRows = []; private CollectionColumn? _currentColumn; private EjsonMode _ejson; - private bool _isLoading; private bool _loadedOnce; - private long? _totalCount; - private TimeSpan _elapsed; - private string _planSummary = ""; - private CollectionSample _sample = CollectionSample.Empty; private Task? _sampleTask; - private CollectionStats? _stats; private bool _disposed; /// 构造(外壳在用的签名,保持不变)。 @@ -47,7 +37,7 @@ internal sealed partial class CollectionTabViewModel : WorkspaceTab public CollectionTabViewModel(IMongoWorkspace workspace, CollectionInfo info, string? filter) : base(workspace) { - _info = info; + Info = info; Title = info.Name; Scope = "@" + info.Database; _ejson = workspace.Connection.Settings.Ejson; @@ -62,13 +52,13 @@ public CollectionTabViewModel(IMongoWorkspace workspace, CollectionInfo info, st } /// 集合信息。 - public CollectionInfo Info => _info; + public CollectionInfo Info { get; private set; } /// 库名。 - public string Database => _info.Database; + public string Database => Info.Database; /// 集合名。 - public string CollectionName => _info.Name; + public string CollectionName => Info.Name; /// public override TabKind Kind => TabKind.Collection; @@ -96,10 +86,9 @@ public CollectionTabViewModel(IMongoWorkspace workspace, CollectionInfo info, st /// 当前视图。 public CollectionViewMode ViewMode { - get => _viewMode; - set + get; set { - if (SetProperty(ref _viewMode, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsGridMode), nameof(IsTreeMode), nameof(IsJsonMode), nameof(PendingText)); if (value == CollectionViewMode.Tree) @@ -112,42 +101,37 @@ public CollectionViewMode ViewMode } } } - } + } = CollectionViewMode.Grid; /// 上一次右侧面板是开着还是收着:新开的集合标签照这个来(Navicat 也记着)。 private static bool s_sidePanelVisible = true; - private bool _isSidePanelVisible = s_sidePanelVisible; - /// /// 数据区右侧的面板(网格的文档检查器、JSON 视图的大纲)开着吗。底栏右下角那颗按钮切换它; /// 收起之后数据区占满整宽,展开时回到收起前拖到的宽度。 /// public bool IsSidePanelVisible { - get => _isSidePanelVisible; - set + get; set { - if (SetProperty(ref _isSidePanelVisible, value)) + if (SetProperty(ref field, value)) { s_sidePanelVisible = value; RaisePropertyChanged(nameof(SidePanelTip)); } } - } + } = s_sidePanelVisible; /// 按钮的提示:「隐藏右侧面板」/「显示右侧面板」。 - public string SidePanelTip => Loc[_isSidePanelVisible ? "Cw_HideSidePanel" : "Cw_ShowSidePanel"]; + public string SidePanelTip => Loc[IsSidePanelVisible ? "Cw_HideSidePanel" : "Cw_ShowSidePanel"]; /// 收起 / 展开右侧面板。 - public RelayCommand ToggleSidePanelCommand => _toggleSidePanel ??= new RelayCommand(() => IsSidePanelVisible = !IsSidePanelVisible); - - private RelayCommand? _toggleSidePanel; + public RelayCommand ToggleSidePanelCommand => field ??= new RelayCommand(() => IsSidePanelVisible = !IsSidePanelVisible); /// 网格视图(分段按钮双向绑定)。 public bool IsGridMode { - get => _viewMode == CollectionViewMode.Grid; + get => ViewMode == CollectionViewMode.Grid; set { if (value) @@ -160,7 +144,7 @@ public bool IsGridMode /// 树视图。 public bool IsTreeMode { - get => _viewMode == CollectionViewMode.Tree; + get => ViewMode == CollectionViewMode.Tree; set { if (value) @@ -173,7 +157,7 @@ public bool IsTreeMode /// JSON 视图。 public bool IsJsonMode { - get => _viewMode == CollectionViewMode.Json; + get => ViewMode == CollectionViewMode.Json; set { if (value) @@ -245,28 +229,26 @@ public bool IsCanonicalMode /// 列。 public IReadOnlyList Columns { - get => _columns; - private set + get; private set { - if (SetProperty(ref _columns, value) && _drill is null) + if (SetProperty(ref field, value) && _drill is null) { RaisePropertyChanged(nameof(GridColumns)); } } - } + } = []; /// 行(本页文档 + 暂存的新文档)。 public ObservableCollection Rows { - get => _rows; - private set + get; private set { - if (SetProperty(ref _rows, value) && _drill is null) + if (SetProperty(ref field, value) && _drill is null) { RaisePropertyChanged(nameof(GridRows)); } } - } + } = []; /// 本页从服务器读到的文档。 public IReadOnlyList Documents => _documents; @@ -333,24 +315,19 @@ public CollectionColumn? CurrentColumn public bool IsCurrentRow(CollectionRow row) => ReferenceEquals(row, GridSelectedRow); /// 补全用的抽样(没抽到时为空样本)。 - public CollectionSample Sample => _sample; + public CollectionSample Sample { get; private set; } = CollectionSample.Empty; /// 集合统计(检查器「集合信息」页、对象树底部之外的另一个入口)。 - public CollectionStats? Stats - { - get => _stats; - private set => SetProperty(ref _stats, value); - } + public CollectionStats? Stats { get; private set => SetProperty(ref field, value); } // ── 加载状态 ───────────────────────────────────────────────────────────── /// 正在查询。 public bool IsLoading { - get => _isLoading; - private set + get; private set { - if (SetProperty(ref _isLoading, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsEmptyCollection), nameof(IsNoResult), nameof(ShowData)); StopCommand.RaiseCanExecuteChanged(); @@ -359,27 +336,26 @@ private set } /// 本页往返耗时。 - public TimeSpan Elapsed => _elapsed; + public TimeSpan Elapsed { get; private set; } /// 执行计划摘要(IXSCAN status_1_createdAt_-1)。 public string PlanSummary { - get => _planSummary; - private set + get; private set { - if (SetProperty(ref _planSummary, value)) + if (SetProperty(ref field, value)) { UpdateStatus(); } } - } + } = ""; /// 空集合(没有筛选、一份文档都没有):设计稿 22 的「空集合」卡。 - public bool IsEmptyCollection => _loadedOnce && !_isLoading && _documents.Count == 0 && Staging.InsertCount == 0 + public bool IsEmptyCollection => _loadedOnce && !IsLoading && _documents.Count == 0 && Staging.InsertCount == 0 && !HasActiveFilter && _pageIndex == 0; /// 筛选无结果。 - public bool IsNoResult => _loadedOnce && !_isLoading && _documents.Count == 0 && Staging.InsertCount == 0 + public bool IsNoResult => _loadedOnce && !IsLoading && _documents.Count == 0 && Staging.InsertCount == 0 && (HasActiveFilter || _pageIndex > 0); /// 显示数据区(不是空态)。 @@ -440,7 +416,7 @@ private async Task RefreshInfoAsync() BsonDocument? raw = await cursor.FirstOrDefaultAsync().ConfigureAwait(true); if (raw is not null) { - _info = MongoConnection.ToCollectionInfo(Database, raw); + Info = MongoConnection.ToCollectionInfo(Database, raw); RaisePropertiesChanged(nameof(Info), nameof(IconKey), nameof(CanEdit), nameof(IsEditable), nameof(ShowViewBanner), nameof(ViewBannerText)); Inspector.RefreshValidation(); @@ -461,7 +437,7 @@ async Task LoadSampleAsync() { try { - _sample = await CollectionSample.LoadAsync(Workspace.Connection, Database, CollectionName, + Sample = await CollectionSample.LoadAsync(Workspace.Connection, Database, CollectionName, Workspace.Connection.Settings.SampleSize, _lifetime.Token).ConfigureAwait(true); RaisePropertyChanged(nameof(Sample)); } @@ -475,7 +451,7 @@ async Task LoadSampleAsync() /// 统计(检查器「集合信息」页第一次打开时取)。 internal async Task LoadStatsAsync() { - if (_stats is not null) + if (Stats is not null) { return; } @@ -507,7 +483,7 @@ private void ApplyResults(List documents, int firstNumber) } _selectedRows = []; _selectedRow = null; - Rows = new ObservableCollection(rows); + Rows = [with(rows)]; _trackedIds = [.. Staging.Edits.Select(static e => e.Id).Concat(Staging.Deletes.Select(static d => d.Id))]; CollectionRow? select = rows.FirstOrDefault(r => keepId is not null && keepId.Equals(r.Id)) ?? rows.FirstOrDefault(r => keepInsert is not null && r.Insert?.Key == keepInsert) @@ -560,7 +536,7 @@ private IReadOnlyList BuildColumns(IReadOnlyList { order.Insert(0, "_id"); } - Dictionary previous = _columns.ToDictionary(static c => c.Name, StringComparer.Ordinal); + var previous = Columns.ToDictionary(static c => c.Name, StringComparer.Ordinal); BsonDocument sort = ParsedSort ?? []; var columns = new List(order.Count); foreach (string name in order) @@ -627,10 +603,10 @@ private void UpdateCurrentCell(CollectionRow? previous, CollectionRow? next) private void UpdateStatus() { string text = Loc.Format("Cw_Status", Info.Namespace, BsonText.Grouped(_documents.Count), - ((int)_elapsed.TotalMilliseconds).ToString(CultureInfo.InvariantCulture)); - if (_planSummary.Length > 0) + ((int)Elapsed.TotalMilliseconds).ToString(CultureInfo.InvariantCulture)); + if (PlanSummary.Length > 0) { - text += " · " + _planSummary; + text += " · " + PlanSummary; } if (!Staging.IsEmpty) { @@ -642,7 +618,7 @@ private void UpdateStatus() /// 从执行计划里挑出获胜阶段的摘要(IXSCAN 索引名 / COLLSCAN / IDHACK)。 internal static string SummarizePlan(BsonDocument explain) { - BsonDocument? planner = explain.GetValue("queryPlanner", BsonNull.Value) as BsonDocument; + var planner = explain.GetValue("queryPlanner", BsonNull.Value) as BsonDocument; if (planner is null && explain.GetValue("stages", BsonNull.Value) is BsonArray stages && stages.FirstOrDefault() is BsonDocument first && first.GetValue("$cursor", BsonNull.Value) is BsonDocument cursor) diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/ConflictDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/ConflictDialogViewModel.cs index 9fb9257..cb6cb91 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/ConflictDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/ConflictDialogViewModel.cs @@ -25,7 +25,6 @@ internal sealed record ConflictResolution(ConflictChoice Choice, IReadOnlySet冲突对话框的一行:字段 · 我的 · 服务器(点哪边就保留哪边)。 internal sealed class ConflictRow : ObservableObject { - private bool _useMine = true; /// 构造。 public ConflictRow(string path, string mine, string server, bool bothChanged) @@ -53,18 +52,18 @@ public ConflictRow(string path, string mine, string server, bool bothChanged) /// 合并时保留我的。 public bool UseMine { - get => _useMine; + get; set { - if (SetProperty(ref _useMine, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(UseServer)); } } - } + } = true; /// 合并时保留服务器的。 - public bool UseServer => !_useMine; + public bool UseServer => !UseMine; /// 选我的。 public RelayCommand PickMineCommand { get; } @@ -163,7 +162,7 @@ private async Task FinishAsync(ConflictChoice choice) _resolved = true; HashSet keep = choice == ConflictChoice.Merge ? Rows.Where(static r => r.UseMine).Select(static r => r.Path).ToHashSet(StringComparer.Ordinal) - : new HashSet(StringComparer.Ordinal); + : [with(StringComparer.Ordinal)]; Close(); await _resolve(new ConflictResolution(choice, keep)).ConfigureAwait(true); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DatePickFlyout.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DatePickFlyout.cs index c61c169..46ad03c 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DatePickFlyout.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DatePickFlyout.cs @@ -1,10 +1,10 @@ using System.Globalization; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Styling; using VelaShell.Plugin.Mongo.Bson; using Calendar = Avalonia.Controls.Calendar; @@ -39,53 +39,48 @@ public static Flyout Show(Control anchor, Loc loc, string text, Action p DisplayDate = start.Date, IsTodayHighlighted = true }; + // 时刻框与两颗按钮用本插件自己的主题(描边输入框、主操作药丸、次操作描边): + // 不指定就落到宿主的 Fluent 默认上 —— 圆角大一号、「应用」是系统强调蓝,和上面的月历不是一套。 var time = new TextBox { + Theme = ThemeOf(anchor, "MongoTextBox"), Text = start.ToString("HH:mm:ss", CultureInfo.InvariantCulture), - Width = 96, - VerticalContentAlignment = VerticalAlignment.Center + Width = 84, + Height = 28, + VerticalAlignment = VerticalAlignment.Center }; - if (anchor.TryFindResource("VelaUiMonoFont", out object? mono) && mono is FontFamily family) - { - time.FontFamily = family; - } ToolTip.SetTip(time, loc["Doc_LocalTime"]); - var now = new Button { Content = loc["Cw_DateNow"], VerticalAlignment = VerticalAlignment.Center }; - var apply = new Button { Content = loc["Common_Apply"], VerticalAlignment = VerticalAlignment.Center }; - apply.Classes.Add("accent"); + var now = new Button { Theme = ThemeOf(anchor, "MongoOutlineButton"), Content = loc["Cw_DateNow"], Padding = new Thickness(12, 0) }; + var apply = new Button { Theme = ThemeOf(anchor, "MongoPillButton"), Content = loc["Common_Apply"], Padding = new Thickness(12, 0) }; var buttons = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6, VerticalAlignment = VerticalAlignment.Center }; buttons.Children.Add(now); buttons.Children.Add(apply); DockPanel.SetDock(buttons, Dock.Right); - var label = new TextBlock - { - Text = loc["Cw_DateTime"], - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 8, 0), - Foreground = ThemeBrushes.Get("VelaTextSecondary", Brushes.Gray) - }; + // .ui = VelaUiFont 11 + VelaTextSecondary(MongoStyles.axaml)。 + var label = new TextBlock { Text = loc["Cw_DateTime"], Margin = new Thickness(2, 0, 8, 0), Classes = { "ui" } }; DockPanel.SetDock(label, Dock.Left); var bar = new DockPanel { LastChildFill = false }; bar.Children.Add(buttons); bar.Children.Add(label); bar.Children.Add(time); + // 月历与时刻栏之间一条 VelaBorderPrimary 细线:上面是"选哪天",下面是"几点 + 怎么提交"。 + var divider = new Border { Height = 1, Background = ThemeBrushes.Get("VelaBorderPrimary", Brushes.Gray) }; + var flyout = new Flyout { - Content = new StackPanel { Spacing = 8, Children = { calendar, bar } }, + Content = new StackPanel { Spacing = 8, Children = { calendar, divider, bar } }, Placement = PlacementMode.BottomEdgeAlignedRight }; - IBrush? normalBorder = null; void Commit(DateTime day) { if (!TryParseTime(time.Text, out TimeSpan of)) { - normalBorder ??= time.BorderBrush; - time.BorderBrush = ThemeBrushes.Get("VelaError", Brushes.Red); + time.Classes.Set("invalid", true); ToolTip.SetTip(time, loc["Cw_DateBadTime"]); - time.Focus(); + _ = time.Focus(); return; } flyout.Hide(); @@ -116,9 +111,9 @@ void Commit(DateTime day) }; time.TextChanged += (_, _) => { - if (normalBorder is not null) + if (time.Classes.Contains("invalid")) { - time.BorderBrush = normalBorder; + time.Classes.Set("invalid", false); ToolTip.SetTip(time, loc["Doc_LocalTime"]); } }; @@ -126,6 +121,10 @@ void Commit(DateTime day) return flyout; } + /// 本插件的具名控件主题(MongoStyles.axaml)。锚点在引了那份样式的视图里,从它往上找得到。 + private static ControlTheme? ThemeOf(Control anchor, string key) => + anchor.TryFindResource(key, out object? found) ? found as ControlTheme : null; + /// 时刻框:9:30、09:30、09:30:15 都认。 internal static bool TryParseTime(string? text, out TimeSpan time) => TimeSpan.TryParseExact((text ?? "").Trim(), TimeFormats, CultureInfo.InvariantCulture, out time) diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorView.axaml.cs index e99c8f4..0378e0a 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorView.axaml.cs @@ -89,7 +89,7 @@ private void OnEditorLostFocus(object? sender, RoutedEventArgs e) { return; } - vm.CommitEdit(field, row); + _ = vm.CommitEdit(field, row); }); } @@ -106,7 +106,7 @@ private void OnDatePickClick(object? sender, RoutedEventArgs e) return; } e.Handled = true; - DatePickFlyout.Show(button, vm.Loc, editor.Text, text => + _ = DatePickFlyout.Show(button, vm.Loc, editor.Text, text => { editor.Text = text; if (vm.CommitEdit(field, row)) @@ -124,11 +124,11 @@ private void FocusSelected() => Dispatcher.UIThread.Post(() => { if (ViewModel?.SelectedField is { } selected && FieldList.ContainerFromItem(selected) is Control container) { - container.Focus(); + _ = container.Focus(); } else { - FieldList.Focus(); + _ = FieldList.Focus(); } }, DispatcherPriority.Loaded); @@ -145,14 +145,14 @@ private void OnKeyDown(object? sender, KeyEventArgs e) e.Handled = true; if (vm.CommitEdit(editing)) { - FieldList.Focus(); + _ = FieldList.Focus(); } } else if (e.Key == Key.Escape) { e.Handled = true; DocInspectorViewModel.CancelEdit(editing); - FieldList.Focus(); + _ = FieldList.Focus(); } return; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorViewModel.cs index 10dd5ac..9290d66 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocInspectorViewModel.cs @@ -11,7 +11,6 @@ namespace VelaShell.Plugin.Mongo.Ui; /// 检查器字段树的一行(设计稿 01 右侧:键 : 值 + 类型标签)。 internal sealed class InspectorField : ObservableObject { - private InlineValueEditor? _editor; /// 构造。 public InspectorField(string path, string key, int depth, BsonValue? value, bool isExpanded, ChangeRelation change, Loc loc) @@ -74,10 +73,9 @@ public InspectorField(string path, string key, int depth, BsonValue? value, bool /// 内联编辑器。 public InlineValueEditor? Editor { - get => _editor; - set + get; set { - if (SetProperty(ref _editor, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(IsEditing)); } @@ -85,7 +83,7 @@ public InlineValueEditor? Editor } /// 编辑中。 - public bool IsEditing => _editor is not null; + public bool IsEditing => Editor is not null; /// /// 检查器里的值写法:ObjectId 缩写、字符串带引号、Decimal 原样、日期 ISO; @@ -105,25 +103,25 @@ private static string Format(BsonValue? value, bool expanded, Loc loc) case BsonKind.Date: return BsonText.IsoDate(value!); case BsonKind.Object: - { - BsonDocument doc = value!.AsBsonDocument; - if (expanded || doc.ElementCount == 0) { - return $"{{{doc.ElementCount}}}"; + BsonDocument doc = value!.AsBsonDocument; + if (expanded || doc.ElementCount == 0) + { + return $"{{{doc.ElementCount}}}"; + } + string keys = string.Join(", ", doc.Names.Take(4)) + (doc.ElementCount > 4 ? ", …" : ""); + return $"{{ {keys} }}"; } - string keys = string.Join(", ", doc.Names.Take(4)) + (doc.ElementCount > 4 ? ", …" : ""); - return $"{{ {keys} }}"; - } case BsonKind.Array: - { - BsonArray array = value!.AsBsonArray; - if (expanded) { - return $"[{array.Count}]"; + BsonArray array = value!.AsBsonArray; + if (expanded) + { + return $"[{array.Count}]"; + } + string literal = "[" + string.Join(",", array.Select(static v => v is BsonDocument or BsonArray ? "…" : BsonText.Literal(v))) + "]"; + return literal.Length <= 40 ? literal : $"[{array.Count}]"; } - string literal = "[" + string.Join(",", array.Select(static v => v is BsonDocument or BsonArray ? "…" : BsonText.Literal(v))) + "]"; - return literal.Length <= 40 ? literal : $"[{array.Count}]"; - } default: return BsonText.Inline(value, loc, shortenIds: true); } @@ -141,54 +139,45 @@ internal sealed record CollectionInfoLine(string Label, string Value); /// internal sealed class DocInspectorViewModel : ObservableObject { - private readonly CollectionTabViewModel _owner; - private readonly HashSet _expanded = new(StringComparer.Ordinal); - private readonly HashSet _collapsed = new(StringComparer.Ordinal); - private CollectionRow? _row; - private int _page; - private ObservableCollection _fields = []; + private readonly HashSet _expanded = [with(StringComparer.Ordinal)]; + private readonly HashSet _collapsed = [with(StringComparer.Ordinal)]; private InspectorField? _selected; - private string _jsonText = ""; - private string _validationText = ""; - private string _validationIcon = "Mongo.shield"; - private string _validationToken = "VelaTextMuted"; - private IReadOnlyList _infoLines = []; /// 构造。 public DocInspectorViewModel(CollectionTabViewModel owner) { - _owner = owner; + Owner = owner; ShowJsonCommand = new RelayCommand(() => Page = 1); - CopyCommand = new AsyncCommand(() => _row is null ? Task.CompletedTask : owner.CopyDocumentsAsync([_row])); + CopyCommand = new AsyncCommand(() => Row is null ? Task.CompletedTask : owner.CopyDocumentsAsync([Row])); CloneCommand = new RelayCommand(() => { - if (_row is not null) + if (Row is not null) { - owner.Clone(_row); + owner.Clone(Row); } }); DeleteCommand = new RelayCommand(() => { - if (_row is not null) + if (Row is not null) { - owner.StageDelete([_row]); + owner.StageDelete([Row]); } }); AddFieldCommand = new RelayCommand(() => { - if (_row is not null) + if (Row is not null) { - owner.PromptAddField(_row, ""); + owner.PromptAddField(Row, ""); } }); DiscardCommand = new RelayCommand(() => { - if (_row is not null) + if (Row is not null) { - owner.DiscardDocument(_row); + owner.DiscardDocument(Row); } }); - ApplyCommand = new AsyncCommand(() => _row switch + ApplyCommand = new AsyncCommand(() => Row switch { { Insert: { } insert } => owner.CommitAsync(new CommitScope(OnlyInsert: insert)), { Id: { } id } => owner.CommitAsync(new CommitScope(id)), @@ -197,27 +186,26 @@ public DocInspectorViewModel(CollectionTabViewModel owner) } /// 文案表。 - public Loc Loc => _owner.Loc; + public Loc Loc => Owner.Loc; /// 所属工作台。 - public CollectionTabViewModel Owner => _owner; + public CollectionTabViewModel Owner { get; } /// 看着的行。 - public CollectionRow? Row => _row; + public CollectionRow? Row { get; private set; } /// 有没有行。 - public bool HasRow => _row is not null; + public bool HasRow => Row is not null; /// 头部的 #4。 - public string NumberText => _row is null ? "" : "#" + _row.NumberText; + public string NumberText => Row is null ? "" : "#" + Row.NumberText; /// 页签:0 字段、1 JSON、2 集合信息。 public int Page { - get => _page; - set + get; set { - if (SetProperty(ref _page, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsFieldsPage), nameof(IsJsonPage), nameof(IsInfoPage)); if (value == 1) @@ -235,7 +223,7 @@ public int Page /// 字段页。 public bool IsFieldsPage { - get => _page == 0; + get => Page == 0; set { if (value) @@ -248,7 +236,7 @@ public bool IsFieldsPage /// JSON 页。 public bool IsJsonPage { - get => _page == 1; + get => Page == 1; set { if (value) @@ -261,7 +249,7 @@ public bool IsJsonPage /// 集合信息页。 public bool IsInfoPage { - get => _page == 2; + get => Page == 2; set { if (value) @@ -272,11 +260,7 @@ public bool IsInfoPage } /// 字段树的可见行。 - public ObservableCollection Fields - { - get => _fields; - private set => SetProperty(ref _fields, value); - } + public ObservableCollection Fields { get; private set => SetProperty(ref field, value); } = []; /// 选中的字段。 public InspectorField? SelectedField @@ -286,45 +270,25 @@ public InspectorField? SelectedField } /// JSON 页的文本。 - public string JsonText - { - get => _jsonText; - private set => SetProperty(ref _jsonText, value); - } + public string JsonText { get; private set => SetProperty(ref field, value); } = ""; /// JSON 页的语法。 - public CodeLanguage JsonLanguage => _owner.Ejson == EjsonMode.Shell ? CodeLanguage.Shell : CodeLanguage.Json; + public CodeLanguage JsonLanguage => Owner.Ejson == EjsonMode.Shell ? CodeLanguage.Shell : CodeLanguage.Json; /// 底部验证状态文字。 - public string ValidationText - { - get => _validationText; - private set => SetProperty(ref _validationText, value); - } + public string ValidationText { get; private set => SetProperty(ref field, value); } = ""; /// 验证状态图标。 - public string ValidationIcon - { - get => _validationIcon; - private set => SetProperty(ref _validationIcon, value); - } + public string ValidationIcon { get; private set => SetProperty(ref field, value); } = "Mongo.shield"; /// 验证状态图标颜色。 - public string ValidationToken - { - get => _validationToken; - private set => SetProperty(ref _validationToken, value); - } + public string ValidationToken { get; private set => SetProperty(ref field, value); } = "VelaTextMuted"; /// 这份文档有暂存修改(「放弃 / 应用修改」可用)。 - public bool HasChanges => _row is { State: CollectionRowState.Modified or CollectionRowState.Added or CollectionRowState.Deleted }; + public bool HasChanges => Row is { State: CollectionRowState.Modified or CollectionRowState.Added or CollectionRowState.Deleted }; /// 「集合信息」页。 - public IReadOnlyList InfoLines - { - get => _infoLines; - private set => SetProperty(ref _infoLines, value); - } + public IReadOnlyList InfoLines { get; private set => SetProperty(ref field, value); } = []; /// JSON 页。 public RelayCommand ShowJsonCommand { get; } @@ -350,12 +314,12 @@ public IReadOnlyList InfoLines /// 换一行看。 public void Load(CollectionRow? row) { - if (!ReferenceEquals(row, _row)) + if (!ReferenceEquals(row, Row)) { _expanded.Clear(); _collapsed.Clear(); } - _row = row; + Row = row; RaisePropertiesChanged(nameof(Row), nameof(HasRow), nameof(NumberText)); Reload(); } @@ -364,30 +328,30 @@ public void Load(CollectionRow? row) public void Reload() { string? keep = _selected?.Path; - if (_row is null) + if (Row is null) { Fields = []; ValidationText = ""; RaisePropertyChanged(nameof(HasChanges)); return; } - BsonDocument doc = _row.Document; - StagedEdit? edit = _row.Insert is null ? _owner.Staging.EditOf(_row.Id) : null; + BsonDocument doc = Row.Document; + StagedEdit? edit = Row.Insert is null ? Owner.Staging.EditOf(Row.Id) : null; var list = new List(); Append(list, doc, "", 0, edit); // 这一页里别的文档有、这一份没有的顶层字段:「— 字段不存在」+「缺失」。 - foreach (CollectionColumn column in _owner.Columns) + foreach (CollectionColumn column in Owner.Columns) { if (!doc.Contains(column.Name)) { list.Add(new InspectorField(column.Name, column.Name, 0, null, false, ChangeRelation.None, Loc) { TypeText = Loc["Cw_TypeMissing"] }); } } - Fields = new ObservableCollection(list); + Fields = [with(list)]; _selected = keep is null ? null : list.FirstOrDefault(f => f.Path == keep); RaisePropertiesChanged(nameof(SelectedField), nameof(HasChanges), nameof(NumberText)); RefreshValidation(); - if (_page == 1) + if (Page == 1) { RefreshJson(); } @@ -448,13 +412,13 @@ public void Toggle(InspectorField field) } if (field.IsExpanded) { - _expanded.Remove(field.Path); - _collapsed.Add(field.Path); + _ = _expanded.Remove(field.Path); + _ = _collapsed.Add(field.Path); } else { - _collapsed.Remove(field.Path); - _expanded.Add(field.Path); + _ = _collapsed.Remove(field.Path); + _ = _expanded.Add(field.Path); } Reload(); } @@ -462,21 +426,21 @@ public void Toggle(InspectorField field) /// 开始编辑一个字段的值。 public void BeginEdit(InspectorField field) { - if (_row is null || field.Path == "_id" && _row.Insert is null || !_owner.EnsureCanWrite()) + if (Row is null || field.Path == "_id" && Row.Insert is null || !Owner.EnsureCanWrite()) { return; } - foreach (InspectorField other in _fields.Where(static f => f.IsEditing)) + foreach (InspectorField other in Fields.Where(static f => f.IsEditing)) { other.Editor = null; } - BsonKind fallback = _owner.Columns.FirstOrDefault(c => c.Name == field.Path)?.Kind ?? _owner.Sample.KindOf(field.Path); - field.Editor = _owner.CreateEditor(field.Value, field.Path, fallback); + BsonKind fallback = Owner.Columns.FirstOrDefault(c => c.Name == field.Path)?.Kind ?? Owner.Sample.KindOf(field.Path); + field.Editor = Owner.CreateEditor(field.Value, field.Path, fallback); SelectedField = field; } /// 提交一个字段的编辑(写入暂存区)。 - public bool CommitEdit(InspectorField field) => _row is not null && CommitEdit(field, _row); + public bool CommitEdit(InspectorField field) => Row is not null && CommitEdit(field, Row); /// /// 把一个字段的编辑提交到指定的那份文档。失焦提交要用它:点网格另一行时, @@ -484,7 +448,7 @@ public void BeginEdit(InspectorField field) /// internal bool CommitEdit(InspectorField field, CollectionRow row) { - if (field.Editor is not { } editor || !_owner.CommitEditor(editor, row, field.Path)) + if (field.Editor is not { } editor || !Owner.CommitEditor(editor, row, field.Path)) { return false; } @@ -498,16 +462,16 @@ internal bool CommitEdit(InspectorField field, CollectionRow row) /// 换类型。 public void ChangeType(InspectorField field, BsonKind kind) { - if (_row is not null && field.Path != "_id") + if (Row is not null && field.Path != "_id") { - _owner.ChangeType(_row, field.Path, kind); + Owner.ChangeType(Row, field.Path, kind); } } /// JSON 页文本(当前写法)。 public void RefreshJson() { - JsonText = _row is null ? "" : BsonText.Pretty(_row.Document, _owner.Ejson); + JsonText = Row is null ? "" : BsonText.Pretty(Row.Document, Owner.Ejson); RaisePropertyChanged(nameof(JsonLanguage)); } @@ -517,18 +481,18 @@ public void RefreshJson() /// public void RefreshValidation() { - if (_row is null) + if (Row is null) { return; } - if (_owner.Info.Validator is not { } validator) + if (Owner.Info.Validator is not { } validator) { ValidationText = Loc["Cw_NoValidator"]; ValidationIcon = "Mongo.shield"; ValidationToken = "VelaTextMuted"; return; } - IReadOnlyList violations = JsonSchemaValidator.Validate(validator, _row.Document, Loc); + IReadOnlyList violations = JsonSchemaValidator.Validate(validator, Row.Document, Loc); if (violations.Count == 0) { ValidationText = Loc["Cw_SchemaOk"]; @@ -546,10 +510,10 @@ public void RefreshValidation() /// 「集合信息」页:形态、文档数、大小、索引、验证规则。 private async Task LoadInfoAsync() { - CollectionInfo info = _owner.Info; - InfoLines = BuildInfo(info, _owner.Stats); - await _owner.LoadStatsAsync().ConfigureAwait(true); - InfoLines = BuildInfo(info, _owner.Stats); + CollectionInfo info = Owner.Info; + InfoLines = BuildInfo(info, Owner.Stats); + await Owner.LoadStatsAsync().ConfigureAwait(true); + InfoLines = BuildInfo(info, Owner.Stats); } private List BuildInfo(CollectionInfo info, CollectionStats? stats) diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeModels.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeModels.cs index 771792f..480a2a7 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeModels.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeModels.cs @@ -11,8 +11,6 @@ namespace VelaShell.Plugin.Mongo.Ui; /// internal sealed class DocTreeRow : ObservableObject { - private bool _isExpanded; - private InlineValueEditor? _editor; /// 构造。 /// 所属网格行(同一份文档)。 @@ -105,10 +103,10 @@ public DocTreeRow(CollectionRow row, string path, string key, BsonValue? value, /// 展开着。 public bool IsExpanded { - get => _isExpanded; + get; set { - if (SetProperty(ref _isExpanded, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(Chevron)); } @@ -116,15 +114,15 @@ public bool IsExpanded } /// 展开箭头。 - public string Chevron => _isExpanded ? "Mongo.chevron-down" : "Mongo.chevron-right"; + public string Chevron => IsExpanded ? "Mongo.chevron-down" : "Mongo.chevron-right"; /// 值列的内联编辑器。 public InlineValueEditor? Editor { - get => _editor; + get; set { - if (SetProperty(ref _editor, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(IsEditing)); } @@ -132,7 +130,7 @@ public InlineValueEditor? Editor } /// 编辑中。 - public bool IsEditing => _editor is not null; + public bool IsEditing => Editor is not null; /// 父路径(数组元素的父是数组)。 public string ParentPath => Path.LastIndexOf('.') is var dot and > 0 ? Path[..dot] : ""; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeView.axaml.cs index 743fc29..66fb37c 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocTreeView.axaml.cs @@ -101,13 +101,13 @@ private void OnDatePickClick(object? sender, RoutedEventArgs e) return; } e.Handled = true; - DatePickFlyout.Show(button, vm.Loc, editor.Text, text => + _ = DatePickFlyout.Show(button, vm.Loc, editor.Text, text => { editor.Text = text; if (vm.CommitEditor(editor, row.Row, row.Path)) { row.Editor = null; - TreeList.Focus(); + _ = TreeList.Focus(); } }); } @@ -126,14 +126,14 @@ private void OnKeyDown(object? sender, KeyEventArgs e) if (vm.CommitEditor(editor, editing.Row, editing.Path)) { editing.Editor = null; - TreeList.Focus(); + _ = TreeList.Focus(); } } else if (e.Key == Key.Escape) { e.Handled = true; editing.Editor = null; - TreeList.Focus(); + _ = TreeList.Focus(); } return; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogView.axaml.cs index 5f1c2e7..18f0d0a 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogView.axaml.cs @@ -1,6 +1,5 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Interactivity; @@ -119,7 +118,7 @@ private void FocusRow(DocumentEditorRow row, bool retry) { return; } - box.Focus(); + _ = box.Focus(); // 占位名(newField)全选:一打字就替换掉。 box.SelectAll(); } @@ -152,11 +151,11 @@ private void OnPreviewKeyDown(object? sender, KeyEventArgs e) { if (FocusedRow() is { } row) { - vm.AddAfter(row); + _ = vm.AddAfter(row); } else { - vm.AddFieldAtEnd(); + _ = vm.AddFieldAtEnd(); } } e.Handled = true; @@ -194,7 +193,7 @@ private bool OwnsKeyboard() /// 焦点所在的那一行(焦点控件的数据上下文)。 private DocumentEditorRow? FocusedRow() { - for (Visual? visual = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Visual; + for (var visual = TopLevel.GetTopLevel(this)?.FocusManager?.GetFocusedElement() as Visual; visual is not null && !ReferenceEquals(visual, this); visual = visual.GetVisualParent()) { @@ -281,7 +280,7 @@ private void OnGripReleased(object? sender, PointerReleasedEventArgs e) { if (_dragRow is { } row && _dropRow is { } target && !ReferenceEquals(row, target)) { - ViewModel?.Move(row, target, _dropAfter); + _ = (ViewModel?.Move(row, target, _dropAfter)); } EndDrag(); e.Pointer.Capture(null); @@ -315,7 +314,7 @@ private void OnKeyBoxGotFocus(object? sender, FocusChangedEventArgs e) private async Task SuggestAfterSampleAsync(DocumentEditorDialogViewModel vm, TextBox box) { - await vm.EnsureSampleAsync(reportErrors: false).ConfigureAwait(true); + _ = await vm.EnsureSampleAsync(reportErrors: false).ConfigureAwait(true); if (ReferenceEquals(_suggestBox, box) && box.IsFocused) { UpdateSuggestions(); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogViewModel.cs index 6b9ffab..bd7eaef 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDialogViewModel.cs @@ -44,37 +44,20 @@ internal sealed class DocumentEditorDialogViewModel : DialogViewModel private readonly List _roots = []; private readonly DocumentEditorTail _tail; - private readonly HashSet _expanded = new(StringComparer.Ordinal); - private readonly HashSet _collapsed = new(StringComparer.Ordinal); - private DocumentEditorMode _mode; - private bool _isSearching; - private string _search = ""; + private readonly HashSet _expanded = [with(StringComparer.Ordinal)]; + private readonly HashSet _collapsed = [with(StringComparer.Ordinal)]; private bool _recomputePending; private bool _jsonStale = true; private bool _applyingJson; private bool _regeneratingJson; private DispatcherTimer? _jsonTimer; - private string _jsonText = ""; private string? _jsonError; - private IReadOnlyList _jsonDiagnostics = []; - private string _previewText = ""; - private IReadOnlyList _previewMarks = []; - private string _commandName = ""; - private string _commandText = ""; - private string _commandNote = ""; - private string _diffLeft = ""; - private string _diffRight = ""; - private IReadOnlyList _diffLeftMarks = []; - private IReadOnlyList _diffRightMarks = []; - private string _idText = ""; private int _modified; private int _added; private int _removed; private int _errors; private int _warnings; - private bool _reordered; private bool _dirty; - private bool _busy; private bool _closed; private DocumentEditorPlan? _plan; private DocumentEditorSchema? _schema; @@ -110,7 +93,7 @@ public DocumentEditorDialogViewModel(IMongoWorkspace workspace, CollectionInfo c BsonDocument initial = Original?.DeepClone().AsBsonDocument ?? new BsonDocument("_id", ObjectId.GenerateNewId()); if (template) { - initial.AddRange(document!.DeepClone().AsBsonDocument); + _ = initial.AddRange(document!.DeepClone().AsBsonDocument); } LoadRows(initial, keepExpansion: false); _dirty = false; @@ -139,7 +122,7 @@ public DocumentEditorDialogViewModel(IMongoWorkspace workspace, CollectionInfo c public override double Height => 708; /// 有未保存的修改时 Esc 不关(误触一下就丢掉一屏编辑,代价太大)。 - public override bool CanCloseWithEscape => !_dirty && !_busy; + public override bool CanCloseWithEscape => !_dirty && !IsBusy; // ── 状态 ──────────────────────────────────────────────────────────────── @@ -155,10 +138,10 @@ public DocumentEditorDialogViewModel(IMongoWorkspace workspace, CollectionInfo c /// 正在保存。 public bool IsBusy { - get => _busy; + get; private set { - if (SetProperty(ref _busy, value)) + if (SetProperty(ref field, value)) { RaiseSaveState(); } @@ -168,25 +151,25 @@ private set /// 子标题行的 _id 芯片:ObjectId("…")。 public string IdText { - get => _idText; - private set => SetProperty(ref _idText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 当前模式。 public DocumentEditorMode Mode { - get => _mode; + get; set { - if (_mode == value) + if (field == value) { return; } - if (_mode == DocumentEditorMode.Json) + if (field == DocumentEditorMode.Json) { LeaveJson(); } - _mode = value; + field = value; if (value == DocumentEditorMode.Json && _jsonStale) { RegenerateJson(); @@ -199,7 +182,7 @@ public DocumentEditorMode Mode /// 表单模式(分段按钮)。 public bool IsFormMode { - get => _mode == DocumentEditorMode.Form; + get => Mode == DocumentEditorMode.Form; set { if (value) @@ -212,7 +195,7 @@ public bool IsFormMode /// JSON 模式。 public bool IsJsonMode { - get => _mode == DocumentEditorMode.Json; + get => Mode == DocumentEditorMode.Json; set { if (value) @@ -225,7 +208,7 @@ public bool IsJsonMode /// 对比模式。 public bool IsDiffMode { - get => _mode == DocumentEditorMode.Diff; + get => Mode == DocumentEditorMode.Diff; set { if (value) @@ -240,10 +223,10 @@ public bool IsDiffMode /// 查找框开着。 public bool IsSearching { - get => _isSearching; + get; set { - if (SetProperty(ref _isSearching, value) && !value) + if (SetProperty(ref field, value) && !value) { Search = ""; } @@ -253,15 +236,15 @@ public bool IsSearching /// 查找的字段名片段(过滤表单行;命中嵌套字段时连同它的祖先一起显示)。 public string Search { - get => _search; + get; set { - if (SetProperty(ref _search, value ?? "")) + if (SetProperty(ref field, value ?? "")) { RefreshVisible(); } } - } + } = ""; // ── 表单 ──────────────────────────────────────────────────────────────── @@ -282,15 +265,15 @@ public string Search /// JSON 模式的文本(mongosh 写法;改它 = 改表单)。 public string JsonText { - get => _jsonText; + get; set { value ??= ""; - if (_jsonText == value) + if (field == value) { return; } - _jsonText = value; + field = value; RaisePropertyChanged(); if (_regeneratingJson || !CanEdit) { @@ -302,79 +285,79 @@ public string JsonText _jsonTimer.Stop(); _jsonTimer.Start(); } - } + } = ""; /// JSON 的语法诊断。 public IReadOnlyList JsonDiagnostics { - get => _jsonDiagnostics; - private set => SetProperty(ref _jsonDiagnostics, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 对比:原文档。 public string DiffLeftText { - get => _diffLeft; - private set => SetProperty(ref _diffLeft, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 对比:当前。 public string DiffRightText { - get => _diffRight; - private set => SetProperty(ref _diffRight, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 对比:原文档里被删掉的行(红)。 public IReadOnlyList DiffLeftMarks { - get => _diffLeftMarks; - private set => SetProperty(ref _diffLeftMarks, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 对比:当前里新增的行(绿)。 public IReadOnlyList DiffRightMarks { - get => _diffRightMarks; - private set => SetProperty(ref _diffRightMarks, value); - } + get; + private set => SetProperty(ref field, value); + } = []; // ── 预览与命令 ────────────────────────────────────────────────────────── /// 实时预览(紧凑的 mongosh 文本)。 public string PreviewText { - get => _previewText; - private set => SetProperty(ref _previewText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 预览里与原文档不同的行。 public IReadOnlyList PreviewMarks { - get => _previewMarks; - private set => SetProperty(ref _previewMarks, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 将执行的命令名(updateOne / replaceOne / insertOne)。 public string CommandName { - get => _commandName; - private set => SetProperty(ref _commandName, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 将执行的命令全文。 public string CommandText { - get => _commandText; - private set => SetProperty(ref _commandText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 命令下面那行说明(只提交变更字段 · 乐观并发…)。 public string CommandNote { - get => _commandNote; - private set => SetProperty(ref _commandNote, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; // ── 底栏 ──────────────────────────────────────────────────────────────── @@ -409,7 +392,7 @@ public string CommandNote public bool HasWarnings => _warnings > 0; /// 顶层字段顺序变了(要整份替换)。 - public bool HasReordered => _reordered; + public bool HasReordered { get; private set; } /// 字段顺序已调整。 public string ReorderedText => Loc["Doc_Reordered"]; @@ -432,10 +415,10 @@ public string CommandNote : ""; /// 能保存。 - public bool CanSave => CanEdit && !_busy && _errors == 0 && _jsonError is null && HasChanges; + public bool CanSave => CanEdit && !IsBusy && _errors == 0 && _jsonError is null && HasChanges; /// 能另存为新文档。 - public bool CanSaveAsNew => CanEdit && !_busy && _errors == 0 && _jsonError is null; + public bool CanSaveAsNew => CanEdit && !IsBusy && _errors == 0 && _jsonError is null; // ── 命令 ──────────────────────────────────────────────────────────────── @@ -531,7 +514,7 @@ private void ApplyExpansion(DocumentEditorRow row) internal void RefreshVisible() { var visible = new List(); - string query = _search.Trim(); + string query = Search.Trim(); foreach (DocumentEditorRow root in _roots) { AddVisible(root, query, visible); @@ -652,8 +635,8 @@ internal void Toggle(DocumentEditorRow row) } row.IsExpanded = !row.IsExpanded; string path = row.Path; - (row.IsExpanded ? _expanded : _collapsed).Add(path); - (row.IsExpanded ? _collapsed : _expanded).Remove(path); + _ = (row.IsExpanded ? _expanded : _collapsed).Add(path); + _ = (row.IsExpanded ? _collapsed : _expanded).Remove(path); RefreshVisible(); ScheduleRecompute(); } @@ -667,11 +650,11 @@ internal void AddFrom(DocumentEditorRow row) } if (row.IsContainer) { - AddChild(row); + _ = AddChild(row); } else { - AddAfter(row); + _ = AddAfter(row); } } @@ -690,8 +673,8 @@ internal void AddFrom(DocumentEditorRow row) if (!container.IsExpanded) { container.IsExpanded = true; - _expanded.Add(container.Path); - _collapsed.Remove(container.Path); + _ = _expanded.Add(container.Path); + _ = _collapsed.Remove(container.Path); } OnEdited(child, structural: true); RequestFocus(child); @@ -750,7 +733,7 @@ internal void Remove(DocumentEditorRow row) { return; } - SiblingsOf(row).Remove(row); + _ = SiblingsOf(row).Remove(row); row.Parent?.OnChildrenChanged(); RaisePropertyChanged(nameof(NewDocumentText)); if (row.Parent is { } parent) @@ -807,7 +790,7 @@ internal bool Move(DocumentEditorRow row, DocumentEditorRow target, bool after) // _id 留在第一位(mongosh 与驱动都这么写,网格的列序也以它打头)。 after = true; } - siblings.Remove(row); + _ = siblings.Remove(row); int index = siblings.IndexOf(anchor) + (after ? 1 : 0); siblings.Insert(Math.Clamp(index, 0, siblings.Count), row); row.Parent?.OnChildrenChanged(); @@ -913,11 +896,11 @@ internal void RecomputeNow() BuildCommand(current); BuildPreview(current, context); - if (_mode == DocumentEditorMode.Diff) + if (Mode == DocumentEditorMode.Diff) { BuildDiff(current); } - if (_mode == DocumentEditorMode.Json && _jsonStale && !_applyingJson && _jsonError is null) + if (Mode == DocumentEditorMode.Json && _jsonStale && !_applyingJson && _jsonError is null) { RegenerateJson(current); } @@ -1020,7 +1003,7 @@ private void Evaluate(DocumentEditorRow row, BsonValue? parentOriginal, Dictiona SchemaViolation? violation = context.ByPath.GetValueOrDefault(row.Path); if (violation is not null) { - context.Consumed.Add(violation); + _ = context.Consumed.Add(violation); } DocumentEditorRowState state; @@ -1226,7 +1209,7 @@ private void BuildCommand(BsonDocument current) if (Original is null) { _plan = null; - _reordered = false; + HasReordered = false; CommandName = "insertOne"; CommandText = DocumentEditorFormat.Command(collectionRef, "insertOne", null, current); CommandNote = Loc["Doc_InsertNote"]; @@ -1234,7 +1217,7 @@ private void BuildCommand(BsonDocument current) } DocumentEditorPlan plan = DocumentEditorDiff.Plan(Original, current); _plan = plan; - _reordered = plan.Replace && !DocumentEditorDiff.SameOrder(Original, current, skipId: true); + HasReordered = plan.Replace && !DocumentEditorDiff.SameOrder(Original, current, skipId: true); BsonDocument? filter = BuildFilter(); bool versioned = Original.Contains(VersionField); if (plan.IsEmpty) @@ -1265,7 +1248,7 @@ private void BuildPreview(BsonDocument current, EvaluationContext context) { if (preview.LinePaths[i] is { } path) { - lineOf.TryAdd(path, i + 1); + _ = lineOf.TryAdd(path, i + 1); } } var marks = new Dictionary(); @@ -1339,19 +1322,19 @@ internal void ApplyJson() { return; } - List diagnostics = [.. ShellJson.Diagnose(_jsonText).Select(d => new EditorDiagnostic( + List diagnostics = [.. ShellJson.Diagnose(JsonText).Select(d => new EditorDiagnostic( d.Offset, Math.Max(1, d.Length), Loc.Format(d.MessageKey, d.Argument), DiagnosticSeverity.Error, d.Fix, d.Fix is null ? null : "Alt+↵"))]; BsonDocument document; try { - document = ShellJson.ParseDocument(_jsonText); + document = ShellJson.ParseDocument(JsonText); } catch (ShellJsonException ex) { _jsonError = ex.Message; if (diagnostics.Count == 0) { - int offset = ex.Offset >= 0 ? Math.Min(ex.Offset, Math.Max(0, _jsonText.Length - 1)) : Math.Max(0, _jsonText.TrimEnd().Length - 1); + int offset = ex.Offset >= 0 ? Math.Min(ex.Offset, Math.Max(0, JsonText.Length - 1)) : Math.Max(0, JsonText.TrimEnd().Length - 1); diagnostics.Add(new(offset, 1, Loc.Format("Doc_JsonError", ex.Message))); } JsonDiagnostics = diagnostics; @@ -1549,7 +1532,7 @@ internal List FillFrom(DocumentEditorSchema schema) { parent.OnChildrenChanged(); parent.IsExpanded = true; - _expanded.Add(parent.Path); + _ = _expanded.Add(parent.Path); } added.Add(field.Path); } @@ -1786,11 +1769,11 @@ internal static BsonDocument Apply(DocumentEditorPlan plan, BsonDocument target) { foreach (BsonElement unset in plan.Unset) { - BsonPath.Unset(target, unset.Name); + _ = BsonPath.Unset(target, unset.Name); } foreach (BsonElement set in plan.Set) { - BsonPath.Set(target, set.Name, set.Value.DeepClone()); + _ = BsonPath.Set(target, set.Name, set.Value.DeepClone()); } return target; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDiff.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDiff.cs index f85584b..6456a29 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDiff.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorDiff.cs @@ -173,11 +173,11 @@ public static (HashSet Removed, HashSet Added) Lines(IReadOnlyList Removed, HashSet Added) Lines(IReadOnlyList= lcs[p, q + 1]) { - removed.Add(start + p + 1); + _ = removed.Add(start + p + 1); p++; } else { - added.Add(start + q + 1); + _ = added.Add(start + q + 1); q++; } } for (; p < n; p++) { - removed.Add(start + p + 1); + _ = removed.Add(start + p + 1); } for (; q < m; q++) { - added.Add(start + q + 1); + _ = added.Add(start + q + 1); } return (removed, added); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorFormat.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorFormat.cs index 9cbaf79..e8c105c 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorFormat.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorFormat.cs @@ -78,32 +78,32 @@ internal static string Short(BsonValue value, Loc loc, int depth = 0) case BsonType.ObjectId: return $"ObjectId(\"{BsonText.Shorten(value.AsObjectId.ToString())}\")"; case BsonType.String: - { - string text = BsonText.OneLine(value.AsString); - return BsonText.Quote(text.Length > 60 ? text[..60] + "…" : text); - } + { + string text = BsonText.OneLine(value.AsString); + return BsonText.Quote(text.Length > 60 ? text[..60] + "…" : text); + } case BsonType.Document: - { - BsonDocument doc = value.AsBsonDocument; - return doc.ElementCount == 0 ? "{}" : $"{{ …{doc.ElementCount} }}"; - } - case BsonType.Array: - { - BsonArray array = value.AsBsonArray; - if (array.Count == 0) { - return "[]"; + BsonDocument doc = value.AsBsonDocument; + return doc.ElementCount == 0 ? "{}" : $"{{ …{doc.ElementCount} }}"; } - if (array.Count <= 6 && array.All(static v => v.BsonType is not (BsonType.Document or BsonType.Array))) + case BsonType.Array: { - string inline = "[ " + string.Join(", ", array.Select(v => Short(v, loc, depth + 1))) + " ]"; - if (inline.Length <= 60) + BsonArray array = value.AsBsonArray; + if (array.Count == 0) { - return inline; + return "[]"; } + if (array.Count <= 6 && array.All(static v => v.BsonType is not (BsonType.Document or BsonType.Array))) + { + string inline = "[ " + string.Join(", ", array.Select(v => Short(v, loc, depth + 1))) + " ]"; + if (inline.Length <= 60) + { + return inline; + } + } + return loc.Format("Doc_PreviewItems", array.Count); } - return loc.Format("Doc_PreviewItems", array.Count); - } default: return BsonText.Literal(value); } @@ -121,10 +121,10 @@ internal static string Short(BsonValue value, Loc loc, int depth = 0) public static string Command(string collectionRef, string method, BsonDocument? filter, BsonDocument body) { var b = new StringBuilder(); - b.Append("db.").Append(collectionRef).Append('.').Append(method).Append("(\n"); + _ = b.Append("db.").Append(collectionRef).Append('.').Append(method).Append("(\n"); if (filter is not null) { - b.Append(" ").Append(BsonText.Literal(filter)).Append(",\n"); + _ = b.Append(" ").Append(BsonText.Literal(filter)).Append(",\n"); } if (method == "updateOne") { @@ -132,9 +132,9 @@ public static string Command(string collectionRef, string method, BsonDocument? } else { - b.Append(Indent(BsonText.Pretty(body, EjsonMode.Shell), " ")); + _ = b.Append(Indent(BsonText.Pretty(body, EjsonMode.Shell), " ")); } - b.Append("\n)"); + _ = b.Append("\n)"); return b.ToString(); } @@ -147,21 +147,21 @@ private static void AppendUpdate(StringBuilder b, BsonDocument update) int op = 0; foreach (BsonElement element in update) { - b.Append(op++ == 0 ? " { " : " ").Append(element.Name).Append(": {"); + _ = b.Append(op++ == 0 ? " { " : " ").Append(element.Name).Append(": {"); if (element.Value is BsonDocument fields) { int i = 0; foreach (BsonElement field in fields) { - b.Append(i++ == 0 ? "\n" : ",\n"); - b.Append(" ").Append(BsonText.FieldName(field.Name)).Append(": ").Append(BsonText.Literal(field.Value)); + _ = b.Append(i++ == 0 ? "\n" : ",\n"); + _ = b.Append(" ").Append(BsonText.FieldName(field.Name)).Append(": ").Append(BsonText.Literal(field.Value)); } } - b.Append(op == update.ElementCount ? "\n } }" : "\n },\n"); + _ = b.Append(op == update.ElementCount ? "\n } }" : "\n },\n"); } if (update.ElementCount == 0) { - b.Append(" {}"); + _ = b.Append(" {}"); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorRow.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorRow.cs index 1a1589c..681f6df 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorRow.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentEditorRow.cs @@ -38,19 +38,10 @@ internal sealed class DocumentEditorRow : ObservableObject { private readonly DocumentEditorDialogViewModel _owner; private string _name; - private BsonKind _kind; private BsonValue _value = BsonNull.Value; private string _text = ""; private string? _parseError; - private string? _nameError; private bool _isExpanded; - private DocumentEditorRowState _state; - private string? _message; - private BsonValue? _originalValue; - private bool _isAddingItem; - private string _newItemText = ""; - private bool _newItemInvalid; - private IReadOnlyList _enumOptions = []; /// 构造(容器值会递归建出子行)。 /// 所属对话框。 @@ -115,7 +106,7 @@ public IReadOnlyList Segments public string SchemaPath => Parent is null ? _name : IsArrayItem ? Parent.SchemaPath : BsonPath.Join(Parent.SchemaPath, _name); /// 是不是数组里的一项(名字是下标,不能改)。 - public bool IsArrayItem => Parent?._kind == BsonKind.Array; + public bool IsArrayItem => Parent?.Kind == BsonKind.Array; /// 顶层 _id。 public bool IsId => Parent is null && _name == "_id"; @@ -168,8 +159,8 @@ public string Name /// 字段名本身的问题(空、重复、$ 开头、含点号);由视图模型在重算时填。 public string? NameError { - get => _nameError; - internal set => SetProperty(ref _nameError, value); + get; + internal set => SetProperty(ref field, value); } /// 数组项重排之后改下标。 @@ -186,15 +177,15 @@ internal void SetIndex(int index) // ── 类型与值 ──────────────────────────────────────────────────────────── /// 类型。 - public BsonKind Kind => _kind; + public BsonKind Kind { get; private set; } /// 类型下拉的选中项(设它 = 换类型)。 public BsonKind SelectedKind { - get => _kind; + get => Kind; set { - if (value != _kind) + if (value != Kind) { ChangeKind(value); } @@ -203,7 +194,7 @@ public BsonKind SelectedKind /// 类型下拉的可选项(设计稿 01 的类型菜单;当前是少见类型时把它也放进去,否则下拉显示为空)。 public IReadOnlyList KindOptions => - BsonKinds.Editable.Contains(_kind) ? BsonKinds.Editable : [.. BsonKinds.Editable, _kind]; + BsonKinds.Editable.Contains(Kind) ? BsonKinds.Editable : [.. BsonKinds.Editable, Kind]; /// 编辑框里的文本(标量)。 public string Text @@ -264,23 +255,22 @@ public string? EnumValue /// 验证规则里这条路径的 enum(字符串)。由视图模型在重算时填。 public IReadOnlyList EnumOptions { - get => _enumOptions; - internal set + get; internal set { - if (!_enumOptions.SequenceEqual(value)) + if (!field.SequenceEqual(value)) { - _enumOptions = value; + field = value; RaisePropertiesChanged(nameof(EnumOptions), nameof(EnumHint)); RaiseShape(); } } - } + } = []; /// enum: 普通 · VIP · SVIP。 - public string EnumHint => _owner.Loc.Format("Doc_EnumHint", string.Join(" · ", _enumOptions)); + public string EnumHint => _owner.Loc.Format("Doc_EnumHint", string.Join(" · ", EnumOptions)); /// 当前值(容器由子行拼出;解析失败的标量保留上一个合法值 —— 反正挡着保存)。 - public BsonValue BuildValue() => _kind switch + public BsonValue BuildValue() => Kind switch { BsonKind.Object => BuildDocument(Children), BsonKind.Array => new BsonArray(Children.Select(static c => c.BuildValue())), @@ -304,7 +294,7 @@ internal static BsonDocument BuildDocument(IEnumerable rows) /// 换类型:按 换算;换算不了的值重置并提示。 public void ChangeKind(BsonKind kind) { - if (!IsValueEditable || kind == _kind) + if (!IsValueEditable || kind == Kind) { RaisePropertyChanged(nameof(SelectedKind)); return; @@ -336,7 +326,7 @@ public void SetValue(BsonValue value) /// 设值但不通知(构造与重建时用)。 private void LoadValue(BsonValue value) { - _kind = BsonKinds.Of(value); + Kind = BsonKinds.Of(value); Children.Clear(); _parseError = null; if (value is BsonDocument document) @@ -365,7 +355,7 @@ private void LoadValue(BsonValue value) private void ParseText() { - if (BsonEdit.TryParse(_text, _kind, out BsonValue parsed, out string? error)) + if (BsonEdit.TryParse(_text, Kind, out BsonValue parsed, out string? error)) { _value = parsed; ParseError = null; @@ -393,7 +383,7 @@ private void GenerateId() // ── 容器 ──────────────────────────────────────────────────────────────── /// 是不是容器(对象 / 数组)。 - public bool IsContainer => BsonKinds.IsContainer(_kind); + public bool IsContainer => BsonKinds.IsContainer(Kind); /// 展开着。 public bool IsExpanded @@ -412,10 +402,10 @@ internal set internal bool ExpansionDecided { get; set; } /// 数组里全是标量(收起时画成一排芯片)。 - public bool IsScalarArray => _kind == BsonKind.Array && Children.All(static c => !c.IsContainer); + public bool IsScalarArray => Kind == BsonKind.Array && Children.All(static c => !c.IsContainer); /// 容器的个数徽记:{3} / [4]。 - public string ContainerCount => _kind == BsonKind.Object ? $"{{{Children.Count}}}" : $"[{Children.Count}]"; + public string ContainerCount => Kind == BsonKind.Object ? $"{{{Children.Count}}}" : $"[{Children.Count}]"; /// /// 容器摘要:对象给字段名(id, name, level),对象数组给每项的"名字 ×数量" @@ -425,7 +415,7 @@ public string ContainerSummary { get { - if (_kind == BsonKind.Object) + if (Kind == BsonKind.Object) { return string.Join(", ", Children.Take(8).Select(static c => c._name)) + (Children.Count > 8 ? ", …" : ""); } @@ -437,23 +427,23 @@ public string ContainerSummary /// 数组项的一句话摘要。 private string ItemSummary() { - if (_kind != BsonKind.Object) + if (Kind != BsonKind.Object) { return IsContainer ? ContainerCount : BsonText.Cell(_value); } - string? head = Children.FirstOrDefault(static c => c._kind == BsonKind.String)?._value.AsString; + string? head = Children.FirstOrDefault(static c => c.Kind == BsonKind.String)?._value.AsString; // 订单行那种"SKU ×数量":只认常见的数量字段名,免得把 { name, age } 写成"张三 ×30"。 DocumentEditorRow? count = Children.FirstOrDefault(static c => - c._name is "qty" or "quantity" or "count" or "num" or "amount" && BsonKinds.IsNumeric(c._kind)); + c._name is "qty" or "quantity" or "count" or "num" or "amount" && BsonKinds.IsNumeric(c.Kind)); string text = head is null ? ContainerCount : BsonText.OneLine(head); return count is null ? text : $"{text} ×{BsonText.Cell(count._value)}"; } /// 芯片上的字(标量数组的项)。 - public string ChipText => _kind == BsonKind.String ? BsonText.OneLine(_text) : BsonText.Cell(_value); + public string ChipText => Kind == BsonKind.String ? BsonText.OneLine(_text) : BsonText.Cell(_value); /// 「+ String」:追加一项时用的类型 —— 跟最后一项走,空数组给字符串。 - public BsonKind NewItemKind => Children.Count > 0 && !Children[^1].IsContainer ? Children[^1]._kind : BsonKind.String; + public BsonKind NewItemKind => Children.Count > 0 && !Children[^1].IsContainer ? Children[^1].Kind : BsonKind.String; /// 「+ String」上的字。 public string NewItemLabel => BsonKinds.Name(NewItemKind); @@ -461,10 +451,9 @@ private string ItemSummary() /// 正在芯片行里输入新的一项。 public bool IsAddingItem { - get => _isAddingItem; - set + get; set { - if (SetProperty(ref _isAddingItem, value) && !value) + if (SetProperty(ref field, value) && !value) { NewItemText = ""; } @@ -474,22 +463,17 @@ public bool IsAddingItem /// 新一项的文本。 public string NewItemText { - get => _newItemText; - set + get; set { - if (SetProperty(ref _newItemText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { NewItemInvalid = false; } } - } + } = ""; /// 新一项解析不了(输入框标红)。 - public bool NewItemInvalid - { - get => _newItemInvalid; - private set => SetProperty(ref _newItemInvalid, value); - } + public bool NewItemInvalid { get; private set => SetProperty(ref field, value); } /// /// 回车:追加这一项,输入框留着接着输下一项(打标签通常是一口气打好几个); @@ -497,12 +481,12 @@ public bool NewItemInvalid /// private void CommitItem() { - if (_newItemText.Length == 0) + if (NewItemText.Length == 0) { IsAddingItem = false; return; } - if (!BsonEdit.TryParse(_newItemText, NewItemKind, out BsonValue item, out _)) + if (!BsonEdit.TryParse(NewItemText, NewItemKind, out BsonValue item, out _)) { NewItemInvalid = true; return; @@ -514,14 +498,14 @@ private void CommitItem() /// 输入框失焦:有内容就追加,然后收起。 internal void FinishItem() { - if (!_isAddingItem) + if (!IsAddingItem) { return; } - if (_newItemText.Trim().Length > 0) + if (NewItemText.Trim().Length > 0) { CommitItem(); - if (_newItemInvalid) + if (NewItemInvalid) { // 解析不了的内容不悄悄丢掉:留着输入框(标红)让用户改。 return; @@ -533,7 +517,7 @@ internal void FinishItem() /// 子行增删之后重排下标、刷新摘要。 internal void OnChildrenChanged() { - if (_kind == BsonKind.Array) + if (Kind == BsonKind.Array) { for (int i = 0; i < Children.Count; i++) { @@ -548,68 +532,67 @@ internal void OnChildrenChanged() /// 原文档同一路径上的值(没有 = 新增);由视图模型在重算时填。 internal BsonValue? OriginalValue { - get => _originalValue; - set + get; set { - _originalValue = value; + field = value; RaisePropertyChanged(nameof(OriginalText)); } } /// 状态。 - public DocumentEditorRowState State => _state; + public DocumentEditorRowState State { get; private set; } /// 行尾那句话(错误 / 警告)。 - public string? Message => _message; + public string? Message { get; private set; } /// 重算后设状态。 internal void SetState(DocumentEditorRowState state, string? message) { - if (_state == state && _message == message) + if (State == state && Message == message) { return; } - _state = state; - _message = message; + State = state; + Message = message; RaisePropertiesChanged(nameof(State), nameof(Message), nameof(IsModified), nameof(IsAdded), nameof(IsError), nameof(IsValueError), nameof(IsWarning), nameof(ShowMessage), nameof(ShowRevert), nameof(ShowEnumHint), nameof(ShowDateExtras), nameof(IsoEcho), nameof(TimeZoneText)); } /// 已修改(橙底)。 - public bool IsModified => _state == DocumentEditorRowState.Modified; + public bool IsModified => State == DocumentEditorRowState.Modified; /// 新增(绿底)。 - public bool IsAdded => _state == DocumentEditorRowState.Added; + public bool IsAdded => State == DocumentEditorRowState.Added; /// 错误(红底)。 - public bool IsError => _state == DocumentEditorRowState.Error; + public bool IsError => State == DocumentEditorRowState.Error; /// 错误出在值上(值框标红;字段名的错误只把字段名标红)。 - public bool IsValueError => _state == DocumentEditorRowState.Error && NameError is null; + public bool IsValueError => State == DocumentEditorRowState.Error && NameError is null; /// 警告(validationAction = warn 时的违规)。 - public bool IsWarning => _state == DocumentEditorRowState.Warning; + public bool IsWarning => State == DocumentEditorRowState.Warning; /// 显示行尾的错误 / 警告。 - public bool ShowMessage => _message is not null && _state is DocumentEditorRowState.Error or DocumentEditorRowState.Warning; + public bool ShowMessage => Message is not null && State is DocumentEditorRowState.Error or DocumentEditorRowState.Warning; /// 显示「原值 … ↺ 还原」。 - public bool ShowRevert => _state == DocumentEditorRowState.Modified && _originalValue is not null && _owner.CanEdit; + public bool ShowRevert => State == DocumentEditorRowState.Modified && OriginalValue is not null && _owner.CanEdit; /// 原值 "paid"。 public string OriginalText { get { - if (_originalValue is null) + if (OriginalValue is null) { return ""; } // 原值只是提示:长字符串截到 16 个字符,别让它把输入框挤窄(完整的原值在对比模式里)。 - string text = _originalValue.IsString && _originalValue.AsString.Length > 16 - ? BsonText.Quote(BsonText.OneLine(_originalValue.AsString[..16]) + "…") - : BsonText.Inline(_originalValue, _owner.Loc, shortenIds: true); + string text = OriginalValue.IsString && OriginalValue.AsString.Length > 16 + ? BsonText.Quote(BsonText.OneLine(OriginalValue.AsString[..16]) + "…") + : BsonText.Inline(OriginalValue, _owner.Loc, shortenIds: true); return _owner.Loc.Format("Doc_WasValue", text); } } @@ -620,35 +603,35 @@ public string OriginalText // ── 值编辑器的形态 ────────────────────────────────────────────────────── /// 枚举下拉。 - public bool ShowEnum => _kind == BsonKind.String && _enumOptions.Count > 0 && !IsIdLocked; + public bool ShowEnum => Kind == BsonKind.String && EnumOptions.Count > 0 && !IsIdLocked; /// 宽文本框(字符串、UUID 与少见类型的字面量)。 - public bool ShowText => _kind is BsonKind.String or BsonKind.Uuid or BsonKind.Binary or BsonKind.Regex + public bool ShowText => Kind is BsonKind.String or BsonKind.Uuid or BsonKind.Binary or BsonKind.Regex or BsonKind.Timestamp or BsonKind.Other && !ShowEnum; /// 数值框(右对齐、160 宽)。 - public bool ShowNumber => BsonKinds.IsNumeric(_kind); + public bool ShowNumber => BsonKinds.IsNumeric(Kind); /// 开关。 - public bool ShowBool => _kind == BsonKind.Boolean; + public bool ShowBool => Kind == BsonKind.Boolean; /// 日期。 - public bool ShowDate => _kind == BsonKind.Date; + public bool ShowDate => Kind == BsonKind.Date; /// ObjectId(+ 生成)。 - public bool ShowObjectId => _kind == BsonKind.ObjectId; + public bool ShowObjectId => Kind == BsonKind.ObjectId; /// null(+ 设为字符串)。 - public bool ShowNull => _kind == BsonKind.Null; + public bool ShowNull => Kind == BsonKind.Null; /// 对象摘要。 - public bool ShowObjectSummary => _kind == BsonKind.Object; + public bool ShowObjectSummary => Kind == BsonKind.Object; /// 芯片(收起的标量数组)。 - public bool ShowChips => _kind == BsonKind.Array && !_isExpanded && IsScalarArray; + public bool ShowChips => Kind == BsonKind.Array && !_isExpanded && IsScalarArray; /// 数组摘要(对象数组,或展开着的数组)。 - public bool ShowArraySummary => _kind == BsonKind.Array && !ShowChips; + public bool ShowArraySummary => Kind == BsonKind.Array && !ShowChips; /// 「展开 / 收起」链接上的字。 public string ToggleText => _owner.Loc[_isExpanded ? "Doc_Collapse" : "Doc_Expand"]; @@ -657,7 +640,7 @@ public string OriginalText public string ChevronKey => _isExpanded ? "Mongo.chevron-down" : "Mongo.chevron-right"; /// 日期旁的时区芯片与 ISO 回显(改过、新加的日期才显示,免得每个日期行都拖一串)。 - public bool ShowDateExtras => ShowDate && _state is DocumentEditorRowState.Modified or DocumentEditorRowState.Added; + public bool ShowDateExtras => ShowDate && State is DocumentEditorRowState.Modified or DocumentEditorRowState.Added; /// 时区芯片:UTC+08:00(表单里的日期按本地时间填)。 public string TimeZoneText @@ -758,7 +741,7 @@ internal void RaiseEditability() } /// - public override string ToString() => $"{Path} ({BsonKinds.Name(_kind)})"; + public override string ToString() => $"{Path} ({BsonKinds.Name(Kind)})"; } /// 表单末尾那一格:文档级的问题(缺少必填字段…)+「添加字段」。 diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentText.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentText.cs index ad6ccd4..2d021e8 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentText.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/DocumentText.cs @@ -69,40 +69,40 @@ public void Container(BsonValue container, string? path, int depth) IReadOnlyList<(string Name, BsonValue Value)> children = container is BsonDocument doc ? [.. doc.Elements.Select(static e => (e.Name, e.Value))] : [.. container.AsBsonArray.Select(static (v, i) => (i.ToString(CultureInfo.InvariantCulture), v))]; - b.Append(isArray ? '[' : '{'); + _ = b.Append(isArray ? '[' : '{'); if (children.Count == 0) { - b.Append(isArray ? ']' : '}'); + _ = b.Append(isArray ? ']' : '}'); return; } for (int i = 0; i < children.Count; i++) { (string name, BsonValue value) = children[i]; string childPath = BsonPath.Join(path, name); - b.Append('\n'); + _ = b.Append('\n'); lines.Add(childPath); Indent(depth + 1); if (!isArray) { - b.Append(Key(name)).Append(": "); + _ = b.Append(Key(name)).Append(": "); } Value(value, childPath, depth + 1); if (i < children.Count - 1) { - b.Append(','); + _ = b.Append(','); } } - b.Append('\n'); + _ = b.Append('\n'); lines.Add(null); Indent(depth); - b.Append(isArray ? ']' : '}'); + _ = b.Append(isArray ? ']' : '}'); } private void Value(BsonValue value, string path, int depth) { if (Compact && value is BsonArray { Count: > 0 } array && array.Any(static v => v is BsonDocument or BsonArray)) { - b.Append("[ ").Append(fold!(array.Count)).Append(" ]"); + _ = b.Append("[ ").Append(fold!(array.Count)).Append(" ]"); return; } if (value is BsonDocument or BsonArray && !Fits(value)) @@ -110,27 +110,22 @@ private void Value(BsonValue value, string path, int depth) Container(value, path, depth); return; } - b.Append(Inline(value, nested: false)); + _ = b.Append(Inline(value, nested: false)); } /// 单行写法; 为真时(容器内部)预览写法会截短 ObjectId 与长字符串。 public string Inline(BsonValue value, bool nested) { - switch (value) + return value switch { - case BsonDocument doc: - return doc.ElementCount == 0 - ? "{}" - : "{ " + string.Join(", ", doc.Elements.Select(e => Key(e.Name) + ": " + Inline(e.Value, nested: true))) + " }"; - case BsonArray array: - return array.Count == 0 ? "[]" : "[ " + string.Join(", ", array.Select(v => Inline(v, nested: true))) + " ]"; - case BsonObjectId id when Compact && nested && mode == EjsonMode.Shell: - return $"ObjectId(\"{id.Value.ToString()[..12]}…\")"; - case BsonString text when Compact && nested && text.Value.Length > 24: - return BsonText.Quote(text.Value[..20] + "…"); - default: - return mode == EjsonMode.Shell ? BsonText.Literal(value) : BsonText.Compact(value, mode); - } + BsonDocument doc => doc.ElementCount == 0 + ? "{}" + : "{ " + string.Join(", ", doc.Elements.Select(e => Key(e.Name) + ": " + Inline(e.Value, nested: true))) + " }", + BsonArray array => array.Count == 0 ? "[]" : "[ " + string.Join(", ", array.Select(v => Inline(v, nested: true))) + " ]", + BsonObjectId id when Compact && nested && mode == EjsonMode.Shell => $"ObjectId(\"{id.Value.ToString()[..12]}…\")", + BsonString text when Compact && nested && text.Value.Length > 24 => BsonText.Quote(text.Value[..20] + "…"), + _ => mode == EjsonMode.Shell ? BsonText.Literal(value) : BsonText.Compact(value, mode), + }; } /// 压成一行放得下吗:只含标量、且不超过一行的宽度。 @@ -151,7 +146,6 @@ private bool Fits(BsonValue value) } } - /// /// 在一段(可能写了一半的)mongosh / JSON 文本里认出"光标落在哪个字段的键或值上",以及每个键在第几行。 /// 容错地扫:字符串、注释跳过,括号配对,键可以是裸标识符或带引号 —— 编辑器里边打字边问,文本多半不完整。 @@ -192,7 +186,6 @@ public static Result Scan(string text, int caret) var lines = new Dictionary(StringComparer.Ordinal); int line = 1; int i = 0; - int tokenStart = 0; while (i < (caret < 0 ? text.Length : end)) { char c = text[i]; @@ -200,7 +193,6 @@ public static Result Scan(string text, int caret) { line++; i++; - tokenStart = i; continue; } if (c is '"' or '\'') @@ -211,9 +203,9 @@ public static Result Scan(string text, int caret) // 带引号的键。 string key = text[(i + 1)..Math.Max(i + 1, close - 1)]; frame.Key = key; - lines.TryAdd(Join(frame.Path, key), line); + _ = lines.TryAdd(Join(frame.Path, key), line); } - tokenStart = i; + i = close; continue; } @@ -234,24 +226,21 @@ public static Result Scan(string text, int caret) } stack.Push(new Frame(path, c == '[')); i++; - tokenStart = i; continue; } if (c is '}' or ']') { if (stack.Count > 0) { - stack.Pop(); + _ = stack.Pop(); } i++; - tokenStart = i; continue; } if (c == ':' && stack.TryPeek(out Frame? obj) && !obj.IsArray) { obj.InValue = true; i++; - tokenStart = i; continue; } if (c == ',' && stack.TryPeek(out Frame? current)) @@ -266,7 +255,6 @@ public static Result Scan(string text, int caret) current.InValue = false; } i++; - tokenStart = i; continue; } if (ShellJson.IsIdentifierStart(c) && stack.TryPeek(out Frame? holder) && !holder.IsArray && !holder.InValue) @@ -277,14 +265,12 @@ public static Result Scan(string text, int caret) i++; } holder.Key = text[start..i]; - lines.TryAdd(Join(holder.Path, holder.Key), line); - tokenStart = start; + _ = lines.TryAdd(Join(holder.Path, holder.Key), line); continue; } if (char.IsWhiteSpace(c)) { i++; - tokenStart = i; continue; } i++; @@ -296,7 +282,7 @@ public static Result Scan(string text, int caret) } // 光标所在的那个词:往回找到空白 / 标点为止。 int wordStart = end; - while (wordStart > 0 && !char.IsWhiteSpace(text[wordStart - 1]) && text[wordStart - 1] is not (',' or ':' or '{' or '[' or '(' )) + while (wordStart > 0 && !char.IsWhiteSpace(text[wordStart - 1]) && text[wordStart - 1] is not (',' or ':' or '{' or '[' or '(')) { wordStart--; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridDrill.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridDrill.cs index bf2f15f..6d76541 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridDrill.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridDrill.cs @@ -53,22 +53,26 @@ private GridDrill(CollectionRow root, string path, bool isArray, int count) switch (BsonPath.Get(root.Document, path)) { case BsonArray array: - { - var drill = new GridDrill(root, path, isArray: true, array.Count); - drill.Columns = ArrayColumns(owner.Loc, array, previous); - for (int i = 0; i < array.Count; i++) { - rows.Add(new CollectionRow(owner, root, drill, i)); + var drill = new GridDrill(root, path, isArray: true, array.Count) + { + Columns = ArrayColumns(owner.Loc, array, previous) + }; + for (int i = 0; i < array.Count; i++) + { + rows.Add(new CollectionRow(owner, root, drill, i)); + } + return drill; } - return drill; - } case BsonDocument document: - { - var drill = new GridDrill(root, path, isArray: false, document.ElementCount); - drill.Columns = DocumentColumns([document], previous); - rows.Add(new CollectionRow(owner, root, drill, -1)); - return drill; - } + { + var drill = new GridDrill(root, path, isArray: false, document.ElementCount) + { + Columns = DocumentColumns([document], previous) + }; + rows.Add(new CollectionRow(owner, root, drill, -1)); + return drill; + } default: return null; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridEditBox.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridEditBox.cs index 2452fcf..7933a33 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridEditBox.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridEditBox.cs @@ -25,7 +25,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { if (IsEffectivelyVisible) { - Focus(); + _ = Focus(); SelectAll(); } }, DispatcherPriority.Input); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridModels.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridModels.cs index 23ef9f1..b635ce4 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridModels.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridModels.cs @@ -42,7 +42,6 @@ internal enum CollectionRowState internal sealed class CollectionColumn : ObservableObject { private double _width; - private int _sortDirection; /// 构造。 /// 字段名( 时只是列头文字)。 @@ -87,10 +86,9 @@ public double Width /// 排序方向:1 升序、-1 降序、0 不排序(列头的箭头)。 public int SortDirection { - get => _sortDirection; - set + get; set { - if (SetProperty(ref _sortDirection, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsSorted), nameof(SortIcon)); } @@ -98,10 +96,10 @@ public int SortDirection } /// 按这一列排序着。 - public bool IsSorted => _sortDirection != 0; + public bool IsSorted => SortDirection != 0; /// 排序箭头图标。 - public string SortIcon => _sortDirection > 0 ? "Mongo.arrow-up" : "Mongo.arrow-down"; + public string SortIcon => SortDirection > 0 ? "Mongo.arrow-up" : "Mongo.arrow-down"; } /// @@ -109,30 +107,27 @@ public int SortDirection /// internal sealed class CollectionRow : ObservableObject { - private readonly CollectionTabViewModel _owner; private IReadOnlyList? _cells; - private BsonDocument _document; - private CollectionRowState _state; /// 服务器上读到的一行。 public CollectionRow(CollectionTabViewModel owner, int number, BsonDocument original) { - _owner = owner; + Owner = owner; Root = this; Number = number; Original = original; - _document = original; + Document = original; Recompute(); } /// 暂存的新文档那一行。 public CollectionRow(CollectionTabViewModel owner, int number, StagedInsert insert) { - _owner = owner; + Owner = owner; Root = this; Number = number; Insert = insert; - _document = insert.Document; + Document = insert.Document; Recompute(); } @@ -146,13 +141,13 @@ public CollectionRow(CollectionTabViewModel owner, int number, StagedInsert inse /// public CollectionRow(CollectionTabViewModel owner, CollectionRow root, GridDrill drill, int index) { - _owner = owner; + Owner = owner; Root = root; Drill = drill; Number = index; Original = root.Original; Insert = root.Insert; - _document = root.Document; + Document = root.Document; Recompute(); } @@ -193,31 +188,31 @@ public string PathOf(CollectionColumn column) => public BsonValue? Id => Original?.GetValue("_id", BsonNull.Value); /// 所属工作台(单元格取文案用)。 - internal CollectionTabViewModel Owner => _owner; + internal CollectionTabViewModel Owner { get; } /// 界面上该显示的版本(原像 + 暂存修改)。 - public BsonDocument Document => _document; + public BsonDocument Document { get; private set; } /// 状态。 - public CollectionRowState State => _state; + public CollectionRowState State { get; private set; } /// 有暂存修改。 - public bool IsModified => _state == CollectionRowState.Modified; + public bool IsModified => State == CollectionRowState.Modified; /// 暂存的新文档。 - public bool IsAdded => _state == CollectionRowState.Added; + public bool IsAdded => State == CollectionRowState.Added; /// 暂存删除。 - public bool IsDeleted => _state == CollectionRowState.Deleted; + public bool IsDeleted => State == CollectionRowState.Deleted; /// 行号格里的状态图标(删除 −、新增 +)。 - public bool HasStateIcon => _state is CollectionRowState.Added or CollectionRowState.Deleted; + public bool HasStateIcon => State is CollectionRowState.Added or CollectionRowState.Deleted; /// 行号格状态图标的键。 - public string StateIcon => _state == CollectionRowState.Added ? "Mongo.plus" : "Mongo.minus"; + public string StateIcon => State == CollectionRowState.Added ? "Mongo.plus" : "Mongo.minus"; /// 行号格状态图标的颜色令牌。 - public string StateIconToken => _state == CollectionRowState.Added ? "VelaStatusConnected" : "VelaError"; + public string StateIconToken => State == CollectionRowState.Added ? "VelaStatusConnected" : "VelaError"; /// 单元格。 public IReadOnlyList Cells => _cells ??= BuildCells(); @@ -225,21 +220,21 @@ public string PathOf(CollectionColumn column) => /// 暂存区变了之后重算这一行(只对已经生成过单元格的行做实事)。 public void Recompute() { - StagingArea staging = _owner.Staging; + StagingArea staging = Owner.Staging; if (Insert is not null) { - _document = Insert.Document; - _state = CollectionRowState.Added; + Document = Insert.Document; + State = CollectionRowState.Added; } else if (staging.IsDeleted(Id)) { - _document = Original!; - _state = CollectionRowState.Deleted; + Document = Original!; + State = CollectionRowState.Deleted; } else { - _document = staging.Current(Original!); - _state = staging.EditOf(Id) is { IsEmpty: false } ? CollectionRowState.Modified : CollectionRowState.Normal; + Document = staging.Current(Original!); + State = staging.EditOf(Id) is { IsEmpty: false } ? CollectionRowState.Modified : CollectionRowState.Normal; } if (_cells is not null) { @@ -255,19 +250,19 @@ public void Recompute() private List BuildCells() { - StagedEdit? edit = Insert is null ? _owner.Staging.EditOf(Id) : null; - IReadOnlyList columns = Drill?.Columns ?? _owner.Columns; + StagedEdit? edit = Insert is null ? Owner.Staging.EditOf(Id) : null; + IReadOnlyList columns = Drill?.Columns ?? Owner.Columns; var cells = new List(columns.Count); foreach (CollectionColumn column in columns) { string path = PathOf(column); // 顶层按字段名直取(字段名里带点也取得到);子表按绝对路径走。 BsonValue? value = Drill is null - ? _document.TryGetValue(column.Name, out BsonValue v) ? v : null - : BsonPath.Get(_document, path); + ? Document.TryGetValue(column.Name, out BsonValue v) ? v : null + : BsonPath.Get(Document, path); ChangeRelation relation = edit?.RelationOf(path) ?? ChangeRelation.None; cells.Add(new CollectionCell(this, column, path, value, relation != ChangeRelation.None, - _owner.CurrentColumn == column && _owner.IsCurrentRow(this))); + Owner.CurrentColumn == column && Owner.IsCurrentRow(this))); } return cells; } @@ -279,7 +274,6 @@ private List BuildCells() internal sealed class CollectionCell : ObservableObject { private bool _isCurrent; - private InlineValueEditor? _editor; /// 构造。 public CollectionCell(CollectionRow row, CollectionColumn column, string path, BsonValue? value, bool isModified, bool isCurrent) @@ -362,33 +356,27 @@ public bool IsCurrent /// 编辑器;不在编辑为 。 public InlineValueEditor? Editor { - get => _editor; + get; set { - InlineValueEditor? previous = _editor; - if (SetProperty(ref _editor, value)) + InlineValueEditor? previous = field; + if (SetProperty(ref field, value)) { - if (previous is not null) - { - previous.PropertyChanged -= OnEditorChanged; - } - if (value is not null) - { - value.PropertyChanged += OnEditorChanged; - } + previous?.PropertyChanged -= OnEditorChanged; + value?.PropertyChanged += OnEditorChanged; RaisePropertiesChanged(nameof(IsEditing), nameof(HasEditorError)); } } } /// 正在编辑。 - public bool IsEditing => _editor is not null; + public bool IsEditing => Editor is not null; /// /// 编辑器里的值解析不了(单元格红框)。单元格模板绑它而不是 Editor.HasError: /// 绝大多数格子不在编辑,路径中段的 null 每一格都会报一条绑定错误。 /// - public bool HasEditorError => _editor?.HasError == true; + public bool HasEditorError => Editor?.HasError == true; private void OnEditorChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { @@ -414,15 +402,15 @@ private void OnEditorChanged(object? sender, System.ComponentModel.PropertyChang switch (kind) { case BsonKind.Object: - { - BsonDocument doc = value!.AsBsonDocument; - return doc.ElementCount == 0 ? ("{ }", loc["Cw_EmptyObject"]) : ($"{{{doc.ElementCount}}}", BsonText.Summary(doc)); - } + { + BsonDocument doc = value!.AsBsonDocument; + return doc.ElementCount == 0 ? ("{ }", loc["Cw_EmptyObject"]) : ($"{{{doc.ElementCount}}}", BsonText.Summary(doc)); + } case BsonKind.Array: - { - BsonArray array = value!.AsBsonArray; - return array.Count == 0 ? ("[ ]", loc.Format("Cw_ItemCount", 0)) : ($"[{array.Count}]", BsonText.ArraySummary(array)); - } + { + BsonArray array = value!.AsBsonArray; + return array.Count == 0 ? ("[ ]", loc.Format("Cw_ItemCount", 0)) : ($"[{array.Count}]", BsonText.ArraySummary(array)); + } default: return ("", BsonText.Cell(value)); } @@ -448,7 +436,6 @@ private static bool IsAscii(string text) internal sealed class InlineValueEditor : ObservableObject { private string _text; - private string? _error; /// 构造。 /// 初始文本()。 @@ -495,10 +482,10 @@ public string Text /// 解析失败的原因;没有为 。 public string? Error { - get => _error; + get; set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } @@ -506,5 +493,5 @@ public string? Error } /// 有错。 - public bool HasError => _error is not null; + public bool HasError => Error is not null; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridPaneView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridPaneView.axaml.cs index 1de708c..984c70a 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridPaneView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/GridPaneView.axaml.cs @@ -1,6 +1,5 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.VisualTree; @@ -24,10 +23,7 @@ public GridPaneView() GridList.TemplateApplied += (_, e) => { _rowsScroll = e.NameScope.Find("PART_ScrollViewer"); - if (_rowsScroll is not null) - { - _rowsScroll.ScrollChanged += (_, _) => HeaderScroll.Offset = new Vector(_rowsScroll.Offset.X, 0); - } + _rowsScroll?.ScrollChanged += (_, _) => HeaderScroll.Offset = new Vector(_rowsScroll.Offset.X, 0); }; GridList.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel, handledEventsToo: true); GridList.AddHandler(KeyDownEvent, OnKeyDown, RoutingStrategies.Tunnel); @@ -36,10 +32,7 @@ public GridPaneView() GridList.ContextRequested += OnContextRequested; GridList.SelectionChanged += (_, _) => { - if (_viewModel is not null) - { - _viewModel.GridSelectedRows = [.. GridList.SelectedItems?.OfType() ?? []]; - } + _viewModel?.GridSelectedRows = [.. GridList.SelectedItems?.OfType() ?? []]; }; // 分隔线上双击:按内容自动调列宽。 ColumnFit.OnGripDoubleClick(HeaderItems, column => @@ -61,7 +54,7 @@ public GridPaneView() internal void Attach(CollectionTabViewModel viewModel) { _viewModel = viewModel; - viewModel.RowAdded += row => GridList.ScrollIntoView(row); + viewModel.RowAdded += GridList.ScrollIntoView; } private void OnHeaderClick(object? sender, RoutedEventArgs e) @@ -261,11 +254,11 @@ private void FocusRow(CollectionRow row) { if (GridList.ContainerFromItem(row) is Control container) { - container.Focus(); + _ = container.Focus(); } else { - GridList.Focus(); + _ = GridList.Focus(); } } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardViewModel.cs index d5151c7..4802005 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardViewModel.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using MongoDB.Bson; using VelaShell.Plugin.Mongo.Analysis; using VelaShell.Plugin.Mongo.Bson; @@ -69,21 +70,9 @@ public JsonOutlineItem(string path, string name, int depth, BsonKind kind, bool internal sealed class JsonCardViewModel : ObservableObject { private readonly CollectionTabViewModel _owner; - private PrintedCard? _printed; - private IReadOnlyList? _marks; - private bool _isEditing; private string _editText = ""; private string _editStart = ""; - private IReadOnlyList _diagnostics = []; - private IReadOnlyList _editMarks = []; - private string _validity = ""; - private bool _isValid = true; - private int _caretLine = 1; - private int _caretColumn = 1; - private int _caretOffset; - private string _caretText = ""; private int _changeCount; - private IReadOnlyList _outline = []; private BsonDocument? _parsed; /// 构造。 @@ -122,13 +111,15 @@ public JsonCardViewModel(CollectionTabViewModel owner, CollectionRow row) /// /// 只读排版(按需算:JSON 视图里一页几万张卡片,虚拟化只会让看得见的那几张来取它)。 /// - private PrintedCard Printed => _printed ??= CardPrinter.Print(Row.Document, _owner.Ejson, Fold); + [AllowNull] + private PrintedCard Printed { get => field ??= CardPrinter.Print(Row.Document, _owner.Ejson, Fold); set; } /// 只读时的文本。 public string Text => Printed.Text; /// 只读时的行标记(暂存修改)。 - public IReadOnlyList Marks => _marks ??= BuildMarks(); + [AllowNull] + public IReadOnlyList Marks { get => field ??= BuildMarks(); private set; } /// 编辑器的语法:Shell 写法用 mongosh 着色,另外两种用 JSON。 public CodeLanguage Language => _owner.Ejson == EjsonMode.Shell ? CodeLanguage.Shell : CodeLanguage.Json; @@ -142,10 +133,10 @@ public JsonCardViewModel(CollectionTabViewModel owner, CollectionRow row) /// 编辑中。 public bool IsEditing { - get => _isEditing; + get; private set { - if (SetProperty(ref _isEditing, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsReadOnlyView), nameof(EditBadge)); _owner.OnCardEditingChanged(this); @@ -154,7 +145,7 @@ private set } /// 只读显示中。 - public bool IsReadOnlyView => !_isEditing; + public bool IsReadOnlyView => !IsEditing; /// 编辑中的文本。 public string EditText @@ -173,76 +164,76 @@ public string EditText /// 编辑器上的诊断。 public IReadOnlyList Diagnostics { - get => _diagnostics; - private set => SetProperty(ref _diagnostics, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 编辑器上的行标记(与原文档相比改了 / 加了的字段)。 public IReadOnlyList EditMarks { - get => _editMarks; - private set => SetProperty(ref _editMarks, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 语法正确 · 符合验证规则。 public string ValidityText { - get => _validity; - private set => SetProperty(ref _validity, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 语法与验证都通过。 public bool IsValid { - get => _isValid; + get; private set { - if (SetProperty(ref _isValid, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(ValidityIcon), nameof(ValidityToken)); } } - } + } = true; /// 校验图标。 - public string ValidityIcon => _isValid ? "Mongo.circle-check" : "Mongo.circle-x"; + public string ValidityIcon => IsValid ? "Mongo.circle-check" : "Mongo.circle-x"; /// 校验图标颜色。 - public string ValidityToken => _isValid ? "VelaStatusConnected" : "VelaError"; + public string ValidityToken => IsValid ? "VelaStatusConnected" : "VelaError"; /// 光标行(编辑器双向写回)。 public int CaretLine { - get => _caretLine; + get; set { - if (SetProperty(ref _caretLine, value)) + if (SetProperty(ref field, value)) { UpdateCaret(); } } - } + } = 1; /// 光标列。 public int CaretColumn { - get => _caretColumn; + get; set { - if (SetProperty(ref _caretColumn, value)) + if (SetProperty(ref field, value)) { UpdateCaret(); } } - } + } = 1; /// 光标偏移。 public int CaretOffset { - get => _caretOffset; + get; set { - if (SetProperty(ref _caretOffset, value)) + if (SetProperty(ref field, value)) { UpdateCaret(); } @@ -252,9 +243,9 @@ public int CaretOffset /// Ln 11, Col 13 · status: String · Shell 语法。 public string CaretText { - get => _caretText; - private set => SetProperty(ref _caretText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 头部「编辑中 · 2 处修改」。 public string EditBadge => Loc.Format("Cw_CardEditing", _changeCount); @@ -262,9 +253,9 @@ public string CaretText /// 大纲。 public IReadOnlyList Outline { - get => _outline; - private set => SetProperty(ref _outline, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 补全(键位置给字段名,值位置给抽样里的取值分布)。 public Func> Completion { get; } @@ -296,10 +287,10 @@ public IReadOnlyList Outline /// 暂存区 / 写法变了:重排只读文本与行标记。 public void Refresh() { - _printed = null; - _marks = null; + Printed = null; + Marks = null; RaisePropertiesChanged(nameof(Text), nameof(Marks), nameof(IdText), nameof(MetaText), nameof(Language), nameof(LineCount)); - if (!_isEditing) + if (!IsEditing) { RefreshOutline(); } @@ -308,7 +299,7 @@ public void Refresh() /// 进入编辑(当前写法的排版文本)。 public void BeginEdit() { - if (_isEditing || !_owner.EnsureCanWrite()) + if (IsEditing || !_owner.EnsureCanWrite()) { return; } @@ -326,7 +317,7 @@ public void BeginEdit() /// 取消编辑。 public void CancelEdit() { - if (!_isEditing) + if (!IsEditing) { return; } @@ -339,7 +330,7 @@ public void CancelEdit() /// 「更新文档」:解析、比对原文档、差异写进暂存区。有错不关编辑器。 public bool Update() { - if (!_isEditing) + if (!IsEditing) { return false; } @@ -392,9 +383,9 @@ public void UpdateCaret() { string type = ""; string path = ""; - if (_isEditing) + if (IsEditing) { - CaretPathScanner.Result at = CaretPathScanner.Scan(_editText, Math.Clamp(_caretOffset, 0, _editText.Length)); + CaretPathScanner.Result at = CaretPathScanner.Scan(_editText, Math.Clamp(CaretOffset, 0, _editText.Length)); path = at.Path; if (_owner.InlineTypeHints && path.Length > 0 && _parsed is not null) { @@ -402,8 +393,8 @@ public void UpdateCaret() type = value is null ? "" : $"{path}: {BsonKinds.Name(BsonKinds.Of(value))} · "; } } - CaretText = Loc.Format("Cw_CardCaret", _caretLine, _caretColumn, type, Language == CodeLanguage.Shell ? Loc["Cw_SyntaxShell"] : Loc["Cw_SyntaxJson"]); - if (_isEditing) + CaretText = Loc.Format("Cw_CardCaret", CaretLine, CaretColumn, type, Language == CodeLanguage.Shell ? Loc["Cw_SyntaxShell"] : Loc["Cw_SyntaxJson"]); + if (IsEditing) { RefreshOutline(path); } @@ -412,9 +403,9 @@ public void UpdateCaret() /// 重建大纲( 高亮)。 public void RefreshOutline(string? currentPath = null) { - BsonDocument doc = _isEditing && _parsed is not null ? _parsed : Row.Document; + BsonDocument doc = IsEditing && _parsed is not null ? _parsed : Row.Document; BsonDocument? baseline = Row.Original; - IReadOnlyDictionary lines = _isEditing + IReadOnlyDictionary lines = IsEditing ? CaretPathScanner.Scan(_editText, -1).KeyLines : Printed.LinePaths.Select((p, i) => (p, i)).Where(static t => t.p is not null) .GroupBy(static t => t.p!).ToDictionary(static g => g.Key, static g => g.First().i + 1); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardsView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardsView.axaml.cs index 54f55ef..0180a48 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardsView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Collection/JsonCardsView.axaml.cs @@ -48,7 +48,7 @@ private void OnKeyDown(object? sender, KeyEventArgs e) { card.Format(); } - card.Update(); + _ = card.Update(); break; case Key.Escape: e.Handled = true; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs index 88db60e..cd6cd16 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignModels.cs @@ -75,10 +75,6 @@ internal sealed class IndexRow : ObservableObject /// 使用列比例条的满宽。 public const double BarWidth = 60; - private bool _isBuilding; - private double _buildPercent; - private string _buildText = ""; - /// 索引名。 public required string Name { get; init; } @@ -139,10 +135,10 @@ internal sealed class IndexRow : ObservableObject /// 正在构建。 public bool IsBuilding { - get => _isBuilding; + get; set { - if (SetProperty(ref _isBuilding, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IconKey), nameof(IconToken), nameof(IsReady)); } @@ -150,15 +146,15 @@ public bool IsBuilding } /// 已建好(使用列画 ops 而不是进度)。 - public bool IsReady => !_isBuilding; + public bool IsReady => !IsBuilding; /// 构建进度(0–100)。 public double BuildPercent { - get => _buildPercent; + get; set { - if (SetProperty(ref _buildPercent, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(BuildWidth)); } @@ -166,20 +162,20 @@ public double BuildPercent } /// 进度条像素宽。 - public double BuildWidth => Math.Clamp(_buildPercent, 0, 100) / 100 * BarWidth; + public double BuildWidth => Math.Clamp(BuildPercent, 0, 100) / 100 * BarWidth; /// 进度文字(构建中 64% · 约 2 分)。 public string BuildText { - get => _buildText; - set => SetProperty(ref _buildText, value); - } + get; + set => SetProperty(ref field, value); + } = ""; /// 名称列图标。 - public string IconKey => _isBuilding ? "Mongo.loader-circle" : IsHidden ? "Mongo.eye-off" : "Mongo.key-round"; + public string IconKey => IsBuilding ? "Mongo.loader-circle" : IsHidden ? "Mongo.eye-off" : "Mongo.key-round"; /// 名称列图标颜色。 - public string IconToken => _isBuilding ? "VelaAccent" : IsUnused ? "VelaWarning" : "VelaTextTertiary"; + public string IconToken => IsBuilding ? "VelaAccent" : IsUnused ? "VelaWarning" : "VelaTextTertiary"; /// 名称颜色。 public string NameToken => IsUnused ? "VelaWarning" : IsHidden ? "VelaTextTertiary" : "VelaTextPrimary"; @@ -234,10 +230,6 @@ internal sealed class NewKeyRow : ObservableObject private string _field; private int _direction; private string _role; - private string _kind = "normal"; - private bool _isFirst; - private bool _isLast; - private int _number; /// 构造。 /// 字段。 @@ -273,17 +265,17 @@ public string Field /// 序号(1 起;表里第一列)。 public int Number { - get => _number; - set => SetProperty(ref _number, value); + get; + set => SetProperty(ref field, value); } /// 是最后一行(「下移」不可用)。 public bool IsLast { - get => _isLast; + get; set { - if (SetProperty(ref _isLast, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(CanMoveDown)); } @@ -291,10 +283,10 @@ public bool IsLast } /// 能上移。 - public bool CanMoveUp => !_isFirst; + public bool CanMoveUp => !IsFirst; /// 能下移。 - public bool CanMoveDown => !_isLast; + public bool CanMoveDown => !IsLast; /// 方向(1 / -1)。 public int Direction @@ -338,23 +330,23 @@ public string Role /// 当前索引类型(面板的分段:normal / text / 2dsphere / hashed / wildcard)。 public string Kind { - get => _kind; + get; set { - if (SetProperty(ref _kind, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(DirectionValue), nameof(HasDirection), nameof(DirectionText)); } } - } + } = "normal"; /// 是第一行(哈希索引只把第一行设成 hashed)。 public bool IsFirst { - get => _isFirst; + get; set { - if (SetProperty(ref _isFirst, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(DirectionValue), nameof(HasDirection), nameof(DirectionText), nameof(CanMoveUp)); } @@ -362,17 +354,17 @@ public bool IsFirst } /// 这一行的键值能不能选方向(普通索引、哈希索引的非首行)。 - public bool HasDirection => _kind == "normal" || (_kind == "hashed" && !_isFirst) || _kind == "wildcard"; + public bool HasDirection => Kind == "normal" || (Kind == "hashed" && !IsFirst) || Kind == "wildcard"; /// 方向下拉上的字。 public string DirectionText => _label(DirectionValue); /// 这一行在键模式里的值。 - public BsonValue DirectionValue => _kind switch + public BsonValue DirectionValue => Kind switch { "text" => "text", "2dsphere" => "2dsphere", - "hashed" when _isFirst => "hashed", + "hashed" when IsFirst => "hashed", "wildcard" => 1, _ => _direction }; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Indexes.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Indexes.cs index 791cd61..615fb32 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Indexes.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Indexes.cs @@ -11,13 +11,8 @@ namespace VelaShell.Plugin.Mongo.Ui; /// 集合设计 · 索引与建议(设计稿 07)。 internal sealed partial class DesignTabViewModel { - private IndexRow? _selectedIndex; private long _totalIndexSize; - private int _readyIndexCount; - private string _indexStatsSince = ""; private Task? _indexesTask; - private bool _hasAdvice; - private string _adviceEmpty = ""; private CancellationTokenSource? _buildPoll; private CollectionStats? _indexStats; @@ -30,10 +25,10 @@ internal sealed partial class DesignTabViewModel /// 选中的索引(隐藏 / 删除作用的对象)。 public IndexRow? SelectedIndex { - get => _selectedIndex; + get; set { - if (SetProperty(ref _selectedIndex, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(CanHideSelected), nameof(CanDropSelected), nameof(HideLabel), nameof(HideIcon)); ToggleHiddenCommand.RaiseCanExecuteChanged(); @@ -45,51 +40,51 @@ public IndexRow? SelectedIndex /// 已建好的索引个数(子页条上的计数)。 public int ReadyIndexCount { - get => _readyIndexCount; - private set => SetProperty(ref _readyIndexCount, value); + get; + private set => SetProperty(ref field, value); } /// 工具行右侧:$indexStats · 自 2026-09-12 起统计。 public string IndexStatsSince { - get => _indexStatsSince; - private set => SetProperty(ref _indexStatsSince, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 有建议卡片。 public bool HasAdvice { - get => _hasAdvice; - private set => SetProperty(ref _hasAdvice, value); + get; + private set => SetProperty(ref field, value); } /// 没有建议时的那行说明。 public string AdviceEmpty { - get => _adviceEmpty; + get; private set { - if (SetProperty(ref _adviceEmpty, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasAdviceEmpty)); } } - } + } = ""; /// 显示空态说明。 - public bool HasAdviceEmpty => _adviceEmpty.Length > 0; + public bool HasAdviceEmpty => AdviceEmpty.Length > 0; /// 选中的索引能隐藏 / 取消隐藏(_id_ 与构建中的不行)。 - public bool CanHideSelected => _selectedIndex is { IsId: false, IsBuilding: false }; + public bool CanHideSelected => SelectedIndex is { IsId: false, IsBuilding: false }; /// 选中的索引能删(_id_ 不行)。 - public bool CanDropSelected => _selectedIndex is { IsId: false }; + public bool CanDropSelected => SelectedIndex is { IsId: false }; /// 隐藏按钮的字:选中的已隐藏就是「取消隐藏」。 - public string HideLabel => _selectedIndex?.IsHidden == true ? Loc["Design_Unhide"] : Loc["Design_Hide"]; + public string HideLabel => SelectedIndex?.IsHidden == true ? Loc["Design_Unhide"] : Loc["Design_Hide"]; /// 隐藏按钮的图标。 - public string HideIcon => _selectedIndex?.IsHidden == true ? "Mongo.eye" : "Mongo.eye-off"; + public string HideIcon => SelectedIndex?.IsHidden == true ? "Mongo.eye" : "Mongo.eye-off"; /// 隐藏 / 取消隐藏选中的索引。 public AsyncCommand ToggleHiddenCommand { get; private set; } = null!; @@ -105,8 +100,8 @@ private set private void InitializeIndexes() { - ToggleHiddenCommand = new(() => _selectedIndex is { } row ? SetHiddenAsync(row, !row.IsHidden) : Task.CompletedTask, () => CanHideSelected); - DropIndexCommand = new(() => _selectedIndex is { } row ? DropIndexAsync(row) : Task.CompletedTask, () => CanDropSelected); + ToggleHiddenCommand = new(() => SelectedIndex is { } row ? SetHiddenAsync(row, !row.IsHidden) : Task.CompletedTask, () => CanHideSelected); + DropIndexCommand = new(() => SelectedIndex is { } row ? DropIndexAsync(row) : Task.CompletedTask, () => CanDropSelected); RefreshIndexesCommand = new(LoadIndexesAsync); AdviceActionCommand = new(static card => card.Action()); } @@ -160,7 +155,7 @@ private async Task LoadIndexesCoreAsync() { if (!building && await IsMultiKeyAsync(spec).ConfigureAwait(true)) { - multiKey.Add(spec.GetValue("name", "").ToString()!); + _ = multiKey.Add(spec.GetValue("name", "").ToString()!); } } @@ -170,7 +165,7 @@ private async Task LoadIndexesCoreAsync() foreach ((BsonDocument spec, bool building) in specs) { string name = spec.GetValue("name", "").ToString()!; - bool hasUsage = usage.TryGetValue(name, out var u); + bool hasUsage = usage.TryGetValue(name, out (long Ops, DateTime Since) u); bool unused = hasUsage && u.Ops == 0 && name != "_id_" && !building && (now - u.Since).TotalDays >= IndexAdvisor.UnusedWindowDays; IndexForm form = IndexAdvisor.ShapeOf(spec, multiKey.Contains(name)); @@ -199,7 +194,7 @@ private async Task LoadIndexesCoreAsync() rows.Add(row); } - string? selected = _selectedIndex?.Name; + string? selected = SelectedIndex?.Name; Indexes.Clear(); foreach (IndexRow row in rows) { @@ -474,45 +469,45 @@ private AdviceCard Card(AdvisorSuggestion s) switch (s.Kind) { case AdvisorKind.CreateIndex: - { - AdvisorQueryGroup q = s.Queries!; - return new AdviceCard { - Suggestion = s, - IconKey = "Mongo.zap", - IconToken = "VelaWarning", - Title = Loc.Format("Design_AdviceCreateTitle", FieldSet(q.Shape.Fields), PlanLabel(q)), - Detail = Loc.Format("Design_AdviceCreateDetail", BsonText.Grouped(q.Count), Math.Round(q.AverageMillis), - BsonText.Count((long)q.AverageExamined), BsonText.Count((long)Math.Round(q.AverageReturned))), - Code = BsonText.Literal(s.Key!), - ActionLabel = Loc["Design_AdviceCreate"], - IsPrimary = true, - Action = () => + AdvisorQueryGroup q = s.Queries!; + return new AdviceCard { - OpenCreatePanel(s.Key!, q.Shape); - return Task.CompletedTask; - } - }; - } + Suggestion = s, + IconKey = "Mongo.zap", + IconToken = "VelaWarning", + Title = Loc.Format("Design_AdviceCreateTitle", FieldSet(q.Shape.Fields), PlanLabel(q)), + Detail = Loc.Format("Design_AdviceCreateDetail", BsonText.Grouped(q.Count), Math.Round(q.AverageMillis), + BsonText.Count((long)q.AverageExamined), BsonText.Count((long)Math.Round(q.AverageReturned))), + Code = BsonText.Literal(s.Key!), + ActionLabel = Loc["Design_AdviceCreate"], + IsPrimary = true, + Action = () => + { + OpenCreatePanel(s.Key!, q.Shape); + return Task.CompletedTask; + } + }; + } case AdvisorKind.ExtendIndex: - { - AdvisorQueryGroup q = s.Queries!; - return new AdviceCard { - Suggestion = s, - IconKey = "Mongo.copy-minus", - IconToken = "VelaTextTertiary", - Title = Loc.Format("Design_AdviceExtendTitle", s.Index!.Name), - Detail = Loc.Format("Design_AdviceExtendDetail", BsonText.Grouped(q.Count), - q.InMemorySort ? Loc["Design_AdviceInMemorySort"] : Loc["Design_AdviceOverScan"], BsonText.Literal(s.Key!)), - ActionLabel = Loc["Design_AdviceViewQuery"], - Action = () => + AdvisorQueryGroup q = s.Queries!; + return new AdviceCard { - Workspace.OpenQuery(Database, QueryText(q)); - return Task.CompletedTask; - } - }; - } + Suggestion = s, + IconKey = "Mongo.copy-minus", + IconToken = "VelaTextTertiary", + Title = Loc.Format("Design_AdviceExtendTitle", s.Index!.Name), + Detail = Loc.Format("Design_AdviceExtendDetail", BsonText.Grouped(q.Count), + q.InMemorySort ? Loc["Design_AdviceInMemorySort"] : Loc["Design_AdviceOverScan"], BsonText.Literal(s.Key!)), + ActionLabel = Loc["Design_AdviceViewQuery"], + Action = () => + { + Workspace.OpenQuery(Database, QueryText(q)); + return Task.CompletedTask; + } + }; + } case AdvisorKind.RedundantIndex: return new AdviceCard { @@ -525,19 +520,19 @@ private AdviceCard Card(AdvisorSuggestion s) Action = () => Indexes.FirstOrDefault(r => r.Name == s.Index.Name) is { } row ? SetHiddenAsync(row, true) : Task.CompletedTask }; default: - { - double share = _totalIndexSize > 0 ? s.Index!.Size * 100.0 / _totalIndexSize : 0; - return new AdviceCard { - Suggestion = s, - IconKey = "Mongo.archive-x", - IconToken = "VelaWarning", - Title = Loc.Format("Design_AdviceUnusedTitle", s.Index!.Name, s.UnusedDays), - Detail = Loc.Format("Design_AdviceUnusedDetail", BsonText.Bytes(s.Index.Size), share.ToString("0.#", CultureInfo.InvariantCulture)), - ActionLabel = Loc["Design_AdviceHide"], - Action = () => Indexes.FirstOrDefault(r => r.Name == s.Index.Name) is { } row ? SetHiddenAsync(row, true) : Task.CompletedTask - }; - } + double share = _totalIndexSize > 0 ? s.Index!.Size * 100.0 / _totalIndexSize : 0; + return new AdviceCard + { + Suggestion = s, + IconKey = "Mongo.archive-x", + IconToken = "VelaWarning", + Title = Loc.Format("Design_AdviceUnusedTitle", s.Index!.Name, s.UnusedDays), + Detail = Loc.Format("Design_AdviceUnusedDetail", BsonText.Bytes(s.Index.Size), share.ToString("0.#", CultureInfo.InvariantCulture)), + ActionLabel = Loc["Design_AdviceHide"], + Action = () => Indexes.FirstOrDefault(r => r.Name == s.Index.Name) is { } row ? SetHiddenAsync(row, true) : Task.CompletedTask + }; + } } } @@ -673,19 +668,19 @@ internal async Task SetHiddenAsync(IndexRow row, bool hidden) return; } if (Workspace.Guard.ConfirmWrites && !await Workspace.ConfirmAsync(new() - { - Title = hidden ? Loc["Design_HideTitle"] : Loc["Design_UnhideTitle"], - Message = Loc.Format(hidden ? "Design_HideBody" : "Design_UnhideBody", row.Name, Namespace), - ConfirmLabel = hidden ? Loc["Design_Hide"] : Loc["Design_Unhide"], - IconKey = hidden ? "Mongo.eye-off" : "Mongo.eye", - Danger = false - }).ConfigureAwait(true)) + { + Title = hidden ? Loc["Design_HideTitle"] : Loc["Design_UnhideTitle"], + Message = Loc.Format(hidden ? "Design_HideBody" : "Design_UnhideBody", row.Name, Namespace), + ConfirmLabel = hidden ? Loc["Design_Hide"] : Loc["Design_Unhide"], + IconKey = hidden ? "Mongo.eye-off" : "Mongo.eye", + Danger = false + }).ConfigureAwait(true)) { return; } try { - await Workspace.Connection.RunCommandAsync(Database, new BsonDocument + _ = await Workspace.Connection.RunCommandAsync(Database, new BsonDocument { { "collMod", CollectionName }, { "index", new BsonDocument { { "name", row.Name }, { "hidden", hidden } } } @@ -744,7 +739,7 @@ internal async Task DropIndexAsync(IndexRow row) } try { - await Workspace.Connection.RunCommandAsync(Database, new BsonDocument + _ = await Workspace.Connection.RunCommandAsync(Database, new BsonDocument { { "dropIndexes", CollectionName }, { "index", row.Name } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs index 28db521..bb161ee 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.NewIndex.cs @@ -16,33 +16,23 @@ namespace VelaShell.Plugin.Mongo.Ui; /// internal sealed partial class DesignTabViewModel { - private bool _isCreateOpen; - private IndexRow? _draftIndex; private string _newKind = "normal"; private bool _newUnique; private bool _newSparse; private bool _newHidden; private bool _newTtl; - private string _newTtlSeconds = "86400"; private bool _newPartial; private string _newPartialText = "{ }"; - private string _newName = ""; private bool _nameEdited; private bool _settingName; - private string _newImpactTitle = ""; - private string _newImpactDetail = ""; - private string _newCommand = ""; - private string _newError = ""; - private bool _isCreating; - private IReadOnlyList _fieldOptions = []; /// 编辑器开着(下半部分从索引建议换成编辑器,索引表收矮、末尾挂草稿行)。 public bool IsCreateOpen { - get => _isCreateOpen; + get; set { - if (SetProperty(ref _isCreateOpen, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasDraft)); } @@ -52,10 +42,10 @@ public bool IsCreateOpen /// 索引表末尾的「新建」草稿行;还没有字段时为 。 public IndexRow? DraftIndex { - get => _draftIndex; + get; private set { - if (SetProperty(ref _draftIndex, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasDraft)); } @@ -63,7 +53,7 @@ private set } /// 草稿行可见(创建期间表里挂的是「构建中」那一行,草稿让位)。 - public bool HasDraft => _isCreateOpen && _draftIndex is not null && !_isCreating; + public bool HasDraft => IsCreateOpen && DraftIndex is not null && !IsCreating; /// 编辑器底栏左侧那句「将在 shop.orders 上执行 createIndexes」。 public string NewTarget => Loc.Format("Design_EditorTarget", Namespace); @@ -143,19 +133,19 @@ public bool NewTtl /// TTL 秒数。 public string NewTtlSeconds { - get => _newTtlSeconds; + get; set { - if (SetProperty(ref _newTtlSeconds, value ?? "")) + if (SetProperty(ref field, value ?? "")) { RaisePropertyChanged(nameof(NewTtlHuman)); RecomputeNewIndex(); } } - } + } = "86400"; /// TTL 秒数换算成人话。 - public string NewTtlHuman => long.TryParse(_newTtlSeconds, out long s) ? "= " + Duration(s) : ""; + public string NewTtlHuman => long.TryParse(NewTtlSeconds, out long s) ? "= " + Duration(s) : ""; /// 部分索引。 public bool NewPartial @@ -186,17 +176,17 @@ public string NewPartialText /// 索引名(默认按键自动生成;手改过就不再跟着变)。 public string NewName { - get => _newName; + get; set { - if (SetProperty(ref _newName, value ?? "") && !_settingName) + if (SetProperty(ref field, value ?? "") && !_settingName) { - _nameEdited = _newName.Length > 0; + _nameEdited = field.Length > 0; RaisePropertyChanged(nameof(NewNameHint)); RecomputeNewIndex(); } } - } + } = ""; /// 名称右侧的小字:自动生成 / 已自定义。 public string NewNameHint => _nameEdited ? Loc["Design_NameCustom"] : Loc["Design_NameAuto"]; @@ -204,48 +194,48 @@ public string NewName /// 预估框第一行(预计大小 ≈ 140 MB · 构建约 3 分钟)。 public string NewImpactTitle { - get => _newImpactTitle; - private set => SetProperty(ref _newImpactTitle, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 预估框说明。 public string NewImpactDetail { - get => _newImpactDetail; - private set => SetProperty(ref _newImpactDetail, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 命令预览。 public string NewCommand { - get => _newCommand; - private set => SetProperty(ref _newCommand, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 表单错误(创建按钮因此不可用);没有为空。 public string NewError { - get => _newError; + get; private set { - if (SetProperty(ref _newError, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasNewError)); CreateIndexCommand.RaiseCanExecuteChanged(); } } - } + } = ""; /// 有表单错误。 - public bool HasNewError => _newError.Length > 0; + public bool HasNewError => NewError.Length > 0; /// 正在创建(按钮转圈)。 public bool IsCreating { - get => _isCreating; + get; private set { - if (SetProperty(ref _isCreating, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasDraft)); CreateIndexCommand.RaiseCanExecuteChanged(); @@ -256,10 +246,10 @@ private set /// 抽样到的字段(字段下拉的选项)。 public IReadOnlyList FieldOptions { - get => _fieldOptions; + get; private set { - if (SetProperty(ref _fieldOptions, value)) + if (SetProperty(ref field, value)) { foreach (NewKeyRow row in NewKeys) { @@ -267,7 +257,7 @@ private set } } } - } + } = []; /// 打开面板(空白表单)。 public RelayCommand OpenCreateCommand { get; private set; } = null!; @@ -297,13 +287,13 @@ private void InitializeNewIndex() AddKeyCommand = new(() => AddKey("", 1, "")); RemoveKeyCommand = new(row => { - NewKeys.Remove(row); + _ = NewKeys.Remove(row); RenumberKeys(); RecomputeNewIndex(); }); MoveKeyUpCommand = new(row => MoveKey(NewKeys.IndexOf(row), NewKeys.IndexOf(row) - 1)); MoveKeyDownCommand = new(row => MoveKey(NewKeys.IndexOf(row), NewKeys.IndexOf(row) + 1)); - CreateIndexCommand = new(CreateIndexAsync, () => !_isCreating && _newError.Length == 0 && NewKeys.Any(static k => k.Field.Length > 0)); + CreateIndexCommand = new(CreateIndexAsync, () => !IsCreating && NewError.Length == 0 && NewKeys.Any(static k => k.Field.Length > 0)); } /// @@ -386,7 +376,7 @@ internal string DirectionLabel(BsonValue value) => : value.ToString() ?? ""; /// 字段 → 抽样到的那一项;抽样里没有为 。 - private FieldOption? OptionOf(string field) => _fieldOptions.FirstOrDefault(f => f.Path == field); + private FieldOption? OptionOf(string field) => FieldOptions.FirstOrDefault(f => f.Path == field); /// ESR 角色 → 小标签文字。 private string RoleText(string? role) => role switch @@ -430,7 +420,7 @@ internal BsonDocument NewKeyDocument() { field = field == "*" ? "$**" : field + ".$**"; } - key.Set(field, row.DirectionValue); + _ = key.Set(field, row.DirectionValue); } if (key.ElementCount == 0 && _newKind == "wildcard") { @@ -461,7 +451,7 @@ internal BsonDocument NewOptionsDocument(BsonDocument key) { throw new FormatException(Loc["Design_ErrTtlSingle"]); } - if (!long.TryParse(_newTtlSeconds, NumberStyles.Integer, CultureInfo.InvariantCulture, out long seconds) || seconds < 0) + if (!long.TryParse(NewTtlSeconds, NumberStyles.Integer, CultureInfo.InvariantCulture, out long seconds) || seconds < 0) { throw new FormatException(Loc["Design_ErrTtlSeconds"]); } @@ -523,13 +513,13 @@ private void RecomputeNewIndex() error = ex.Message; } } - if (error.Length == 0 && Indexes.FirstOrDefault(i => i.Name == _newName.Trim()) is { } clash) + if (error.Length == 0 && Indexes.FirstOrDefault(i => i.Name == NewName.Trim()) is { } clash) { error = Loc.Format("Design_ErrNameExists", clash.Name); } - if (_nameEdited && _newName.Trim() != auto && _newName.Trim().Length > 0) + if (_nameEdited && NewName.Trim() != auto && NewName.Trim().Length > 0) { - options["name"] = _newName.Trim(); + options["name"] = NewName.Trim(); } NewError = error; NewCommand = key.ElementCount == 0 @@ -543,9 +533,9 @@ private void RecomputeNewIndex() /// 草稿行:与表里的行同一个模型,大小列写预估,使用列写「待创建」。 private IndexRow Draft(BsonDocument key, BsonDocument options, long size) { - string name = _newName.Trim().Length > 0 ? _newName.Trim() : IndexAdvisor.DefaultName(key); + string name = NewName.Trim().Length > 0 ? NewName.Trim() : IndexAdvisor.DefaultName(key); var spec = new BsonDocument { { "key", key }, { "name", name } }; - spec.Merge(options, overwriteExistingElements: false); + _ = spec.Merge(options, overwriteExistingElements: false); IndexForm form = IndexAdvisor.ShapeOf(spec); return new IndexRow { @@ -612,7 +602,7 @@ private async Task CreateIndexAsync() NewError = ex.Message; return; } - string name = _newName.Trim().Length > 0 ? _newName.Trim() : IndexAdvisor.DefaultName(key); + string name = NewName.Trim().Length > 0 ? NewName.Trim() : IndexAdvisor.DefaultName(key); if (Workspace.Guard.ConfirmWrites || Workspace.Guard.IsProduction) { bool ok = await Workspace.ConfirmAsync(new() @@ -631,7 +621,7 @@ private async Task CreateIndexAsync() } } var spec = new BsonDocument { { "key", key }, { "name", name } }; - spec.Merge(options, overwriteExistingElements: false); + _ = spec.Merge(options, overwriteExistingElements: false); IsCreating = true; var placeholder = new IndexRow { @@ -648,7 +638,7 @@ private async Task CreateIndexAsync() Task progress = TrackCreateAsync(placeholder, poll.Token); try { - await Workspace.Connection.RunCommandAsync(Database, new BsonDocument + _ = await Workspace.Connection.RunCommandAsync(Database, new BsonDocument { { "createIndexes", CollectionName }, { "indexes", new BsonArray { spec } } @@ -676,7 +666,7 @@ private async Task CreateIndexAsync() { } IsCreating = false; - Indexes.Remove(placeholder); + _ = Indexes.Remove(placeholder); } await LoadIndexesAsync().ConfigureAwait(true); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Options.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Options.cs index 7103f4f..9a970ea 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Options.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Options.cs @@ -18,18 +18,7 @@ internal sealed partial class DesignTabViewModel /// 时序粒度从细到粗(只能往粗改)。 public static IReadOnlyList Granularities { get; } = ["seconds", "minutes", "hours"]; - private string _collationText = ""; - private string _cappedSize = ""; - private string _cappedMax = ""; - private string _tsTimeField = ""; - private string _tsMetaField = ""; - private string _tsGranularity = ""; - private string _tsExpire = ""; - private string _viewOn = ""; - private string _viewPipeline = ""; - private bool _prePost; private bool _prePostOriginal; - private string _optionsJson = ""; /// TTL 索引。 public ObservableCollection TtlRows { get; } = []; @@ -40,94 +29,94 @@ internal sealed partial class DesignTabViewModel /// 排序规则(只读);没有为空。 public string CollationText { - get => _collationText; + get; private set { - if (SetProperty(ref _collationText, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasCollation)); } } - } + } = ""; /// 设了排序规则。 - public bool HasCollation => _collationText.Length > 0; + public bool HasCollation => CollationText.Length > 0; /// 固定集合字节上限(可改,6.0+)。 public string CappedSize { - get => _cappedSize; - set => SetProperty(ref _cappedSize, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 固定集合文档数上限(可改,6.0+;空 = 不限)。 public string CappedMax { - get => _cappedMax; - set => SetProperty(ref _cappedMax, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 固定集合字节上限换算(= 1.0 GB)。 - public string CappedSizeHuman => long.TryParse(_cappedSize, out long b) ? "= " + BsonText.Bytes(b) : ""; + public string CappedSizeHuman => long.TryParse(CappedSize, out long b) ? "= " + BsonText.Bytes(b) : ""; /// 时序:时间字段(只读)。 public string TsTimeField { - get => _tsTimeField; - private set => SetProperty(ref _tsTimeField, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 时序:元数据字段(只读)。 public string TsMetaField { - get => _tsMetaField; - private set => SetProperty(ref _tsMetaField, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 时序:粒度(只能往粗改)。 public string TsGranularity { - get => _tsGranularity; - set => SetProperty(ref _tsGranularity, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 时序:过期秒数(空 = 不过期)。 public string TsExpire { - get => _tsExpire; + get; set { - if (SetProperty(ref _tsExpire, value ?? "")) + if (SetProperty(ref field, value ?? "")) { RaisePropertyChanged(nameof(TsExpireHuman)); } } - } + } = ""; /// 时序过期换算。 - public string TsExpireHuman => long.TryParse(_tsExpire, out long s) ? "= " + Duration(s) : Loc["Design_NeverExpire"]; + public string TsExpireHuman => long.TryParse(TsExpire, out long s) ? "= " + Duration(s) : Loc["Design_NeverExpire"]; /// 视图:源集合。 public string ViewOn { - get => _viewOn; - set => SetProperty(ref _viewOn, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 视图:管道(mongosh 写法)。 public string ViewPipeline { - get => _viewPipeline; - set => SetProperty(ref _viewPipeline, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 变更流前后镜像(changeStreamPreAndPostImages,6.0+)。 public bool PrePostImages { - get => _prePost; + get; set { - if (SetProperty(ref _prePost, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(PrePostDirty)); } @@ -135,14 +124,14 @@ public bool PrePostImages } /// 前后镜像开关改过了。 - public bool PrePostDirty => _prePost != _prePostOriginal; + public bool PrePostDirty => PrePostImages != _prePostOriginal; /// listCollections 的 options 原文(只读,页底)。 public string OptionsJson { - get => _optionsJson; - private set => SetProperty(ref _optionsJson, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 应用一条 TTL 的新秒数。 public AsyncCommand ApplyTtlCommand { get; private set; } = null!; @@ -171,7 +160,7 @@ private void InitializeOptions() ApplyViewCommand = new(ApplyViewAsync); ApplyPrePostCommand = new(async () => { - if (await CollModAsync(new BsonDocument("changeStreamPreAndPostImages", new BsonDocument("enabled", _prePost)), Loc["Design_OptPrePost"]) + if (await CollModAsync(new BsonDocument("changeStreamPreAndPostImages", new BsonDocument("enabled", PrePostImages)), Loc["Design_OptPrePost"]) .ConfigureAwait(true)) { await LoadOptionsAsync().ConfigureAwait(true); @@ -239,15 +228,15 @@ internal async Task ApplyTtlAsync(TtlRow row) private async Task ApplyCappedAsync() { var changes = new BsonDocument(); - if (long.TryParse(_cappedSize, NumberStyles.Integer, CultureInfo.InvariantCulture, out long size) && size > 0) + if (long.TryParse(CappedSize, NumberStyles.Integer, CultureInfo.InvariantCulture, out long size) && size > 0) { changes["cappedSize"] = size; } - if (_cappedMax.Trim().Length == 0) + if (CappedMax.Trim().Length == 0) { changes["cappedMax"] = 0; } - else if (long.TryParse(_cappedMax, NumberStyles.Integer, CultureInfo.InvariantCulture, out long max) && max >= 0) + else if (long.TryParse(CappedMax, NumberStyles.Integer, CultureInfo.InvariantCulture, out long max) && max >= 0) { changes["cappedMax"] = max; } @@ -255,7 +244,7 @@ private async Task ApplyCappedAsync() { return; } - await CollModAsync(changes, Loc["Design_OptCapped"]).ConfigureAwait(true); + _ = await CollModAsync(changes, Loc["Design_OptCapped"]).ConfigureAwait(true); await LoadOptionsAsync().ConfigureAwait(true); } @@ -265,23 +254,23 @@ private async Task ApplyTimeSeriesAsync() var changes = new BsonDocument(); BsonDocument ts = _live.Options.GetValue("timeseries", BsonNull.Value) as BsonDocument ?? []; string current = ts.GetValue("granularity", BsonNull.Value) is BsonString g ? g.Value : ""; - if (_tsGranularity.Length > 0 && _tsGranularity != current) + if (TsGranularity.Length > 0 && TsGranularity != current) { - if (Granularities.ToList().IndexOf(_tsGranularity) < Granularities.ToList().IndexOf(current)) + if (Granularities.ToList().IndexOf(TsGranularity) < Granularities.ToList().IndexOf(current)) { Workspace.Toast(new() { Title = Loc["Design_ErrGranularity"], Kind = ToastKind.Warning }); return; } - changes["timeseries"] = new BsonDocument("granularity", _tsGranularity); + changes["timeseries"] = new BsonDocument("granularity", TsGranularity); } - if (_tsExpire.Trim().Length == 0) + if (TsExpire.Trim().Length == 0) { if (_live.Options.Contains("expireAfterSeconds")) { changes["expireAfterSeconds"] = "off"; } } - else if (long.TryParse(_tsExpire, NumberStyles.Integer, CultureInfo.InvariantCulture, out long seconds) && seconds >= 0) + else if (long.TryParse(TsExpire, NumberStyles.Integer, CultureInfo.InvariantCulture, out long seconds) && seconds >= 0) { changes["expireAfterSeconds"] = seconds; } @@ -289,7 +278,7 @@ private async Task ApplyTimeSeriesAsync() { return; } - await CollModAsync(changes, Loc["Design_OptTimeSeries"]).ConfigureAwait(true); + _ = await CollModAsync(changes, Loc["Design_OptTimeSeries"]).ConfigureAwait(true); await LoadOptionsAsync().ConfigureAwait(true); } @@ -299,18 +288,18 @@ private async Task ApplyViewAsync() BsonArray pipeline; try { - pipeline = ShellJson.ParseArray(_viewPipeline); + pipeline = ShellJson.ParseArray(ViewPipeline); } catch (ShellJsonException ex) { Workspace.Toast(new() { Title = Loc.Format("Design_SyntaxError", ex.Message), Kind = ToastKind.Warning }); return; } - if (_viewOn.Trim().Length == 0) + if (ViewOn.Trim().Length == 0) { return; } - await CollModAsync(new BsonDocument { { "viewOn", _viewOn.Trim() }, { "pipeline", pipeline } }, Loc["Design_OptView"]).ConfigureAwait(true); + _ = await CollModAsync(new BsonDocument { { "viewOn", ViewOn.Trim() }, { "pipeline", pipeline } }, Loc["Design_OptView"]).ConfigureAwait(true); await LoadOptionsAsync().ConfigureAwait(true); } @@ -340,8 +329,8 @@ private async Task CollModAsync(BsonDocument changes, string what) try { var command = new BsonDocument("collMod", CollectionName); - command.Merge(changes); - await Workspace.Connection.RunCommandAsync(Database, command, Lifetime).ConfigureAwait(true); + _ = command.Merge(changes); + _ = await Workspace.Connection.RunCommandAsync(Database, command, Lifetime).ConfigureAwait(true); Workspace.Toast(new() { Title = Loc.Format("Design_CollModDone", what), Kind = ToastKind.Success }); await RefreshInfoAsync().ConfigureAwait(true); await Workspace.RefreshTreeAsync(Database).ConfigureAwait(true); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Schema.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Schema.cs index 2573a08..8f7f214 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Schema.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Schema.cs @@ -16,21 +16,7 @@ internal sealed partial class DesignTabViewModel public static IReadOnlyList SampleSizes { get; } = [100, 1000, 5000]; private int _sampleSize; - private string _schemaFilter = ""; private bool _filterSeeded; - private bool _isAnalyzing; - private string _schemaTiming = ""; - private string _sampledText = ""; - private AnalyzedSchema? _schema; - private IReadOnlyList _sample = []; - private BsonDocument? _generated; - private string _generatedText = ""; - private string _genLevel = "moderate"; - private string _genAction = "warn"; - private bool _isGenChecking; - private long _genFailCount = -1; - private string _genCheckTitle = ""; - private string _genCheckReasons = ""; /// 抽样文档数。 public int SampleSize @@ -51,9 +37,9 @@ public int SampleSize /// 沿用的筛选条件(mongosh 写法;空 = 全集合抽样)。 public string SchemaFilter { - get => _schemaFilter; - set => SetProperty(ref _schemaFilter, value ?? ""); - } + get; + set => SetProperty(ref field, value ?? ""); + } = ""; /// 字段行。 public ObservableCollection SchemaFields { get; } = []; @@ -61,10 +47,10 @@ public string SchemaFilter /// 正在抽样分析。 public bool IsAnalyzing { - get => _isAnalyzing; + get; private set { - if (SetProperty(ref _isAnalyzing, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(ShowSchemaEmpty)); UpdateStatus(); @@ -73,29 +59,29 @@ private set } /// 抽样结果为空(空集合或筛选不到)。 - public bool ShowSchemaEmpty => !_isAnalyzing && _schema is { Sampled: 0 }; + public bool ShowSchemaEmpty => !IsAnalyzing && Schema is { Sampled: 0 }; /// 工具行右侧:分析用时 0.4 s · 2026-09-26 21:08。 public string SchemaTiming { - get => _schemaTiming; - private set => SetProperty(ref _schemaTiming, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 抽到了多少(抽样 1,000 / 1,284,902)。 public string SampledText { - get => _sampledText; - private set => SetProperty(ref _sampledText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 最近一次的分析结果。 public AnalyzedSchema? Schema { - get => _schema; + get; private set { - if (SetProperty(ref _schema, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(ShowSchemaEmpty)); } @@ -103,15 +89,14 @@ private set } /// 抽样到的文档(验证规则页的试写文档以其中一份起头)。 - internal IReadOnlyList Sample => _sample; + internal IReadOnlyList Sample { get; private set; } = []; /// 由抽样生成的 validator。 public BsonDocument? Generated { - get => _generated; - private set + get; private set { - if (SetProperty(ref _generated, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasGenerated)); } @@ -119,77 +104,55 @@ private set } /// 有生成结果。 - public bool HasGenerated => _generated is not null; + public bool HasGenerated => Generated is not null; /// 生成规则的预览文本。 - public string GeneratedText - { - get => _generatedText; - private set => SetProperty(ref _generatedText, value); - } + public string GeneratedText { get; private set => SetProperty(ref field, value); } = ""; /// 生成规则用的 validationLevel(默认 moderate:只校验新写入与本来就合规的文档的更新)。 public string GenLevel { - get => _genLevel; - set + get; set { - if (SetProperty(ref _genLevel, value ?? "moderate")) + if (SetProperty(ref field, value ?? "moderate")) { RaisePropertyChanged(nameof(GenCheckReasons)); } } - } + } = "moderate"; /// 生成规则用的 validationAction(默认 warn:先观察,不拦写入)。 - public string GenAction - { - get => _genAction; - set => SetProperty(ref _genAction, value ?? "warn"); - } + public string GenAction { get; set => SetProperty(ref field, value ?? "warn"); } = "warn"; /// 正在做现有数据预检。 - public bool IsGenChecking - { - get => _isGenChecking; - private set => SetProperty(ref _isGenChecking, value); - } + public bool IsGenChecking { get; private set => SetProperty(ref field, value); } /// 预检不通过的份数;未检为 -1。 public long GenFailCount { - get => _genFailCount; - private set + get; private set { - if (SetProperty(ref _genFailCount, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(GenHasFailures), nameof(GenAllPass), nameof(GenFailLink)); } } - } + } = -1; /// 有不通过的文档(橙色预检框)。 - public bool GenHasFailures => _genFailCount > 0; + public bool GenHasFailures => GenFailCount > 0; /// 全部通过(绿色预检框)。 - public bool GenAllPass => _genFailCount == 0; + public bool GenAllPass => GenFailCount == 0; /// 预检标题。 - public string GenCheckTitle - { - get => _genCheckTitle; - private set => SetProperty(ref _genCheckTitle, value); - } + public string GenCheckTitle { get; private set => SetProperty(ref field, value); } = ""; /// 主要原因 + 建议。 - public string GenCheckReasons - { - get => _genCheckReasons; - private set => SetProperty(ref _genCheckReasons, value); - } + public string GenCheckReasons { get; private set => SetProperty(ref field, value); } = ""; /// 「在查询编辑器中查看这 37 份文档 →」。 - public string GenFailLink => Loc.Format("Design_GenViewFailures", BsonText.Grouped(Math.Max(0, _genFailCount))); + public string GenFailLink => Loc.Format("Design_GenViewFailures", BsonText.Grouped(Math.Max(0, GenFailCount))); /// 重新分析。 public AsyncCommand AnalyzeCommand { get; private set; } = null!; @@ -222,30 +185,30 @@ private void InitializeSchema() }); CopyGeneratedCommand = new(async () => { - if (_generated is not null) + if (Generated is not null) { - await Workspace.CopyAsync(JsonSchemaGenerator.Format(_generated)).ConfigureAwait(true); + await Workspace.CopyAsync(JsonSchemaGenerator.Format(Generated)).ConfigureAwait(true); Workspace.Toast(new() { Title = Loc["Common_Copied"], Kind = ToastKind.Success }); } }); ViewGeneratedFailuresCommand = new(() => { - if (_generated is not null) + if (Generated is not null) { - Workspace.OpenQuery(Database, FailingQuery(_generated), run: true); + Workspace.OpenQuery(Database, FailingQuery(Generated), run: true); } }); EditGeneratedCommand = new(() => { - if (_generated is not null) + if (Generated is not null) { - LoadRuleIntoEditor(_generated, _genLevel, _genAction); + LoadRuleIntoEditor(Generated, GenLevel, GenAction); Page = DesignPage.Validation; } }); - ApplyGeneratedCommand = new(() => _generated is null + ApplyGeneratedCommand = new(() => Generated is null ? Task.CompletedTask - : ApplyValidatorAsync(_generated, _genLevel, _genAction, _genFailCount)); + : ApplyValidatorAsync(Generated, GenLevel, GenAction, GenFailCount)); } /// 不符合某条 validator 的文档的查询(db.orders.find({ $nor: [ … ] }))。 @@ -258,7 +221,7 @@ internal string FailingQuery(BsonDocument validator) => /// internal async Task AnalyzeAsync() { - if (_isAnalyzing || _disposed) + if (IsAnalyzing || _disposed) { return; } @@ -269,12 +232,12 @@ internal async Task AnalyzeAsync() { _filterSeeded = true; IReadOnlyList history = await Workspace.Store.LoadFilterHistoryAsync(Workspace.ConnectionKey, Namespace).ConfigureAwait(true); - if (_schemaFilter.Length == 0 && history.Count > 0) + if (SchemaFilter.Length == 0 && history.Count > 0) { SchemaFilter = history[0]; } } - BsonDocument filter = ShellJson.ParseDocument(_schemaFilter); + BsonDocument filter = ShellJson.ParseDocument(SchemaFilter); var pipeline = new List(); if (filter.ElementCount > 0) { @@ -295,7 +258,7 @@ internal async Task AnalyzeAsync() watch.Stop(); long? total = await Workspace.Connection.EstimatedCountAsync(Database, CollectionName, Lifetime).ConfigureAwait(true); - _sample = docs; + Sample = docs; Schema = schema; SchemaFields.Clear(); foreach (SchemaFieldRow row in rows) @@ -363,11 +326,11 @@ private SchemaFieldRow BuildSchemaRow(AnalyzedField field) switch (viz) { case AnalyzedViz.Bars: - { - string token = BsonKinds.ColorToken(field.DominantKind); - bars = - [ - .. field.Values + { + string token = BsonKinds.ColorToken(field.DominantKind); + bars = + [ + .. field.Values .OrderByDescending(static v => v.Value) .Take(SchemaAnalyzer.MaxBars) .Select(v => @@ -376,35 +339,35 @@ .. field.Values string label = v.Key.IsString ? BsonText.OneLine(v.Key.AsString) : SchemaAnalyzer.Label(v.Key); return new ValueBar(label, Percent(ratio), ratio, token); }) - ]; - break; - } + ]; + break; + } case AnalyzedViz.Histogram: - { - (double[] bins, double min, double max, bool clipped) = SchemaAnalyzer.Histogram(field); - histogram = [new ChartSeries(field.Path, "VelaShellGreen", bins)]; - double width = (max - min) / Math.Max(1, bins.Length); - labels = [.. bins.Select((_, i) => $"{SchemaAnalyzer.Number(min + (width * i))} – {SchemaAnalyzer.Number(min + (width * (i + 1)))}")]; - axisMin = SchemaAnalyzer.Number(min); - axisMax = SchemaAnalyzer.Number(max) + (clipped ? "+" : ""); - break; - } + { + (double[] bins, double min, double max, bool clipped) = SchemaAnalyzer.Histogram(field); + histogram = [new ChartSeries(field.Path, "VelaShellGreen", bins)]; + double width = (max - min) / Math.Max(1, bins.Length); + labels = [.. bins.Select((_, i) => $"{SchemaAnalyzer.Number(min + (width * i))} – {SchemaAnalyzer.Number(min + (width * (i + 1)))}")]; + axisMin = SchemaAnalyzer.Number(min); + axisMax = SchemaAnalyzer.Number(max) + (clipped ? "+" : ""); + break; + } case AnalyzedViz.Timeline: - { - (double[] bins, DateTime start, DateTime end, int perBin) = SchemaAnalyzer.Timeline(field); - histogram = [new ChartSeries(field.Path, "VelaShellMagenta", bins)]; - labels = [.. bins.Select((_, i) => start.AddDays(i * perBin).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))]; - axisMin = start.ToString("MM-dd", CultureInfo.InvariantCulture); - axisMax = end.ToString("MM-dd", CultureInfo.InvariantCulture); - break; - } + { + (double[] bins, DateTime start, DateTime end, int perBin) = SchemaAnalyzer.Timeline(field); + histogram = [new ChartSeries(field.Path, "VelaShellMagenta", bins)]; + labels = [.. bins.Select((_, i) => start.AddDays(i * perBin).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))]; + axisMin = start.ToString("MM-dd", CultureInfo.InvariantCulture); + axisMax = end.ToString("MM-dd", CultureInfo.InvariantCulture); + break; + } case AnalyzedViz.Lengths: - { - IReadOnlyList buckets = SchemaAnalyzer.Lengths(field); - double peak = buckets.Count == 0 ? 1 : Math.Max(0.0001, buckets.Max(static b => b.Ratio)); - lengths = [.. buckets.Select(b => new LengthBar(b.Label, Math.Round(36 * b.Ratio / peak), b.Count))]; - break; - } + { + IReadOnlyList buckets = SchemaAnalyzer.Lengths(field); + double peak = buckets.Count == 0 ? 1 : Math.Max(0.0001, buckets.Max(static b => b.Ratio)); + lengths = [.. buckets.Select(b => new LengthBar(b.Label, Math.Round(36 * b.Ratio / peak), b.Count))]; + break; + } } return new SchemaFieldRow { @@ -438,7 +401,7 @@ private async Task PrecheckGeneratedAsync(BsonDocument validator) try { (long count, IReadOnlyList<(string Label, int Count)> reasons) = await CountFailuresAsync(validator).ConfigureAwait(true); - if (!ReferenceEquals(validator, _generated)) + if (!ReferenceEquals(validator, Generated)) { return; } @@ -480,7 +443,7 @@ private async Task PrecheckGeneratedAsync(BsonDocument validator) } catch (MongoExecutionTimeoutException) { - count = _sample.Count(doc => !SampleMatches(validator, doc)); + count = Sample.Count(doc => !SampleMatches(validator, doc)); } List failing = count == 0 ? [] diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Stats.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Stats.cs index f06d9f4..735ad9e 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Stats.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Stats.cs @@ -9,7 +9,6 @@ namespace VelaShell.Plugin.Mongo.Ui; internal sealed partial class DesignTabViewModel { private CollectionStats? _stats; - private string _statsRaw = ""; /// 指标卡。 public ObservableCollection StatCards { get; } = []; @@ -20,9 +19,9 @@ internal sealed partial class DesignTabViewModel /// 原始 JSON。 public string StatsRaw { - get => _statsRaw; - private set => SetProperty(ref _statsRaw, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 刷新统计。 public AsyncCommand RefreshStatsCommand { get; private set; } = null!; @@ -35,7 +34,7 @@ private void InitializeStats() RefreshStatsCommand = new(LoadStatsAsync); CopyStatsCommand = new(async () => { - await Workspace.CopyAsync(_statsRaw).ConfigureAwait(true); + await Workspace.CopyAsync(StatsRaw).ConfigureAwait(true); Workspace.Toast(new() { Title = Loc["Common_Copied"], Kind = ToastKind.Success }); }); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Validation.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Validation.cs index 710d773..2e2f5e9 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Validation.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Design/DesignTabViewModel.Validation.cs @@ -1,5 +1,4 @@ using System.Collections.ObjectModel; -using System.Globalization; using System.Text.RegularExpressions; using Avalonia.Threading; using MongoDB.Bson; @@ -44,30 +43,16 @@ internal sealed partial class DesignTabViewModel private bool _editorReady; private bool _validationLoading; - private string _ruleText = ""; private string _appliedText = ""; - private string _ruleLevel = "strict"; - private string _ruleAction = "error"; private string _appliedLevel = "strict"; private string _appliedAction = "error"; private BsonDocument? _ruleValidator; - private IReadOnlyList _ruleDiagnostics = []; - private bool _ruleSyntaxOk = true; - private string _ruleSyntaxText = ""; - private bool _isPrechecking; private bool _precheckRan; private bool _precheckAgain; private bool _precheckResample; private List _precheckIds = []; private long _passCount; private long _failCount = -1; - private IReadOnlyList _passPieces = []; - private string _tryDocText = ""; - private IReadOnlyList _tryDiagnostics = []; - private IReadOnlyList _tryMarks = []; - private bool? _tryPassed; - private string _tryTitle = ""; - private string _tryDetail = ""; private DispatcherTimer? _tryTimer; private DispatcherTimer? _ruleTimer; private int _tryVersion; @@ -75,10 +60,10 @@ internal sealed partial class DesignTabViewModel /// 编辑器里的规则文本(mongosh 写法)。 public string RuleText { - get => _ruleText; + get; set { - if (SetProperty(ref _ruleText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { ParseRule(); RecomputeRuleState(); @@ -86,69 +71,69 @@ public string RuleText Restart(ref _tryTimer, TimeSpan.FromMilliseconds(350), () => _ = EvaluateTryDocAsync()); } } - } + } = ""; /// validationLevel(off / moderate / strict)。 public string RuleLevel { - get => _ruleLevel; + get; set { - if (SetProperty(ref _ruleLevel, value ?? "strict")) + if (SetProperty(ref field, value ?? "strict")) { RecomputeRuleState(); } } - } + } = "strict"; /// validationAction(warn / error)。 public string RuleAction { - get => _ruleAction; + get; set { - if (SetProperty(ref _ruleAction, value ?? "error")) + if (SetProperty(ref field, value ?? "error")) { RecomputeRuleState(); Restart(ref _tryTimer, TimeSpan.FromMilliseconds(50), () => _ = EvaluateTryDocAsync()); } } - } + } = "error"; /// 编辑器诊断(语法错误)。 public IReadOnlyList RuleDiagnostics { - get => _ruleDiagnostics; - private set => SetProperty(ref _ruleDiagnostics, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 语法正确。 public bool RuleSyntaxOk { - get => _ruleSyntaxOk; + get; private set { - if (SetProperty(ref _ruleSyntaxOk, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(RuleSyntaxBad)); ApplyRuleCommand.RaiseCanExecuteChanged(); } } - } + } = true; /// 语法有误。 - public bool RuleSyntaxBad => !_ruleSyntaxOk; + public bool RuleSyntaxBad => !RuleSyntaxOk; /// 工具行右侧的语法状态文字。 public string RuleSyntaxText { - get => _ruleSyntaxText; - private set => SetProperty(ref _ruleSyntaxText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 底部命令预览。 public string RuleCommand => - $"db.runCommand({{ collMod: {QuotedCollection}, validator: {{…}}, validationLevel: {BsonText.Quote(_ruleLevel)}, validationAction: {BsonText.Quote(_ruleAction)} }})"; + $"db.runCommand({{ collMod: {QuotedCollection}, validator: {{…}}, validationLevel: {BsonText.Quote(RuleLevel)}, validationAction: {BsonText.Quote(RuleAction)} }})"; /// 子页条上验证规则旁的小字。 public string ValidationTabHint => _live.Validator switch @@ -164,8 +149,8 @@ public string RuleSyntaxText /// 规则测试:正在预检。 public bool IsPrechecking { - get => _isPrechecking; - private set => SetProperty(ref _isPrechecking, value); + get; + private set => SetProperty(ref field, value); } /// 预检抽了多少份(抽样 1,000)。 @@ -180,9 +165,9 @@ public bool IsPrechecking /// 通过 / 不通过比例条。 public IReadOnlyList PassPieces { - get => _passPieces; - private set => SetProperty(ref _passPieces, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 不通过原因(按 关键字 + 路径 分组)。 public ObservableCollection FailureReasons { get; } = []; @@ -193,37 +178,37 @@ public IReadOnlyList PassPieces /// 试写文档(mongosh 写法)。 public string TryDocText { - get => _tryDocText; + get; set { - if (SetProperty(ref _tryDocText, value ?? "")) + if (SetProperty(ref field, value ?? "")) { Restart(ref _tryTimer, TimeSpan.FromMilliseconds(350), () => _ = EvaluateTryDocAsync()); } } - } + } = ""; /// 试写文档的诊断(违规的值下面画波浪线)。 public IReadOnlyList TryDiagnostics { - get => _tryDiagnostics; - private set => SetProperty(ref _tryDiagnostics, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 试写文档的整行标记(违规的行红底)。 public IReadOnlyList TryMarks { - get => _tryMarks; - private set => SetProperty(ref _tryMarks, value); - } + get; + private set => SetProperty(ref field, value); + } = []; /// 试写结果:通过 / 不通过 / 未知(语法错误)。 public bool? TryPassed { - get => _tryPassed; + get; private set { - if (SetProperty(ref _tryPassed, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(TryOk), nameof(TryFailed), nameof(TryUnknown)); } @@ -231,42 +216,42 @@ private set } /// 通过(绿框)。 - public bool TryOk => _tryPassed == true; + public bool TryOk => TryPassed == true; /// 不通过(红框)。 - public bool TryFailed => _tryPassed == false; + public bool TryFailed => TryPassed == false; /// 没法判断(灰框)。 - public bool TryUnknown => _tryPassed is null && _tryTitle.Length > 0; + public bool TryUnknown => TryPassed is null && TryTitle.Length > 0; /// 结果框标题。 public string TryTitle { - get => _tryTitle; + get; private set { - if (SetProperty(ref _tryTitle, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(TryUnknown)); } } - } + } = ""; /// 结果框明细(每条违规一行)。 public string TryDetail { - get => _tryDetail; + get; private set { - if (SetProperty(ref _tryDetail, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasTryDetail)); } } - } + } = ""; /// 有明细。 - public bool HasTryDetail => _tryDetail.Length > 0; + public bool HasTryDetail => TryDetail.Length > 0; /// 历史版本(新的在前)。 public ObservableCollection RuleHistory { get; } = []; @@ -337,7 +322,7 @@ private void InitializeValidation() }); ApplyRuleCommand = new(() => _ruleValidator is null ? Task.CompletedTask - : ApplyValidatorAsync(_ruleValidator, _ruleLevel, _ruleAction, _failCount), () => _ruleSyntaxOk); + : ApplyValidatorAsync(_ruleValidator, RuleLevel, RuleAction, _failCount), () => RuleSyntaxOk); RefreshPrecheckCommand = new(() => RunPrecheckAsync(resample: true)); OpenFailureCommand = new(row => Workspace.OpenCollection(Database, CollectionName, row.Filter)); FailureReasons.CollectionChanged += (_, _) => RaisePropertyChanged(nameof(HasFailureReasons)); @@ -366,7 +351,7 @@ private async Task LoadValidationAsync(bool resetEditor) } await LoadHistoryAsync().ConfigureAwait(true); await RunPrecheckAsync(resample: !_precheckRan).ConfigureAwait(true); - if (_tryDocText.Length == 0) + if (TryDocText.Length == 0) { await SeedTryDocAsync().ConfigureAwait(true); } @@ -411,7 +396,7 @@ internal void LoadRuleIntoEditor(BsonDocument validator, string level, string ac private void ParseRule() { var diagnostics = new List(); - foreach (ShellDiagnostic d in ShellJson.Diagnose(_ruleText)) + foreach (ShellDiagnostic d in ShellJson.Diagnose(RuleText)) { diagnostics.Add(new(d.Offset, Math.Max(1, d.Length), Loc.Format(d.MessageKey, d.Argument), DiagnosticSeverity.Error, d.Fix)); } @@ -421,12 +406,12 @@ private void ParseRule() { try { - parsed = ShellJson.ParseDocument(_ruleText); + parsed = ShellJson.ParseDocument(RuleText); } catch (ShellJsonException ex) { error = ex.Message; - int offset = ex.Offset >= 0 ? Math.Min(ex.Offset, Math.Max(0, _ruleText.Length - 1)) : 0; + int offset = ex.Offset >= 0 ? Math.Min(ex.Offset, Math.Max(0, RuleText.Length - 1)) : 0; diagnostics.Add(new(offset, 1, ex.Message)); } } @@ -444,8 +429,8 @@ private void ParseRule() private void RecomputeRuleState() { IsModified = _editorReady - && (!string.Equals(_ruleText.Trim(), _appliedText.Trim(), StringComparison.Ordinal) - || _ruleLevel != _appliedLevel || _ruleAction != _appliedAction); + && (!string.Equals(RuleText.Trim(), _appliedText.Trim(), StringComparison.Ordinal) + || RuleLevel != _appliedLevel || RuleAction != _appliedAction); RaisePropertyChanged(nameof(RuleCommand)); UpdateStatus(); } @@ -464,7 +449,7 @@ internal async Task RunPrecheckAsync(bool resample) { return; } - if (_isPrechecking) + if (IsPrechecking) { // 正在判上一版规则:记一笔,判完马上用最新的规则再判一次(而不是丢掉这次改动)。 _precheckAgain = true; @@ -482,7 +467,7 @@ internal async Task RunPrecheckAsync(bool resample) IMongoCollection collection = Workspace.Connection.Collection(Database, CollectionName); if (resample || _precheckIds.Count == 0) { - var pipeline = new[] + BsonDocument[] pipeline = new[] { new BsonDocument("$sample", new BsonDocument("size", PrecheckSampleSize)), new BsonDocument("$project", new BsonDocument("_id", 1)) @@ -672,7 +657,7 @@ internal async Task EvaluateTryDocAsync() return; } int version = ++_tryVersion; - if (_tryDocText.Trim().Length == 0) + if (TryDocText.Trim().Length == 0) { TryPassed = null; TryTitle = ""; @@ -681,12 +666,12 @@ internal async Task EvaluateTryDocAsync() TryMarks = []; return; } - if (!ShellJson.TryParseDocument(_tryDocText, out BsonDocument doc, out string? error)) + if (!ShellJson.TryParseDocument(TryDocText, out BsonDocument doc, out string? error)) { TryPassed = null; TryTitle = Loc.Format("Design_SyntaxError", error); TryDetail = ""; - TryDiagnostics = [.. ShellJson.Diagnose(_tryDocText).Select(d => new EditorDiagnostic(d.Offset, Math.Max(1, d.Length), Loc.Format(d.MessageKey, d.Argument)))]; + TryDiagnostics = [.. ShellJson.Diagnose(TryDocText).Select(d => new EditorDiagnostic(d.Offset, Math.Max(1, d.Length), Loc.Format(d.MessageKey, d.Argument)))]; TryMarks = []; return; } @@ -706,14 +691,14 @@ internal async Task EvaluateTryDocAsync() bool failed = serverFailed ?? violations.Count > 0; TryPassed = !failed; TryTitle = failed - ? Loc.Format("Design_TryFail", _ruleAction, Loc[_ruleAction == "warn" ? "Design_TryFailWarn" : "Design_TryFailError"]) + ? Loc.Format("Design_TryFail", RuleAction, Loc[RuleAction == "warn" ? "Design_TryFailWarn" : "Design_TryFailError"]) : Loc["Design_TryPass"]; TryDetail = !failed ? "" : violations.Count > 0 ? string.Join("\n", violations.Take(6).Select(v => $"{ViolationPath(v)}:{v.Message}")) : Loc["Design_TryServerOnly"]; - (TryMarks, TryDiagnostics) = failed ? MarkViolations(_tryDocText, violations) : ([], []); + (TryMarks, TryDiagnostics) = failed ? MarkViolations(TryDocText, violations) : ([], []); } /// properties.total.minimum(设计稿 16 结果框里的写法)。 @@ -876,7 +861,7 @@ internal async Task ApplyValidatorAsync(BsonDocument validator, string level, st try { await SaveHistoryAsync().ConfigureAwait(true); - await Workspace.Connection.RunCommandAsync(Database, new BsonDocument + _ = await Workspace.Connection.RunCommandAsync(Database, new BsonDocument { { "collMod", CollectionName }, { "validator", validator }, @@ -912,7 +897,7 @@ private async Task SaveHistoryAsync() /// Schema 分析刚完成:试写文档还空着就用抽样起头。 private void OnSchemaForValidation() { - if (_validationLoaded && _tryDocText.Length == 0) + if (_validationLoaded && TryDocText.Length == 0) { _ = SeedTryDocAsync(); } @@ -1047,11 +1032,11 @@ private void OnSchemaForValidation() var failing = FailureReasons.Where(r => r.RawKeyword == word).ToList(); if (failing.Count > 0) { - string consequence = _ruleLevel == "off" + string consequence = RuleLevel == "off" ? Loc["Design_HoverOff"] - : _ruleAction == "warn" - ? Loc.Format("Design_HoverWarn", _ruleLevel) - : Loc[_ruleLevel == "moderate" ? "Design_HoverErrorModerate" : "Design_HoverErrorStrict"]; + : RuleAction == "warn" + ? Loc.Format("Design_HoverWarn", RuleLevel) + : Loc[RuleLevel == "moderate" ? "Design_HoverErrorModerate" : "Design_HoverErrorStrict"]; body += Loc.Format("Design_HoverFailures", BsonText.Grouped(failing.Sum(static r => r.Count)), string.Join(", ", failing.Select(static r => r.Path)), consequence); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/ConnectionDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/ConnectionDialogViewModel.cs index 665f139..736c1ba 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/ConnectionDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/ConnectionDialogViewModel.cs @@ -11,8 +11,6 @@ internal sealed class HostRowViewModel : ObservableObject { private string _host; private string _port; - private string? _badge; - private Tone _badgeTone; /// 构造。 public HostRowViewModel(string host, int port, bool isFirst, Action remove, Action changed) @@ -64,10 +62,10 @@ public string Port /// 测试后发现的角色。 public string? Badge { - get => _badge; + get; set { - if (SetProperty(ref _badge, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasBadge)); } @@ -75,15 +73,15 @@ public string? Badge } /// 有角色徽章。 - public bool HasBadge => !string.IsNullOrEmpty(_badge); + public bool HasBadge => !string.IsNullOrEmpty(Badge); /// 徽章语气色。 public Tone BadgeTone { - get => _badgeTone; + get; set { - if (SetProperty(ref _badgeTone, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsBadgeOk), nameof(IsBadgeInfo), nameof(IsBadgeWarn), nameof(IsBadgeErr)); } @@ -91,16 +89,16 @@ public Tone BadgeTone } /// 主节点 / 单机(绿)。 - public bool IsBadgeOk => _badgeTone == Tone.Success; + public bool IsBadgeOk => BadgeTone == Tone.Success; /// 从节点(蓝)。 - public bool IsBadgeInfo => _badgeTone == Tone.Info; + public bool IsBadgeInfo => BadgeTone == Tone.Info; /// 恢复中(橙)。 - public bool IsBadgeWarn => _badgeTone == Tone.Warning; + public bool IsBadgeWarn => BadgeTone == Tone.Warning; /// 不可达(红)。 - public bool IsBadgeErr => _badgeTone == Tone.Danger; + public bool IsBadgeErr => BadgeTone == Tone.Danger; /// 删掉这一行。 public RelayCommand RemoveCommand { get; } @@ -244,7 +242,6 @@ internal sealed class ConnectionDialogViewModel : DialogViewModel, IDisposable private readonly MongoWorkspaceViewModel _shell; private readonly MongoProfile _original; private readonly string? _passwordFrom; - private bool _hasStoredPassword; private string _name; private string _group; private string _environment; @@ -257,8 +254,6 @@ internal sealed class ConnectionDialogViewModel : DialogViewModel, IDisposable private string _mechanism; private string _authSource; private string _username; - private string _password = ""; - private bool _passwordVisible; private bool _sshEnabled; private SshChoice? _sshChoice; private bool _tlsEnabled; @@ -268,7 +263,6 @@ internal sealed class ConnectionDialogViewModel : DialogViewModel, IDisposable private bool _confirmWrites; private bool _disableDropDatabase; private bool _policyTouched; - private bool _showAdvanced; private bool _directConnection; private string _appName; private string _connectTimeout; @@ -276,13 +270,7 @@ internal sealed class ConnectionDialogViewModel : DialogViewModel, IDisposable private string _pageSize; private string _sampleSize; private string _ejson; - private bool _showSystemDatabases; - private string _previewText = ""; - private bool _isTesting; - private string _testSummary = ""; private bool? _testSucceeded; - private string _error = ""; - private bool _isSaving; private CancellationTokenSource? _probe; /// 构造。 @@ -361,7 +349,7 @@ public ConnectionDialogViewModel(MongoWorkspaceViewModel shell, MongoProfile pro AddHostCommand = new RelayCommand(AddHost); TogglePasswordCommand = new RelayCommand(() => PasswordVisible = !PasswordVisible); ToggleAdvancedCommand = new RelayCommand(() => ShowAdvanced = !ShowAdvanced); - CopyPreviewCommand = new AsyncCommand(() => _shell.CopyAsync(_previewText)); + CopyPreviewCommand = new AsyncCommand(() => _shell.CopyAsync(PreviewText)); BrowseCaCommand = new AsyncCommand(async () => TlsCaFile = await PickAsync(TlsCaFile, Loc["Mongo_TlsCaFile"], new FileKind("PEM", "*.pem", "*.crt", "*.cer")).ConfigureAwait(true)); BrowseCertCommand = new AsyncCommand(async () => TlsCertFile = await PickAsync(TlsCertFile, Loc["Mongo_TlsCertFile"], new FileKind("PFX / PEM", "*.pfx", "*.p12", "*.pem")).ConfigureAwait(true)); TestCommand = new AsyncCommand(TestAsync); @@ -604,30 +592,30 @@ public string Username /// 口令(编辑时留空 = 不改)。 public string Password { - get => _password; + get; set { - if (SetProperty(ref _password, value)) + if (SetProperty(ref field, value)) { ClearProbe(); } } - } + } = ""; /// 口令明文显示。 public bool PasswordVisible { - get => _passwordVisible; - set => SetProperty(ref _passwordVisible, value); + get; + set => SetProperty(ref field, value); } /// 密钥库里已经存着这条连接的口令(口令框的占位字写「已保存」)。 public bool HasStoredPassword { - get => _hasStoredPassword; + get; private set { - if (SetProperty(ref _hasStoredPassword, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(PasswordPlaceholder)); } @@ -635,7 +623,7 @@ private set } /// 口令框的占位字。 - public string PasswordPlaceholder => _hasStoredPassword ? Loc["Conn_PasswordStored"] : ""; + public string PasswordPlaceholder => HasStoredPassword ? Loc["Conn_PasswordStored"] : ""; /// 切换明文显示。 public RelayCommand TogglePasswordCommand { get; } @@ -754,8 +742,8 @@ public bool DisableDropDatabase /// 「高级」展开着。 public bool ShowAdvanced { - get => _showAdvanced; - set => SetProperty(ref _showAdvanced, value); + get; + set => SetProperty(ref field, value); } /// 展开 / 收起「高级」。 @@ -829,8 +817,8 @@ public string Ejson /// 对象树列出系统库。 public bool ShowSystemDatabases { - get => _showSystemDatabases; - set => SetProperty(ref _showSystemDatabases, value); + get; + set => SetProperty(ref field, value); } // ── 右侧栏:连接字符串 ───────────────────────────────────────────────── @@ -841,9 +829,9 @@ public bool ShowSystemDatabases /// 整串(复制用)。 public string PreviewText { - get => _previewText; - private set => SetProperty(ref _previewText, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 复制连接串。 public AsyncCommand CopyPreviewCommand { get; } @@ -865,25 +853,25 @@ public string PreviewText /// 正在测。 public bool IsProbing { - get => _isTesting; - private set => SetProperty(ref _isTesting, value); + get; + private set => SetProperty(ref field, value); } /// 页脚的结论(连接成功 · 38 ms)。 public string TestSummary { - get => _testSummary; + get; private set { - if (SetProperty(ref _testSummary, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasTestSummary)); } } - } + } = ""; /// 有结论。 - public bool HasTestSummary => _testSummary.Length > 0; + public bool HasTestSummary => TestSummary.Length > 0; /// 测试通过了。 public bool TestPassed => _testSucceeded == true; @@ -899,24 +887,24 @@ private set /// 页脚的错误(名字没填、主机没填、存不下)。 public string Error { - get => _error; + get; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } } - } + } = ""; /// 有错误。 - public bool HasError => _error.Length > 0; + public bool HasError => Error.Length > 0; /// 正在存(按钮灰掉,免得点两次)。 public bool IsSaving { - get => _isSaving; - private set => SetProperty(ref _isSaving, value); + get; + private set => SetProperty(ref field, value); } /// 仅保存。 @@ -998,7 +986,7 @@ private void RefreshPreview() private void ClearProbe() { _probe?.Cancel(); - if (Steps.Count == 0 && Members.Count == 0 && _testSummary.Length == 0) + if (Steps.Count == 0 && Members.Count == 0 && TestSummary.Length == 0) { return; } @@ -1059,7 +1047,7 @@ internal MongoProfile ToProfile() p.Set(MongoSettings.KeyPageSize, NonDefault(_pageSize, 50)); p.Set(MongoSettings.KeySampleSize, NonDefault(_sampleSize, 1000)); p.Set(MongoSettings.KeyEjson, _ejson == "relaxed" ? null : _ejson); - p.Set(MongoSettings.KeyShowSystemDatabases, _showSystemDatabases ? "true" : null); + p.Set(MongoSettings.KeyShowSystemDatabases, ShowSystemDatabases ? "true" : null); return p; static string Bool(bool value) => value ? "true" : "false"; @@ -1091,9 +1079,9 @@ internal MongoProfile ToProfile() /// 口令框为空时,测试与连接用密钥库里存着的那个。 private async Task EffectivePasswordAsync() { - if (_password.Length > 0 || _username.Trim().Length == 0) + if (Password.Length > 0 || _username.Trim().Length == 0) { - return _password; + return Password; } return await _shell.Profiles.GetPasswordAsync(_passwordFrom ?? _original.Id).ConfigureAwait(true); } @@ -1197,9 +1185,9 @@ private async Task SaveAsync(bool connect) } // 口令:填了就存;匿名就把存着的删掉;复制来的连接带上原来那条的;其余(编辑时留空)不动。 string? password; - if (_password.Length > 0) + if (Password.Length > 0) { - password = _password; + password = Password; } else if (profile.Username.Length == 0) { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogView.axaml.cs index d8bf15c..477ae52 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogView.axaml.cs @@ -22,7 +22,7 @@ internal NewCollectionDialogView(NewCollectionDialogViewModel viewModel) DataContext = viewModel; AttachedToVisualTree += (_, _) => Dispatcher.UIThread.Post(() => { - NameBox.Focus(); + _ = NameBox.Focus(); FitPreview(); }); NameBox.KeyDown += (_, e) => diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogViewModel.cs index ba28418..1ad56f5 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Dialogs/NewCollectionDialogViewModel.cs @@ -58,28 +58,12 @@ internal sealed record NewCollUnit(string Label, long Factor) /// internal sealed class NewCollectionDialogViewModel : DialogViewModel { - private readonly HashSet _existing = new(StringComparer.Ordinal); - private string _name = ""; + private readonly HashSet _existing = [with(StringComparer.Ordinal)]; private NewCollectionKind _kind = NewCollectionKind.Plain; - private string _timeField = "ts"; - private string _metaField = ""; - private string _granularity = "seconds"; - private string _expireValue = ""; private NewCollUnit _expireUnit; - private string _cappedSize = "100"; private NewCollUnit _cappedUnit; - private string _cappedMax = ""; private string? _viewSource; - private string _pipelineText = "[\n { $match: {} }\n]"; - private bool _useCollation; - private string _collationLocale = "zh"; - private int _collationStrength = 2; - private bool _copyIndexes; - private string? _copyIndexesFrom; private IReadOnlyList _sourceIndexes = []; - private bool _editValidationAfter; - private bool _isCreating; - private string _error = ""; /// 构造。 /// 外壳服务。 @@ -108,7 +92,7 @@ public NewCollectionDialogViewModel(IMongoWorkspace workspace, string database, CreateCommand = new(CreateAsync, () => CanCreate); foreach (CollectionInfo info in workspace.CollectionsOf(database)) { - _existing.Add(info.Name); + _ = _existing.Add(info.Name); } FillSources(workspace.CollectionsOf(database)); _ = LoadExistingAsync(); @@ -130,33 +114,33 @@ public NewCollectionDialogViewModel(IMongoWorkspace workspace, string database, public override double Height => 560; /// - public override bool CanCloseWithEscape => !_isCreating; + public override bool CanCloseWithEscape => !IsCreating; // ── 名称 ───────────────────────────────────────────────────────────── /// 集合名称。 public string Name { - get => _name; + get; set { - if (SetProperty(ref _name, value)) + if (SetProperty(ref field, value)) { Changed(nameof(NameState), nameof(NameMessage), nameof(NameOk), nameof(NameBad), nameof(NameIconKey)); } } - } + } = ""; /// 名称校验结论。 - public CollectionNameState NameState => ValidateName(_name, Database, _existing, out _); + public CollectionNameState NameState => ValidateName(Name, Database, _existing, out _); /// 名称旁那行小字(✓ 可用 · shop.device_metrics / 已存在 · shop.orders / 非法原因)。 public string NameMessage { get { - CollectionNameState state = ValidateName(_name, Database, _existing, out string? reason); - string ns = $"{Database}.{_name.Trim()}"; + CollectionNameState state = ValidateName(Name, Database, _existing, out string? reason); + string ns = $"{Database}.{Name.Trim()}"; return state switch { CollectionNameState.Available => Loc.Format("NewColl_NameAvailable", ns), @@ -292,63 +276,63 @@ public NewCollectionKind Kind /// timeField(必填)。 public string TimeField { - get => _timeField; + get; set { - if (SetProperty(ref _timeField, value)) + if (SetProperty(ref field, value)) { Changed(nameof(SampleDocument)); } } - } + } = "ts"; /// metaField(可选)。 public string MetaField { - get => _metaField; + get; set { - if (SetProperty(ref _metaField, value)) + if (SetProperty(ref field, value)) { Changed(nameof(SampleDocument)); } } - } + } = ""; /// 粒度:seconds / minutes / hours。 public string Granularity { - get => _granularity; + get; set { - if (SetProperty(ref _granularity, value)) + if (SetProperty(ref field, value)) { Changed(nameof(IsSeconds), nameof(IsMinutes), nameof(IsHours)); } } - } + } = "seconds"; /// 分段:seconds。 - public bool IsSeconds { get => _granularity == "seconds"; set { if (value) { Granularity = "seconds"; } } } + public bool IsSeconds { get => Granularity == "seconds"; set { if (value) { Granularity = "seconds"; } } } /// 分段:minutes。 - public bool IsMinutes { get => _granularity == "minutes"; set { if (value) { Granularity = "minutes"; } } } + public bool IsMinutes { get => Granularity == "minutes"; set { if (value) { Granularity = "minutes"; } } } /// 分段:hours。 - public bool IsHours { get => _granularity == "hours"; set { if (value) { Granularity = "hours"; } } } + public bool IsHours { get => Granularity == "hours"; set { if (value) { Granularity = "hours"; } } } /// 自动过期的数值(空 = 不过期)。 public string ExpireValue { - get => _expireValue; + get; set { - if (SetProperty(ref _expireValue, value)) + if (SetProperty(ref field, value)) { Changed(nameof(ExpireSecondsText)); } } - } + } = ""; /// 自动过期的单位。 public NewCollUnit ExpireUnit @@ -371,7 +355,7 @@ public NewCollUnit ExpireUnit /// 换算后的秒数;没填或填错为 。 public long? ExpireSeconds => - long.TryParse(_expireValue.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long n) + long.TryParse(ExpireValue.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long n) && n > 0 && n <= long.MaxValue / _expireUnit.Factor ? n * _expireUnit.Factor : null; @@ -381,8 +365,8 @@ public string SampleDocument { get { - string time = _timeField.Trim().Length > 0 ? BsonText.FieldName(_timeField.Trim()) : "ts"; - string meta = _metaField.Trim().Length > 0 ? $", {BsonText.FieldName(_metaField.Trim())}: {{ … }}" : ""; + string time = TimeField.Trim().Length > 0 ? BsonText.FieldName(TimeField.Trim()) : "ts"; + string meta = MetaField.Trim().Length > 0 ? $", {BsonText.FieldName(MetaField.Trim())}: {{ … }}" : ""; return $"{{ {time}: ISODate(…){meta}, value: 21.4 }}"; } } @@ -392,9 +376,9 @@ public string SampleDocument /// 容量上限的数值。 public string CappedSize { - get => _cappedSize; - set => SetAndRefresh(ref _cappedSize, value); - } + get; + set => SetAndRefresh(ref field, value); + } = "100"; /// 容量单位。 public NewCollUnit CappedUnit @@ -415,9 +399,9 @@ public NewCollUnit CappedUnit /// 文档数上限(可选)。 public string CappedMax { - get => _cappedMax; - set => SetAndRefresh(ref _cappedMax, value); - } + get; + set => SetAndRefresh(ref field, value); + } = ""; // ── 视图选项 ───────────────────────────────────────────────────────── @@ -431,9 +415,9 @@ public string? ViewSource /// 视图的管道(mongosh 写法)。 public string PipelineText { - get => _pipelineText; - set => SetAndRefresh(ref _pipelineText, value ?? ""); - } + get; + set => SetAndRefresh(ref field, value ?? ""); + } = "[\n { $match: {} }\n]"; /// 可选的源集合(集合与视图)。 public ObservableCollection ViewSources { get; } = []; @@ -443,41 +427,41 @@ public string PipelineText /// 指定排序规则。 public bool UseCollation { - get => _useCollation; - set => SetAndRefresh(ref _useCollation, value); + get; + set => SetAndRefresh(ref field, value); } /// 排序规则的 locale。 public string CollationLocale { - get => _collationLocale; - set => SetAndRefresh(ref _collationLocale, value); - } + get; + set => SetAndRefresh(ref field, value); + } = "zh"; /// 排序规则的 strength(1–5)。 public int CollationStrength { - get => _collationStrength; - set => SetAndRefresh(ref _collationStrength, Math.Clamp(value, 1, 5)); - } + get; + set => SetAndRefresh(ref field, Math.Clamp(value, 1, 5)); + } = 2; /// strength 下拉的选项。 public IReadOnlyList Strengths { get; } = [1, 2, 3, 4, 5]; /// 排序规则未勾选时那行灰字(locale: "zh", strength: 2)。 - public string CollationSummary => $"locale: {BsonText.Quote(_collationLocale)}, strength: {_collationStrength}"; + public string CollationSummary => $"locale: {BsonText.Quote(CollationLocale)}, strength: {CollationStrength}"; /// 复制索引。 public bool CopyIndexes { - get => _copyIndexes; + get; set { - if (!SetProperty(ref _copyIndexes, value)) + if (!SetProperty(ref field, value)) { return; } - if (value && _copyIndexesFrom is null && IndexSources.Count > 0) + if (value && CopyIndexesFrom is null && IndexSources.Count > 0) { CopyIndexesFrom = IndexSources[0]; } @@ -488,10 +472,10 @@ public bool CopyIndexes /// 从哪个集合复制索引。 public string? CopyIndexesFrom { - get => _copyIndexesFrom; + get; set { - if (SetProperty(ref _copyIndexesFrom, value)) + if (SetProperty(ref field, value)) { _sourceIndexes = []; Changed(); @@ -509,8 +493,8 @@ public string? CopyIndexesFrom /// 创建后立即打开「验证规则」页。 public bool EditValidationAfter { - get => _editValidationAfter; - set => SetProperty(ref _editValidationAfter, value); + get; + set => SetProperty(ref field, value); } /// 「创建后编辑验证规则」可用(时序集合与视图不支持验证规则)。 @@ -523,7 +507,7 @@ public string Preview { get { - string name = _name.Trim().Length > 0 ? _name.Trim() : Loc["NewColl_NamePlaceholder"]; + string name = Name.Trim().Length > 0 ? Name.Trim() : Loc["NewColl_NamePlaceholder"]; BsonDocument command = BuildCreateCommand(lenient: true); if (_kind == NewCollectionKind.View) { @@ -532,7 +516,7 @@ public string Preview } var options = new BsonDocument(command.Where(static e => e.Name != "create")); var lines = new List { ObjectScripts.CreateCollection(name, options) }; - if (_copyIndexes && CanCopyIndexes) + if (CopyIndexes && CanCopyIndexes) { lines.AddRange(_sourceIndexes.Where(ObjectScripts.IsReplayable).Select(i => ObjectScripts.CreateIndex(name, i))); } @@ -548,33 +532,33 @@ public string OptionsError switch (_kind) { case NewCollectionKind.TimeSeries: - { - string time = _timeField.Trim(); - string meta = _metaField.Trim(); - if (time.Length == 0) - { - return Loc["NewColl_ErrTimeField"]; - } - if (time.Contains('$', StringComparison.Ordinal) || time.Contains('.', StringComparison.Ordinal) || time == "_id") - { - return Loc["NewColl_ErrFieldName"]; - } - if (meta.Length > 0 && (meta == time || meta == "_id" || meta.Contains('$', StringComparison.Ordinal) || meta.Contains('.', StringComparison.Ordinal))) { - return Loc["NewColl_ErrMetaField"]; + string time = TimeField.Trim(); + string meta = MetaField.Trim(); + if (time.Length == 0) + { + return Loc["NewColl_ErrTimeField"]; + } + if (time.Contains('$', StringComparison.Ordinal) || time.Contains('.', StringComparison.Ordinal) || time == "_id") + { + return Loc["NewColl_ErrFieldName"]; + } + if (meta.Length > 0 && (meta == time || meta == "_id" || meta.Contains('$', StringComparison.Ordinal) || meta.Contains('.', StringComparison.Ordinal))) + { + return Loc["NewColl_ErrMetaField"]; + } + if (ExpireValue.Trim().Length > 0 && ExpireSeconds is null) + { + return Loc["NewColl_ErrExpire"]; + } + break; } - if (_expireValue.Trim().Length > 0 && ExpireSeconds is null) - { - return Loc["NewColl_ErrExpire"]; - } - break; - } case NewCollectionKind.Capped: if (CappedBytes is null) { return Loc["NewColl_ErrSize"]; } - if (_cappedMax.Trim().Length > 0 && CappedMaxDocs is null) + if (CappedMax.Trim().Length > 0 && CappedMaxDocs is null) { return Loc["NewColl_ErrMax"]; } @@ -590,11 +574,11 @@ public string OptionsError } break; } - if (_useCollation && _collationLocale.Trim().Length == 0) + if (UseCollation && CollationLocale.Trim().Length == 0) { return Loc["NewColl_ErrCollation"]; } - if (_copyIndexes && CanCopyIndexes && _copyIndexesFrom is null) + if (CopyIndexes && CanCopyIndexes && CopyIndexesFrom is null) { return Loc["NewColl_ErrCopySource"]; } @@ -603,15 +587,15 @@ public string OptionsError } /// 能不能点「创建集合」。 - public bool CanCreate => !_isCreating && NameState == CollectionNameState.Available && OptionsError.Length == 0; + public bool CanCreate => !IsCreating && NameState == CollectionNameState.Available && OptionsError.Length == 0; /// 正在创建。 public bool IsCreating { - get => _isCreating; + get; private set { - if (SetProperty(ref _isCreating, value)) + if (SetProperty(ref field, value)) { Changed(); } @@ -621,15 +605,15 @@ private set /// 底栏左侧的错误(选项错误或服务器拒绝的原因)。 public string Error { - get => _error.Length > 0 ? _error : OptionsError; + get => field.Length > 0 ? field : OptionsError; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } } - } + } = ""; /// 底栏有没有错误。 public bool HasError => Error.Length > 0; @@ -638,18 +622,18 @@ private set public AsyncCommand CreateCommand { get; } private long? CappedBytes => - double.TryParse(_cappedSize.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double n) && n > 0 + double.TryParse(CappedSize.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double n) && n > 0 ? (long)Math.Ceiling(n * _cappedUnit.Factor) : null; private long? CappedMaxDocs => - long.TryParse(_cappedMax.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long n) && n > 0 ? n : null; + long.TryParse(CappedMax.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long n) && n > 0 ? n : null; private bool TryParsePipeline(out BsonArray pipeline, out string? error) { try { - pipeline = ShellJson.ParseArray(_pipelineText); + pipeline = ShellJson.ParseArray(PipelineText); if (pipeline.Any(static s => !s.IsBsonDocument || s.AsBsonDocument.ElementCount != 1)) { error = Loc["NewColl_ErrStage"]; @@ -672,50 +656,50 @@ private bool TryParsePipeline(out BsonArray pipeline, out string? error) /// internal BsonDocument BuildCreateCommand(bool lenient = false) { - var command = new BsonDocument("create", _name.Trim()); + var command = new BsonDocument("create", Name.Trim()); switch (_kind) { case NewCollectionKind.TimeSeries: - { - var ts = new BsonDocument("timeField", _timeField.Trim()); - if (_metaField.Trim().Length > 0) - { - ts.Add("metaField", _metaField.Trim()); - } - ts.Add("granularity", _granularity); - command.Add("timeseries", ts); - if (ExpireSeconds is { } seconds) { - command.Add("expireAfterSeconds", Number(seconds)); + var ts = new BsonDocument("timeField", TimeField.Trim()); + if (MetaField.Trim().Length > 0) + { + _ = ts.Add("metaField", MetaField.Trim()); + } + _ = ts.Add("granularity", Granularity); + _ = command.Add("timeseries", ts); + if (ExpireSeconds is { } seconds) + { + _ = command.Add("expireAfterSeconds", Number(seconds)); + } + break; } - break; - } case NewCollectionKind.Capped: - command.Add("capped", true); - command.Add("size", Number(CappedBytes ?? 0)); + _ = command.Add("capped", true); + _ = command.Add("size", Number(CappedBytes ?? 0)); if (CappedMaxDocs is { } max) { - command.Add("max", Number(max)); + _ = command.Add("max", Number(max)); } break; case NewCollectionKind.Clustered: - command.Add("clusteredIndex", new BsonDocument + _ = command.Add("clusteredIndex", new BsonDocument { { "key", new BsonDocument("_id", 1) }, { "unique", true } }); break; case NewCollectionKind.View: - command.Add("viewOn", _viewSource ?? ""); - command.Add("pipeline", TryParsePipeline(out BsonArray pipeline, out _) || lenient ? pipeline : []); + _ = command.Add("viewOn", _viewSource ?? ""); + _ = command.Add("pipeline", TryParsePipeline(out BsonArray pipeline, out _) || lenient ? pipeline : []); break; } - if (_useCollation && _collationLocale.Trim().Length > 0) + if (UseCollation && CollationLocale.Trim().Length > 0) { - command.Add("collation", new BsonDocument + _ = command.Add("collation", new BsonDocument { - { "locale", _collationLocale.Trim() }, - { "strength", _collationStrength } + { "locale", CollationLocale.Trim() }, + { "strength", CollationStrength } }); } return command; @@ -731,15 +715,15 @@ internal async Task CreateAsync() { return; } - string name = _name.Trim(); + string name = Name.Trim(); NewCollectionKind kind = _kind; - bool editValidation = _editValidationAfter && CanEditValidation; + bool editValidation = EditValidationAfter && CanEditValidation; IsCreating = true; Error = ""; try { - await Workspace.Connection.RunCommandAsync(Database, BuildCreateCommand()).ConfigureAwait(true); - (int copied, List failed) = _copyIndexes && CanCopyIndexes && _copyIndexesFrom is { } source + _ = await Workspace.Connection.RunCommandAsync(Database, BuildCreateCommand()).ConfigureAwait(true); + (int copied, List failed) = CopyIndexes && CanCopyIndexes && CopyIndexesFrom is { } source ? await CopyIndexesAsync(source, name).ConfigureAwait(true) : (0, []); Workspace.Toast(new() @@ -784,10 +768,10 @@ internal async Task CreateAsync() foreach (BsonDocument index in indexes.Where(ObjectScripts.IsReplayable)) { var spec = new BsonDocument("key", index["key"]); - spec.AddRange(ObjectScripts.IndexOptions(index)); + _ = spec.AddRange(ObjectScripts.IndexOptions(index)); try { - await Workspace.Connection.RunCommandAsync(Database, new BsonDocument + _ = await Workspace.Connection.RunCommandAsync(Database, new BsonDocument { { "createIndexes", target }, { "indexes", new BsonArray { spec } } @@ -813,7 +797,7 @@ private async Task LoadExistingAsync() _existing.Clear(); foreach (CollectionInfo info in all) { - _existing.Add(info.Name); + _ = _existing.Add(info.Name); } FillSources(all); Changed(nameof(NameState), nameof(NameMessage), nameof(NameOk), nameof(NameBad), nameof(NameIconKey)); @@ -856,7 +840,7 @@ private async Task LoadSourceIndexesAsync(string? source) try { IReadOnlyList indexes = await Workspace.Connection.ListIndexesAsync(Database, source).ConfigureAwait(true); - if (_copyIndexesFrom == source) + if (CopyIndexesFrom == source) { _sourceIndexes = indexes; Changed(); @@ -871,7 +855,7 @@ private void SetAndRefresh(ref T field, T value, [System.Runtime.CompilerServ { if (SetProperty(ref field, value, name)) { - Changed(name == nameof(UseCollation) || name == nameof(CollationLocale) || name == nameof(CollationStrength) + Changed(name is (nameof(UseCollation)) or (nameof(CollationLocale)) or (nameof(CollationStrength)) ? nameof(CollationSummary) : null); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/DimExtension.cs b/plugins/VelaShell.Plugin.Mongo/Ui/DimExtension.cs index 61e8fcf..a662e77 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/DimExtension.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/DimExtension.cs @@ -1,5 +1,4 @@ using Avalonia.Markup.Xaml; -using Avalonia.Media; namespace VelaShell.Plugin.Mongo.Ui; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CodeEditor.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CodeEditor.cs index d133183..5897f7d 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CodeEditor.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CodeEditor.cs @@ -26,8 +26,6 @@ public sealed class CodeEditor : UserControl { private static readonly Lock RegistrationGate = new(); private static bool _registered; - - private readonly TextEditor _editor; private readonly DecorationRenderer _decorations; private readonly MarkerMargin _markers; private readonly Popup _popup; @@ -99,7 +97,7 @@ public sealed class CodeEditor : UserControl /// 构造。 public CodeEditor() { - _editor = new TextEditor + Editor = new TextEditor { Background = Brushes.Transparent, BorderThickness = new(0), @@ -108,21 +106,21 @@ public CodeEditor() HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollBarVisibility = ScrollBarVisibility.Auto }; - _editor.Options.IndentationSize = 2; - _editor.Options.ConvertTabsToSpaces = true; - _editor.Options.HighlightCurrentLine = true; - _editor.Options.EnableHyperlinks = false; - _editor.Options.EnableEmailHyperlinks = false; + Editor.Options.IndentationSize = 2; + Editor.Options.ConvertTabsToSpaces = true; + Editor.Options.HighlightCurrentLine = true; + Editor.Options.EnableHyperlinks = false; + Editor.Options.EnableEmailHyperlinks = false; // 文档末尾之下不留空白可滚:小块编辑器(阶段体、metadata、筛选框)里那一截空白只会让内容跑出视野。 - _editor.Options.AllowScrollBelowDocument = false; - _editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Transparent; + Editor.Options.AllowScrollBelowDocument = false; + Editor.TextArea.TextView.LinkTextForegroundBrush = Brushes.Transparent; _decorations = new DecorationRenderer(Brush, () => FontFamily, () => FontSize); - _editor.TextArea.TextView.BackgroundRenderers.Add(_decorations); + Editor.TextArea.TextView.BackgroundRenderers.Add(_decorations); _markers = new MarkerMargin(Brush); - _editor.TextArea.LeftMargins.Insert(0, _markers); + Editor.TextArea.LeftMargins.Insert(0, _markers); - _editor.TextChanged += (_, _) => + Editor.TextChanged += (_, _) => { if (FitContent) { @@ -133,28 +131,28 @@ public CodeEditor() return; } _syncing = true; - SetCurrentValue(TextProperty, _editor.Text); + SetCurrentValue(TextProperty, Editor.Text); _syncing = false; }; - _editor.TextArea.Caret.PositionChanged += (_, _) => + Editor.TextArea.Caret.PositionChanged += (_, _) => { - AvaloniaEdit.Document.TextLocation location = _editor.TextArea.Caret.Location; + AvaloniaEdit.Document.TextLocation location = Editor.TextArea.Caret.Location; _syncing = true; - SetCurrentValue(CaretOffsetProperty, _editor.CaretOffset); + SetCurrentValue(CaretOffsetProperty, Editor.CaretOffset); _syncing = false; SetCurrentValue(CaretLineProperty, location.Line); SetCurrentValue(CaretColumnProperty, location.Column); CaretMoved?.Invoke(this, EventArgs.Empty); - if (_session is not null && (_editor.CaretOffset < _session.ReplaceOffset)) + if (_session is not null && (Editor.CaretOffset < _session.ReplaceOffset)) { ClosePopup(); } }; - _editor.TextArea.TextEntered += OnTextEntered; - _editor.TextArea.AddHandler(KeyDownEvent, OnEditorKeyDown, RoutingStrategies.Tunnel); - _editor.TextArea.LostFocus += (_, _) => Dispatcher.UIThread.Post(() => + Editor.TextArea.TextEntered += OnTextEntered; + Editor.TextArea.AddHandler(KeyDownEvent, OnEditorKeyDown, RoutingStrategies.Tunnel); + Editor.TextArea.LostFocus += (_, _) => Dispatcher.UIThread.Post(() => { - if (_popupView?.IsKeyboardFocusWithin != true && !_editor.TextArea.IsKeyboardFocusWithin) + if (_popupView?.IsKeyboardFocusWithin != true && !Editor.TextArea.IsKeyboardFocusWithin) { ClosePopup(); } @@ -169,13 +167,13 @@ public CodeEditor() { _session.Selected = item; Accept(); - _editor.TextArea.Focus(); + _ = Editor.TextArea.Focus(); } }; _popup = new Popup { Child = _popupView, - PlacementTarget = _editor.TextArea.TextView, + PlacementTarget = Editor.TextArea.TextView, Placement = PlacementMode.AnchorAndGravity, PlacementAnchor = Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor.BottomLeft, PlacementGravity = Avalonia.Controls.Primitives.PopupPositioning.PopupGravity.BottomRight, @@ -183,15 +181,15 @@ public CodeEditor() }; var host = new Panel(); - host.Children.Add(_editor); + host.Children.Add(Editor); host.Children.Add(_popup); Content = host; Focusable = false; // 默认字体与前景按**样式优先级**绑:调用方在 AXAML 里写的 FontSize / FontFamily(本地值)要能盖过它。 - Bind(FontFamilyProperty, this.GetResourceObservable("VelaUiMonoFont"), Avalonia.Data.BindingPriority.Style); - Bind(FontSizeProperty, this.GetResourceObservable("VelaFontSize12"), Avalonia.Data.BindingPriority.Style); - Bind(ForegroundProperty, this.GetResourceObservable("VelaTextPrimary"), Avalonia.Data.BindingPriority.Style); + _ = Bind(FontFamilyProperty, this.GetResourceObservable("VelaUiMonoFont"), Avalonia.Data.BindingPriority.Style); + _ = Bind(FontSizeProperty, this.GetResourceObservable("VelaFontSize12"), Avalonia.Data.BindingPriority.Style); + _ = Bind(ForegroundProperty, this.GetResourceObservable("VelaTextPrimary"), Avalonia.Data.BindingPriority.Style); // 高亮配色是一次性取值(xshd 要 Color 不要 Brush),换肤时必须有人叫醒它 —— // 应用级资源变更覆盖全部三种换肤情形(与 DockerPanel 的 CodeEditor 同一个理由)。 @@ -203,7 +201,7 @@ public CodeEditor() public event EventHandler? CaretMoved; /// 里面那个编辑器(查询编辑器要直接用它取选区、按语句定位)。 - public TextEditor Editor => _editor; + public TextEditor Editor { get; } /// public string? Text @@ -290,13 +288,13 @@ protected override Size MeasureOverride(Size availableSize) { return measured; } - double lineHeight = _editor.TextArea.TextView.DefaultLineHeight; + double lineHeight = Editor.TextArea.TextView.DefaultLineHeight; if (double.IsNaN(lineHeight) || lineHeight <= 0) { lineHeight = FontSize * 1.4; } - int lines = Math.Clamp(_editor.Document.LineCount, 1, Math.Max(1, MaxFitLines)); - double height = (lines * lineHeight) + _editor.Padding.Top + _editor.Padding.Bottom + 2; + int lines = Math.Clamp(Editor.Document.LineCount, 1, Math.Max(1, MaxFitLines)); + double height = (lines * lineHeight) + Editor.Padding.Top + Editor.Padding.Bottom + 2; return new Size(measured.Width, Math.Min(height, availableSize.Height)); } @@ -322,20 +320,20 @@ public int CaretColumn } /// 选中的文本(没选中为空串)。 - public string SelectedText => _editor.SelectedText; + public string SelectedText => Editor.SelectedText; /// 在光标处插入文本(| 标出插入后光标落点)。 - public void InsertAtCaret(string text) => Replace(_editor.CaretOffset, 0, text); + public void InsertAtCaret(string text) => Replace(Editor.CaretOffset, 0, text); /// 替换一段文本(| 标出替换后光标落点)。 public void Replace(int offset, int length, string text) { int caret = text.IndexOf('|', StringComparison.Ordinal); string clean = caret >= 0 ? text.Remove(caret, 1) : text; - offset = Math.Clamp(offset, 0, _editor.Document.TextLength); - length = Math.Clamp(length, 0, _editor.Document.TextLength - offset); - _editor.Document.Replace(offset, length, clean); - _editor.CaretOffset = offset + (caret >= 0 ? caret : clean.Length); + offset = Math.Clamp(offset, 0, Editor.Document.TextLength); + length = Math.Clamp(length, 0, Editor.Document.TextLength - offset); + Editor.Document.Replace(offset, length, clean); + Editor.CaretOffset = offset + (caret >= 0 ? caret : clean.Length); } /// @@ -347,24 +345,24 @@ public void Replace(int offset, int length, string text) /// 替换后光标落在新文本里的位置;为 null 则落在末尾。 public void ReplaceRaw(int offset, int length, string text, int? caretInText = null) { - offset = Math.Clamp(offset, 0, _editor.Document.TextLength); - length = Math.Clamp(length, 0, _editor.Document.TextLength - offset); - _editor.Document.Replace(offset, length, text); - _editor.CaretOffset = offset + Math.Clamp(caretInText ?? text.Length, 0, text.Length); + offset = Math.Clamp(offset, 0, Editor.Document.TextLength); + length = Math.Clamp(length, 0, Editor.Document.TextLength - offset); + Editor.Document.Replace(offset, length, text); + Editor.CaretOffset = offset + Math.Clamp(caretInText ?? text.Length, 0, text.Length); } /// 选中一段。 public void Select(int offset, int length) { - offset = Math.Clamp(offset, 0, _editor.Document.TextLength); - _editor.Select(offset, Math.Clamp(length, 0, _editor.Document.TextLength - offset)); + offset = Math.Clamp(offset, 0, Editor.Document.TextLength); + Editor.Select(offset, Math.Clamp(length, 0, Editor.Document.TextLength - offset)); } /// 滚到某一行。 - public void ScrollToLine(int line) => _editor.ScrollToLine(Math.Max(1, line)); + public void ScrollToLine(int line) => Editor.ScrollToLine(Math.Max(1, line)); /// 把焦点给编辑区。 - public void FocusEditor() => _editor.TextArea.Focus(); + public void FocusEditor() => Editor.TextArea.Focus(); /// /// 应用光标所在(或光标所在行第一条)诊断的一键修复;没有可修的返回 false。 @@ -375,12 +373,12 @@ public bool TryApplyFix() { return false; } - int caret = _editor.CaretOffset; - int line = _editor.Document.GetLineByOffset(caret).LineNumber; + int caret = Editor.CaretOffset; + int line = Editor.Document.GetLineByOffset(caret).LineNumber; EditorDiagnostic? fix = diagnostics.FirstOrDefault(d => d.FixText is not null && caret >= d.Offset && caret <= d.Offset + d.Length) ?? diagnostics.FirstOrDefault(d => d.FixText is not null - && d.Offset <= _editor.Document.TextLength - && _editor.Document.GetLineByOffset(d.Offset).LineNumber == line); + && d.Offset <= Editor.Document.TextLength + && Editor.Document.GetLineByOffset(d.Offset).LineNumber == line); if (fix is null) { return false; @@ -407,15 +405,15 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang _syncing = true; string incoming = Text ?? ""; // 只在真不一样时才写:同一份文本写回去会把光标顶到开头,用户打一个字就跳一次。 - if (_editor.Text != incoming) + if (Editor.Text != incoming) { - _editor.Text = incoming; + Editor.Text = incoming; } _syncing = false; } else if (change.Property == CaretOffsetProperty && !_syncing) { - _editor.CaretOffset = Math.Clamp(CaretOffset, 0, _editor.Document.TextLength); + Editor.CaretOffset = Math.Clamp(CaretOffset, 0, Editor.Document.TextLength); } else if (change.Property == LanguageProperty || change.Property == ForegroundProperty || change.Property == FontFamilyProperty || change.Property == FontSizeProperty) @@ -424,33 +422,33 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang } else if (change.Property == IsReadOnlyProperty) { - _editor.IsReadOnly = IsReadOnly; + Editor.IsReadOnly = IsReadOnly; } else if (change.Property == WordWrapProperty) { - _editor.WordWrap = WordWrap; + Editor.WordWrap = WordWrap; } else if (change.Property == ShowLineNumbersProperty) { - _editor.ShowLineNumbers = ShowLineNumbers; + Editor.ShowLineNumbers = ShowLineNumbers; } else if (change.Property == FitContentProperty || change.Property == MaxFitLinesProperty) { - _editor.VerticalScrollBarVisibility = FitContent && _editor.Document.LineCount <= MaxFitLines + Editor.VerticalScrollBarVisibility = FitContent && Editor.Document.LineCount <= MaxFitLines ? ScrollBarVisibility.Disabled : ScrollBarVisibility.Auto; InvalidateMeasure(); } else if (change.Property == HighlightCurrentLineProperty) { - _editor.Options.HighlightCurrentLine = HighlightCurrentLine; + Editor.Options.HighlightCurrentLine = HighlightCurrentLine; } else if (change.Property == DiagnosticsProperty || change.Property == LineMarksProperty) { _decorations.Diagnostics = Diagnostics ?? []; _decorations.LineMarks = LineMarks ?? []; - _markers.Update(_editor.Document, _decorations.Diagnostics, _decorations.LineMarks); - _editor.TextArea.TextView.InvalidateLayer(AvaloniaEdit.Rendering.KnownLayer.Background); + _markers.Update(Editor.Document, _decorations.Diagnostics, _decorations.LineMarks); + Editor.TextArea.TextView.InvalidateLayer(AvaloniaEdit.Rendering.KnownLayer.Background); } } @@ -567,7 +565,7 @@ private void OnEditorKeyDown(object? sender, KeyEventArgs e) case Key.Back: Dispatcher.UIThread.Post(() => { - if (_session is not null && (_editor.CaretOffset <= _session.ReplaceOffset || !_session.Filter(CurrentPrefix()))) + if (_session is not null && (Editor.CaretOffset <= _session.ReplaceOffset || !_session.Filter(CurrentPrefix()))) { ClosePopup(); } @@ -589,7 +587,7 @@ private async Task RequestCompletionAsync(bool explicitRequest) CompletionSet? set; try { - set = await provider(new CompletionRequest(_editor.Text, _editor.CaretOffset, explicitRequest)).ConfigureAwait(true); + set = await provider(new CompletionRequest(Editor.Text, Editor.CaretOffset, explicitRequest)).ConfigureAwait(true); } catch (Exception) { @@ -599,9 +597,9 @@ private async Task RequestCompletionAsync(bool explicitRequest) { return; } - int prefixLength = Math.Max(0, _editor.CaretOffset - set.ReplaceOffset); - string prefix = _editor.Document.GetText(Math.Clamp(set.ReplaceOffset, 0, _editor.Document.TextLength), - Math.Min(prefixLength, _editor.Document.TextLength - set.ReplaceOffset)); + int prefixLength = Math.Max(0, Editor.CaretOffset - set.ReplaceOffset); + string prefix = Editor.Document.GetText(Math.Clamp(set.ReplaceOffset, 0, Editor.Document.TextLength), + Math.Min(prefixLength, Editor.Document.TextLength - set.ReplaceOffset)); var session = new CompletionSession(set) { InitialPrefix = prefix }; if (prefix.Length > 0 && !session.Filter(prefix)) { @@ -615,8 +613,8 @@ private async Task RequestCompletionAsync(bool explicitRequest) private void PlacePopup() { - Rect caret = _editor.TextArea.Caret.CalculateCaretRectangle(); - Vector scroll = _editor.TextArea.TextView.ScrollOffset; + Rect caret = Editor.TextArea.Caret.CalculateCaretRectangle(); + Vector scroll = Editor.TextArea.TextView.ScrollOffset; _popup.PlacementRect = new Rect(caret.X - scroll.X, caret.Y - scroll.Y, Math.Max(1, caret.Width), caret.Height); } @@ -628,7 +626,7 @@ private void Accept() return; } int start = _session.ReplaceOffset; - int length = Math.Max(0, _editor.CaretOffset - start); + int length = Math.Max(0, Editor.CaretOffset - start); ClosePopup(); Replace(start, length, item.InsertText ?? item.Label); } @@ -644,22 +642,22 @@ private string CurrentPrefix() { if (_session is null) { - int start = _editor.CaretOffset; - while (start > 0 && IsWordChar(_editor.Document.GetCharAt(start - 1))) + int start = Editor.CaretOffset; + while (start > 0 && IsWordChar(Editor.Document.GetCharAt(start - 1))) { start--; } - return _editor.Document.GetText(start, _editor.CaretOffset - start); + return Editor.Document.GetText(start, Editor.CaretOffset - start); } - int from = Math.Clamp(_session.ReplaceOffset, 0, _editor.Document.TextLength); - return _editor.Document.GetText(from, Math.Max(0, _editor.CaretOffset - from)); + int from = Math.Clamp(_session.ReplaceOffset, 0, Editor.Document.TextLength); + return Editor.Document.GetText(from, Math.Max(0, Editor.CaretOffset - from)); } private char PreviousNonSpace() { - for (int i = _editor.CaretOffset - 1; i >= 0; i--) + for (int i = Editor.CaretOffset - 1; i >= 0; i--) { - char c = _editor.Document.GetCharAt(i); + char c = Editor.Document.GetCharAt(i); if (!char.IsWhiteSpace(c)) { return c; @@ -676,20 +674,20 @@ private char PreviousNonSpace() private void ApplyLanguage() { - _editor.FontFamily = FontFamily; - _editor.FontSize = FontSize; + Editor.FontFamily = FontFamily; + Editor.FontSize = FontSize; if (Foreground is { } foreground) { - _editor.Foreground = foreground; + Editor.Foreground = foreground; } - _editor.TextArea.Caret.CaretBrush = Brush("VelaAccent"); - _editor.TextArea.SelectionBrush = ThemeBrushes.GetDim("VelaAccent", 0.25); - _editor.TextArea.SelectionBorder = null; - _editor.TextArea.SelectionForeground = null; - _editor.LineNumbersForeground = Brush("VelaTextMuted"); - _editor.TextArea.TextView.CurrentLineBackground = ThemeBrushes.GetDim("VelaTextPrimary", 0.04); - _editor.TextArea.TextView.CurrentLineBorder = null; - _editor.SyntaxHighlighting = Definition(Language) is { } definition ? Recolor(definition) : null; + Editor.TextArea.Caret.CaretBrush = Brush("VelaAccent"); + Editor.TextArea.SelectionBrush = ThemeBrushes.GetDim("VelaAccent", 0.25); + Editor.TextArea.SelectionBorder = null; + Editor.TextArea.SelectionForeground = null; + Editor.LineNumbersForeground = Brush("VelaTextMuted"); + Editor.TextArea.TextView.CurrentLineBackground = ThemeBrushes.GetDim("VelaTextPrimary", 0.04); + Editor.TextArea.TextView.CurrentLineBorder = null; + Editor.SyntaxHighlighting = Definition(Language) is { } definition ? Recolor(definition) : null; } private IHighlightingDefinition Recolor(IHighlightingDefinition definition) diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CompletionPopupView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CompletionPopupView.axaml.cs index b9ff74c..e482aa2 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CompletionPopupView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/CompletionPopupView.axaml.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using Avalonia.Controls; -using Avalonia.Input; namespace VelaShell.Plugin.Mongo.Ui; @@ -27,7 +26,7 @@ public CompletionPopupView() { try { - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); + _ = Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } catch (Exception) { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/EditorRenderers.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/EditorRenderers.cs index 5c2e9ac..230b2e1 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Editor/EditorRenderers.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Editor/EditorRenderers.cs @@ -172,15 +172,9 @@ public void Update(TextDocument? document, IReadOnlyList diagn /// protected override void OnTextViewChanged(TextView? oldTextView, TextView? newTextView) { - if (oldTextView is not null) - { - oldTextView.VisualLinesChanged -= OnVisualLinesChanged; - } + oldTextView?.VisualLinesChanged -= OnVisualLinesChanged; base.OnTextViewChanged(oldTextView, newTextView); - if (newTextView is not null) - { - newTextView.VisualLinesChanged += OnVisualLinesChanged; - } + newTextView?.VisualLinesChanged += OnVisualLinesChanged; } private void OnVisualLinesChanged(object? sender, EventArgs e) => InvalidateVisual(); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/Glyph.cs b/plugins/VelaShell.Plugin.Mongo/Ui/Glyph.cs index 3ae9b20..741e161 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/Glyph.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/Glyph.cs @@ -41,7 +41,7 @@ static Glyph() { AffectsRender(DataProperty, BrushProperty, SizeProperty); AffectsMeasure(SizeProperty); - KeyProperty.Changed.AddClassHandler((glyph, _) => glyph.ResolveKey()); + _ = KeyProperty.Changed.AddClassHandler((glyph, _) => glyph.ResolveKey()); // 图标是定尺寸的,默认的 Stretch 对它没有意义:排版会把它拉成整行高, // 而 Render 从原点开画 —— 于是图标贴在行顶,旁边居中的文字与它错开一截。 // 这是"图标和文字不在一个水平中轴上"的根因,逐处补 VerticalAlignment 补不干净。 @@ -94,7 +94,7 @@ private void ResolveKey() { return; } - if (this.TryFindResource(key, out var resource) && resource is Geometry geometry) + if (this.TryFindResource(key, out object? resource) && resource is Geometry geometry) { Data = geometry; } @@ -107,11 +107,11 @@ public override void Render(DrawingContext context) { return; } - var scale = Size / ViewBox; + double scale = Size / ViewBox; // 在实际分到的框里居中再画。默认对齐下这一步是零位移(框正好是 Size×Size); // 但调用方显式写了 Stretch、或父容器给了更大的框时,图标仍然待在正中, // 而不是缩在左上角。 - var transform = Matrix.CreateScale(scale, scale) * + Matrix transform = Matrix.CreateScale(scale, scale) * Matrix.CreateTranslation((Bounds.Width - Size) / 2, (Bounds.Height - Size) / 2); using (context.PushTransform(transform)) { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsDetailsViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsDetailsViewModel.cs index f57b123..84f93ec 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsDetailsViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsDetailsViewModel.cs @@ -20,16 +20,7 @@ internal sealed class GridFsDetailsViewModel : ObservableObject, IDisposable { private readonly GridFsTabViewModel _owner; private CancellationTokenSource? _loadCts; - private GridFsEntry? _entry; private GridFsFile? _file; - private Bitmap? _image; - private string _previewMessage = ""; - private string _imageInfo = ""; - private bool _isPreviewLoading; - private string _metadataText = ""; - private string _metadataDraft = ""; - private string? _metadataError; - private bool _isEditing; /// 构造。 /// 所属标签页。 @@ -39,7 +30,7 @@ public GridFsDetailsViewModel(GridFsTabViewModel owner) DownloadCommand = new(() => _file is { } f ? _owner.DownloadFileAsync(f) : Task.CompletedTask, () => _file is not null); OpenExternalCommand = new(() => _file is { } f ? _owner.OpenExternalAsync(f) : Task.CompletedTask, () => _file is not null); DeleteCommand = new(() => _file is { } f ? _owner.DeleteVersionAsync(f, [.. Versions]) : Task.CompletedTask, () => _file is not null); - ZoomCommand = new(Zoom, () => _image is not null); + ZoomCommand = new(Zoom, () => Image is not null); EditMetadataCommand = new(BeginEdit, () => _file is not null); SaveMetadataCommand = new(SaveMetadataAsync); CancelMetadataCommand = new(() => IsEditing = false); @@ -60,10 +51,10 @@ public GridFsDetailsViewModel(GridFsTabViewModel owner) /// 当前行(文件或目录);没选为 。 public GridFsEntry? Entry { - get => _entry; + get; private set { - if (SetProperty(ref _entry, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(HasEntry), nameof(IsEmpty), nameof(IsFolder), nameof(IsFile), nameof(Name), nameof(IconKey), nameof(IconToken), nameof(FolderSummary), nameof(FolderLastUpload)); @@ -90,25 +81,25 @@ private set } /// 有选中。 - public bool HasEntry => _entry is not null; + public bool HasEntry => Entry is not null; /// 空态(没选)。 - public bool IsEmpty => _entry is null; + public bool IsEmpty => Entry is null; /// 选的是文件。 - public bool IsFile => _entry?.IsFile == true; + public bool IsFile => Entry?.IsFile == true; /// 选的是目录。 - public bool IsFolder => _entry?.IsFolder == true; + public bool IsFolder => Entry?.IsFolder == true; /// 头部名字。 - public string Name => _file?.BaseName ?? _entry?.Name ?? ""; + public string Name => _file?.BaseName ?? Entry?.Name ?? ""; /// 头部图标。 - public string IconKey => _entry?.IconKey ?? "Fs.file"; + public string IconKey => Entry?.IconKey ?? "Fs.file"; /// 头部图标颜色。 - public string IconToken => _entry?.IconToken ?? "VelaTextTertiary"; + public string IconToken => Entry?.IconToken ?? "VelaTextTertiary"; /// 头部徽章:最新版本「v3 · 最新」,旧版本「v2」。 public string BadgeText @@ -130,12 +121,12 @@ public string BadgeText // ── 目录 ─────────────────────────────────────────────────────────────── /// 目录摘要「12 个文件 · 61.9 MB」。 - public string FolderSummary => _entry is { IsFolder: true } folder + public string FolderSummary => Entry is { IsFolder: true } folder ? Loc.Format("Fs_FolderSummary", BsonText.Grouped(folder.FileCount), BsonText.Bytes(folder.Size)) : ""; /// 目录里最近一次上传。 - public string FolderLastUpload => _entry is { IsFolder: true, Uploaded: { } at } + public string FolderLastUpload => Entry is { IsFolder: true, Uploaded: { } at } ? Loc.Format("Fs_LastUpload", GridFsEntry.FormatLocal(at, "yyyy-MM-dd HH:mm")) : ""; @@ -147,10 +138,10 @@ public string BadgeText /// public Bitmap? Image { - get => _image; + get; private set { - if (SetProperty(ref _image, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasImage)); ZoomCommand.RaiseCanExecuteChanged(); @@ -159,28 +150,28 @@ private set } /// 有图。 - public bool HasImage => _image is not null; + public bool HasImage => Image is not null; /// 没图时预览框里那句(「无法预览」「超过 8 MB,不预览」「正在加载预览…」)。 public string PreviewMessage { - get => _previewMessage; - private set => SetProperty(ref _previewMessage, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 预览加载中。 public bool IsPreviewLoading { - get => _isPreviewLoading; - private set => SetProperty(ref _isPreviewLoading, value); + get; + private set => SetProperty(ref field, value); } /// 「1600 × 1600 · JPEG」。 public string ImageInfo { - get => _imageInfo; - private set => SetProperty(ref _imageInfo, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; // ── fs.files 字段 ────────────────────────────────────────────────────── @@ -212,44 +203,44 @@ public string ImageInfo /// metadata 的 mongosh 写法(只读显示)。 public string MetadataText { - get => _metadataText; + get; private set { - if (SetProperty(ref _metadataText, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(MetadataHeight)); } } - } + } = ""; /// /// 只读 metadata 框的高度:按行数撑开(每行 14px + 编辑器上下内边距与边框),最多 180 再滚动。 /// 编辑器(AvaloniaEdit)放在 StackPanel 里拿到的是无限高,量不出内容高度 —— 只能由这里给。 /// - public double MetadataHeight => Math.Clamp(((_metadataText.Count(static c => c == '\n') + 1) * 14) + 18, 40, 180); + public double MetadataHeight => Math.Clamp(((MetadataText.Count(static c => c == '\n') + 1) * 14) + 18, 40, 180); /// 编辑中的草稿。 public string MetadataDraft { - get => _metadataDraft; + get; set { - if (SetProperty(ref _metadataDraft, value ?? "")) + if (SetProperty(ref field, value ?? "")) { - MetadataError = ShellJson.TryParseDocument(_metadataDraft, out _, out string? error) + MetadataError = ShellJson.TryParseDocument(field, out _, out string? error) ? null : Loc.Format("Fs_MetadataInvalid", error); } } - } + } = ""; /// 草稿解析错误;没有为 。 public string? MetadataError { - get => _metadataError; + get; private set { - if (SetProperty(ref _metadataError, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasMetadataError)); } @@ -257,15 +248,15 @@ private set } /// 有解析错误。 - public bool HasMetadataError => _metadataError is not null; + public bool HasMetadataError => MetadataError is not null; /// 正在编辑 metadata。 public bool IsEditing { - get => _isEditing; + get; private set { - if (SetProperty(ref _isEditing, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(IsViewing)); } @@ -273,7 +264,7 @@ private set } /// 只读显示中。 - public bool IsViewing => !_isEditing; + public bool IsViewing => !IsEditing; // ── 版本 ─────────────────────────────────────────────────────────────── @@ -524,7 +515,7 @@ internal static string FormatName(string contentType, string filename) private void Zoom() { - if (_image is { } image && _file is { } file) + if (Image is { } image && _file is { } file) { _owner.Workspace.ShowDialog(new GridFsImageDialogViewModel(_owner, file, image, ImageInfo)); } @@ -548,7 +539,7 @@ private async Task SaveMetadataAsync() { return; } - if (!ShellJson.TryParseDocument(_metadataDraft, out BsonDocument parsed, out string? error)) + if (!ShellJson.TryParseDocument(MetadataDraft, out BsonDocument parsed, out string? error)) { MetadataError = Loc.Format("Fs_MetadataInvalid", error); return; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsEntry.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsEntry.cs index 8cddd4f..7a28333 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsEntry.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsEntry.cs @@ -30,11 +30,6 @@ internal enum GridFsEntryKind /// internal sealed class GridFsEntry : ObservableObject { - private bool _isChecked; - private double _progress; - private string _progressText = ""; - private Bitmap? _thumbnail; - private GridFsEntry(GridFsEntryKind kind, string name, string path) { Kind = kind; @@ -96,10 +91,10 @@ private GridFsEntry(GridFsEntryKind kind, string name, string path) /// 勾选。 public bool IsChecked { - get => _isChecked; + get; set { - if (CanCheck && SetProperty(ref _isChecked, value)) + if (CanCheck && SetProperty(ref field, value)) { CheckedChanged?.Invoke(this); } @@ -171,24 +166,24 @@ public bool IsChecked /// 上传进度(0–1)。 public double Progress { - get => _progress; - set => SetProperty(ref _progress, value); + get; + set => SetProperty(ref field, value); } /// 上传进度的块数(3 / 5)。 public string ProgressText { - get => _progressText; - set => SetProperty(ref _progressText, value); - } + get; + set => SetProperty(ref field, value); + } = ""; /// 缩略图(缩略图视图里懒加载)。 public Bitmap? Thumbnail { - get => _thumbnail; + get; set { - if (SetProperty(ref _thumbnail, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasThumbnail)); } @@ -196,7 +191,7 @@ public Bitmap? Thumbnail } /// 有缩略图。 - public bool HasThumbnail => _thumbnail is not null; + public bool HasThumbnail => Thumbnail is not null; /// 「..」行。 public static GridFsEntry Parent(string parentPrefix) => new(GridFsEntryKind.Parent, "..", parentPrefix); @@ -257,7 +252,6 @@ internal enum GridFsTransferKind /// internal sealed class GridFsTransfer { - private long _bytes; /// 种类。 public required GridFsTransferKind Kind { get; init; } @@ -293,14 +287,14 @@ internal sealed class GridFsTransfer public long ChunkTotal => Math.Max(1, (Size + ChunkSize - 1) / ChunkSize); /// 已走过的字节(任意线程写,UI 定时读)。 - public long Bytes => Interlocked.Read(ref _bytes); + public long Bytes { get => Interlocked.Read(ref field); private set; } /// 进度回调(驱动在线程池线程上调)。 public IProgress Reporter => new Sink(this); private sealed class Sink(GridFsTransfer owner) : IProgress { - public void Report(long value) => Interlocked.Exchange(ref owner._bytes, value); + public void Report(long value) => Interlocked.Exchange(ref owner.Bytes, value); } } @@ -320,22 +314,18 @@ internal sealed record GridFsCrumb(string Name, string Prefix, bool IsLast); /// 是不是详情当前展示的那份。 internal sealed class GridFsVersionRow(GridFsFile file, int number, bool isCurrent, bool isShown) : ObservableObject { - private GridFsFile _file = file; - private bool _isShown = isShown; - private bool _isChecked; - /// 这一份(改了 metadata 之后换成重读的那份)。 public GridFsFile File { - get => _file; + get; set { - if (SetProperty(ref _file, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(SizeText), nameof(DateText)); } } - } + } = file; /// 版本号(按上传时间从旧到新 1、2、3;GridFS 不存版本号,删掉旧的之后其余的按位置重排)。 public int Number { get; } = number; @@ -346,15 +336,15 @@ public GridFsFile File /// 是不是详情当前展示的那份。 public bool IsShown { - get => _isShown; - set => SetProperty(ref _isShown, value); - } + get; + set => SetProperty(ref field, value); + } = isShown; /// 勾选了(准备一起删)。只有旧版本能勾。 public bool IsChecked { - get => _isChecked; - set => SetProperty(ref _isChecked, value && !IsCurrent); + get; + set => SetProperty(ref field, value && !IsCurrent); } /// v3。 diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsImageDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsImageDialogViewModel.cs index 52e860f..df44c52 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsImageDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsImageDialogViewModel.cs @@ -9,7 +9,6 @@ namespace VelaShell.Plugin.Mongo.Ui; internal sealed class GridFsImageDialogViewModel : DialogViewModel, IViewFactory { private readonly GridFsTabViewModel _owner; - private bool _actualSize; /// 构造。 /// 所属标签页(下载走它的传输队列)。 @@ -52,7 +51,7 @@ public GridFsImageDialogViewModel(GridFsTabViewModel owner, GridFsFile file, Bit /// 适应窗口。 public bool IsFit { - get => !_actualSize; + get => !IsActualSize; set { if (value) @@ -65,10 +64,10 @@ public bool IsFit /// 100%(原始像素,可滚动)。 public bool IsActualSize { - get => _actualSize; + get; set { - if (SetProperty(ref _actualSize, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(IsFit)); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsNewBucketDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsNewBucketDialogViewModel.cs index f73bb99..6594610 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsNewBucketDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsNewBucketDialogViewModel.cs @@ -12,8 +12,6 @@ namespace VelaShell.Plugin.Mongo.Ui; internal sealed class GridFsNewBucketDialogViewModel : DialogViewModel, IViewFactory { private readonly string _database; - private string _name = ""; - private string? _error; /// 构造。 /// 外壳服务。 @@ -24,7 +22,7 @@ public GridFsNewBucketDialogViewModel(IMongoWorkspace workspace, string database _database = database; Title = Loc["Fs_NewBucketTitle"]; Subtitle = database; - CreateCommand = new AsyncCommand(CreateAsync, () => GridFsPaths.ValidateBucketName(_name) is null); + CreateCommand = new AsyncCommand(CreateAsync, () => GridFsPaths.ValidateBucketName(Name) is null); } /// @@ -39,32 +37,32 @@ public GridFsNewBucketDialogViewModel(IMongoWorkspace workspace, string database /// 桶名。 public string Name { - get => _name; + get; set { - if (SetProperty(ref _name, value ?? "")) + if (SetProperty(ref field, value ?? "")) { - string? key = _name.Length == 0 ? null : GridFsPaths.ValidateBucketName(_name); + string? key = field.Length == 0 ? null : GridFsPaths.ValidateBucketName(field); Error = key is null ? null : Loc[key]; RaisePropertiesChanged(nameof(FilesName), nameof(ChunksName)); CreateCommand.RaiseCanExecuteChanged(); } } - } + } = ""; /// 预览:images.files。 - public string FilesName => (_name.Trim().Length == 0 ? "…" : _name.Trim()) + ".files"; + public string FilesName => (Name.Trim().Length == 0 ? "…" : Name.Trim()) + ".files"; /// 预览:images.chunks。 - public string ChunksName => (_name.Trim().Length == 0 ? "…" : _name.Trim()) + ".chunks"; + public string ChunksName => (Name.Trim().Length == 0 ? "…" : Name.Trim()) + ".chunks"; /// 错误。 public string? Error { - get => _error; + get; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } @@ -72,14 +70,14 @@ private set } /// 有错误。 - public bool HasError => _error is not null; + public bool HasError => Error is not null; /// 新建。 public AsyncCommand CreateCommand { get; } private async Task CreateAsync() { - string name = _name.Trim(); + string name = Name.Trim(); if (GridFsPaths.ValidateBucketName(name) is { } invalid) { Error = Loc[invalid]; diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogView.axaml.cs index a0e5d58..1cf8118 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogView.axaml.cs @@ -14,7 +14,7 @@ internal GridFsRenameDialogView(GridFsRenameDialogViewModel viewModel) DataContext = viewModel; AttachedToVisualTree += (_, _) => Dispatcher.UIThread.Post(() => { - NameBox.Focus(); + _ = NameBox.Focus(); string text = NameBox.Text ?? ""; int slash = text.LastIndexOf('/'); int dot = viewModel.Entry.IsFolder ? -1 : text.LastIndexOf('.'); diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogViewModel.cs index b15a6e7..e231efc 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsRenameDialogViewModel.cs @@ -15,7 +15,6 @@ internal sealed class GridFsRenameDialogViewModel : DialogViewModel, IViewFactor { private readonly Func> _apply; private string _name; - private string? _error; /// 构造。 /// 外壳服务。 @@ -65,10 +64,10 @@ public string Name /// 错误(重名、移进自己…)。 public string? Error { - get => _error; + get; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); } @@ -76,7 +75,7 @@ private set } /// 有错误。 - public bool HasError => _error is not null; + public bool HasError => Error is not null; /// 重命名。 public AsyncCommand ConfirmCommand { get; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml.cs index cedbdc1..9cb8b19 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabView.axaml.cs @@ -211,10 +211,7 @@ private void OnRowContextRequested(object? sender, ContextRequestedEventArgs e) Loc loc = _viewModel.Loc; var items = new List(); - void Add(string label, string icon, System.Windows.Input.ICommand command, object? parameter = null, bool danger = false) - { - items.Add(MenuKit.Command(label, icon, command, parameter, danger)); - } + void Add(string label, string icon, System.Windows.Input.ICommand command, object? parameter = null, bool danger = false) => items.Add(MenuKit.Command(label, icon, command, parameter, danger)); if (entry.IsFolder) { @@ -244,7 +241,7 @@ private async Task LaunchAsync(string path) { return true; } - using Process? process = Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + using var process = Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); // UseShellExecute 交给已在运行的程序打开时会返回 null,那也算成功。 return true; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.Actions.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.Actions.cs index 6c721f0..6027729 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.Actions.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.Actions.cs @@ -29,9 +29,6 @@ internal sealed partial class GridFsTabViewModel private CancellationTokenSource? _thumbCts; private GridFsTransfer? _current; private string? _reselectPath; - private string _lastTransfer = ""; - private double _transferProgress; - private string _transferStats = ""; /// 上传文件(多选)。 public AsyncCommand UploadFilesCommand { get; private set; } = null!; @@ -97,23 +94,26 @@ private void InitializeActions() public string TransferIconToken => _current is null ? "VelaTextMuted" : "VelaAccent"; /// 「正在上传 detail-07.jpg」;空闲时是上一批的结果或「无传输任务」。 - public string TransferTitle => _current is { } job + public string TransferTitle + { + get => _current is { } job ? Loc.Format(job.Kind == GridFsTransferKind.Upload ? "Fs_Uploading" : "Fs_Downloading", job.Name) - : _lastTransfer.Length > 0 ? _lastTransfer : Loc["Fs_Idle"]; + : field.Length > 0 ? field : Loc["Fs_Idle"]; private set; + } = ""; /// 当前这一个的进度(0–1)。 public double TransferProgress { - get => _transferProgress; - private set => SetProperty(ref _transferProgress, value); + get; + private set => SetProperty(ref field, value); } /// 「62% · 3.1 MB/s · 剩余 1 s」。 public string TransferStats { - get => _transferStats; - private set => SetProperty(ref _transferStats, value); - } + get; + private set => SetProperty(ref field, value); + } = ""; /// 「队列:2 个文件」;队列空为空。 public string QueueText => _queue.Count > 0 ? Loc.Format("Fs_Queue", _queue.Count) : ""; @@ -243,7 +243,7 @@ internal BsonDocument DefaultMetadata(IReadOnlyList items) /// 某个文件的 metadata:模板里没写 contentType 的,按这个文件的扩展名补上。 internal static BsonDocument MetadataFor(BsonDocument template, string relative) { - var doc = template.DeepClone().AsBsonDocument; + BsonDocument doc = template.DeepClone().AsBsonDocument; if (!doc.Contains("contentType")) { doc.InsertAt(0, new BsonElement("contentType", GridFsPaths.GuessContentType(relative))); @@ -442,13 +442,13 @@ private async Task PumpAsync() { await using var source = new FileStream(job.LocalPath, FileMode.Open, FileAccess.Read, FileShare.Read, 1 << 16, FileOptions.Asynchronous | FileOptions.SequentialScan); - await _service.UploadAsync(job.Filename, source, job.Metadata, job.Reporter, job.ChunkSize, cts.Token) + _ = await _service.UploadAsync(job.Filename, source, job.Metadata, job.Reporter, job.ChunkSize, cts.Token) .ConfigureAwait(true); uploaded = true; } else { - Directory.CreateDirectory(Path.GetDirectoryName(job.LocalPath)!); + _ = Directory.CreateDirectory(Path.GetDirectoryName(job.LocalPath)!); await using (var target = new FileStream(job.LocalPath, FileMode.Create, FileAccess.Write, FileShare.None, 1 << 16, FileOptions.Asynchronous)) { @@ -502,7 +502,7 @@ await _service.UploadAsync(job.Filename, source, job.Metadata, job.Reporter, job _queue.Clear(); _pendingRows.Clear(); } - _lastTransfer = cancelled + TransferTitle = cancelled ? Loc["Fs_TransfersCancelled"] : done == 0 ? "" : Loc.Format(kind == GridFsTransferKind.Upload ? "Fs_UploadDone" : "Fs_DownloadDone", done, BsonText.Bytes(bytes)); TransferProgress = 0; @@ -551,8 +551,8 @@ private void RemovePendingRow(GridFsTransfer job) GridFsEntry? row = _pendingRows.FirstOrDefault(r => ReferenceEquals(r.Transfer, job)); if (row is not null) { - _pendingRows.Remove(row); - Entries.Remove(row); + _ = _pendingRows.Remove(row); + _ = Entries.Remove(row); } } @@ -578,8 +578,8 @@ private void CancelTransfers() _queue.Clear(); foreach (GridFsEntry row in _pendingRows.Where(r => !ReferenceEquals(r.Transfer, _current)).ToList()) { - _pendingRows.Remove(row); - Entries.Remove(row); + _ = _pendingRows.Remove(row); + _ = Entries.Remove(row); } RaiseTransfer(); } @@ -773,18 +773,18 @@ internal async Task DeleteVersionAsync(GridFsFile file, IReadOnlyList doomed, string labels = string.Join(", ", old.OrderBy(static v => v.Number).Select(static v => v.Label)); int kept = versions.Count - old.Count; if (!await Workspace.ConfirmAsync(new() - { - Title = Loc["Fs_DeleteVersionsTitle"], - Message = Loc.Format("Fs_DeleteVersionsBody", labels, latest.BaseName, old.Count, kept), - ConfirmLabel = Loc.Format("Fs_DeleteVersionsConfirm", old.Count), - IconKey = "Mongo.trash-2", - Facts = + { + Title = Loc["Fs_DeleteVersionsTitle"], + Message = Loc.Format("Fs_DeleteVersionsBody", labels, latest.BaseName, old.Count, kept), + ConfirmLabel = Loc.Format("Fs_DeleteVersionsConfirm", old.Count), + IconKey = "Mongo.trash-2", + Facts = [ new(Loc["Fs_FactVersions"], BsonText.Grouped(old.Count)), new(Loc["Fs_FactChunks"], BsonText.Grouped(chunks)), new(Loc["Fs_FactFreed"], BsonText.Bytes(bytes)) ], - TypeToConfirm = Workspace.Guard.ConfirmWrites ? Bucket.Name : null - }).ConfigureAwait(true)) + TypeToConfirm = Workspace.Guard.ConfirmWrites ? Bucket.Name : null + }).ConfigureAwait(true)) { return; } @@ -880,7 +880,7 @@ internal async Task RestoreVersionAsync(GridFsFile version, string label) } try { - await _service.RestoreAsync(version).ConfigureAwait(true); + _ = await _service.RestoreAsync(version).ConfigureAwait(true); Workspace.Toast(new() { Title = Loc.Format("Fs_Restored", label, version.BaseName), Kind = ToastKind.Success }); } catch (Exception ex) when (ex is MongoException or TimeoutException) @@ -921,20 +921,20 @@ private async Task CheckOrphansAsync() long chunks = orphans.Sum(static o => o.Chunks); long bytes = orphans.Sum(static o => o.Bytes); if (!await Workspace.ConfirmAsync(new() - { - Title = Loc["Fs_OrphansTitle"], - Message = Loc.Format("Fs_OrphansBody", BsonText.Grouped(chunks), BsonText.Bytes(bytes), BsonText.Grouped(orphans.Count), + { + Title = Loc["Fs_OrphansTitle"], + Message = Loc.Format("Fs_OrphansBody", BsonText.Grouped(chunks), BsonText.Bytes(bytes), BsonText.Grouped(orphans.Count), Bucket.FilesCollection), - ConfirmLabel = Loc["Fs_OrphansConfirm"], - IconKey = "Mongo.layers", - Facts = + ConfirmLabel = Loc["Fs_OrphansConfirm"], + IconKey = "Mongo.layers", + Facts = [ new(Loc["Fs_FactOrphanFiles"], BsonText.Grouped(orphans.Count)), new(Loc["Fs_FactChunks"], BsonText.Grouped(chunks)), new(Loc["Fs_FactSize"], BsonText.Bytes(bytes)) ], - TypeToConfirm = Workspace.Guard.ConfirmWrites ? Bucket.Name : null - }).ConfigureAwait(true)) + TypeToConfirm = Workspace.Guard.ConfirmWrites ? Bucket.Name : null + }).ConfigureAwait(true)) { return; } @@ -1029,9 +1029,7 @@ private async Task LoadThumbnailsAsync() using var stream = new MemoryStream(bytes, writable: false); return width is { } w ? Bitmap.DecodeToWidth(stream, w) : new Bitmap(stream); } -#pragma warning disable CA1031 // 解码器对坏图抛的异常类型因平台与格式而异;任何一种都只意味着"这张没法预览"。 catch (Exception) -#pragma warning restore CA1031 { return null; } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.cs index bc05bab..4848481 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsTabViewModel.cs @@ -35,22 +35,12 @@ internal enum GridFsSort /// internal sealed partial class GridFsTabViewModel : WorkspaceTab { - private readonly GridFsService _service; private readonly DispatcherTimer _searchDelay; private CancellationTokenSource? _loadCts; private List _listed = []; - private string _prefix = ""; private bool _virtualDirs = true; - private GridFsTypeFilter _typeFilter = GridFsTypeFilter.All; private string _searchText = ""; - private GridFsSort _sort = GridFsSort.Uploaded; - private bool _sortDescending = true; - private bool _isGridView; private GridFsEntry? _selectedEntry; - private bool _isLoading; - private bool _truncated; - private string? _loadError; - private IReadOnlyList _crumbs = []; private bool _bulkChecking; private bool _applying; @@ -63,7 +53,7 @@ public GridFsTabViewModel(IMongoWorkspace workspace, GridFsBucketInfo bucket) Bucket = bucket; Title = bucket.Name; Scope = "@" + bucket.Database; - _service = new GridFsService(workspace.Connection, bucket); + Service = new GridFsService(workspace.Connection, bucket); Details = new GridFsDetailsViewModel(this); _searchDelay = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) }; _searchDelay.Tick += (_, _) => @@ -75,7 +65,7 @@ public GridFsTabViewModel(IMongoWorkspace workspace, GridFsBucketInfo bucket) _transferTicker.Tick += (_, _) => UpdateTransferProgress(); NavigateCommand = new(prefix => _ = NavigateAsync(prefix)); - UpCommand = new(() => _ = NavigateAsync(GridFsPaths.Parent(_prefix)), () => _prefix.Length > 0); + UpCommand = new(() => _ = NavigateAsync(GridFsPaths.Parent(Prefix)), () => Prefix.Length > 0); OpenEntryCommand = new(OpenEntryAsync); SortCommand = new(SortBy); InitializeActions(); @@ -102,7 +92,7 @@ public GridFsTabViewModel(IMongoWorkspace workspace, GridFsBucketInfo bucket) public override string IconToken => "VelaWarning"; /// 服务(详情面板与对话框共用)。 - internal GridFsService Service => _service; + internal GridFsService Service { get; } /// 文件表的行(「..」、目录、文件、上传中)。 public ObservableCollection Entries { get; } = []; @@ -111,11 +101,7 @@ public GridFsTabViewModel(IMongoWorkspace workspace, GridFsBucketInfo bucket) public GridFsDetailsViewModel Details { get; } /// 面包屑(当前目录逐级)。 - public IReadOnlyList Crumbs - { - get => _crumbs; - private set => SetProperty(ref _crumbs, value); - } + public IReadOnlyList Crumbs { get; private set => SetProperty(ref field, value); } = []; /// 跳到某一级。 public RelayCommand NavigateCommand { get; } @@ -132,13 +118,13 @@ public IReadOnlyList Crumbs // ── 目录与筛选 ───────────────────────────────────────────────────────── /// 当前虚拟目录(带结尾斜杠;根为空串)。 - public string Prefix => _prefix; + public string Prefix { get; private set; } = ""; /// 当前目录给人看的写法(根是 /)。 - public string PrefixText => _prefix.Length == 0 ? "/" : _prefix; + public string PrefixText => Prefix.Length == 0 ? "/" : Prefix; /// 实际列的前缀:关掉虚拟目录时平铺整个桶。 - private string ListPrefix => _virtualDirs ? _prefix : ""; + private string ListPrefix => _virtualDirs ? Prefix : ""; /// 按 / 显示为虚拟目录。 public bool VirtualDirs @@ -161,21 +147,20 @@ public bool VirtualDirs /// 类型芯片。 public GridFsTypeFilter TypeFilter { - get => _typeFilter; - set + get; set { - if (SetProperty(ref _typeFilter, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(IsAllTypes), nameof(IsImages), nameof(IsDocuments), nameof(IsVideos)); _ = ReloadAsync(); } } - } + } = GridFsTypeFilter.All; /// 芯片「全部」。 public bool IsAllTypes { - get => _typeFilter == GridFsTypeFilter.All; + get => TypeFilter == GridFsTypeFilter.All; set { if (value) @@ -188,7 +173,7 @@ public bool IsAllTypes /// 芯片「图片」。 public bool IsImages { - get => _typeFilter == GridFsTypeFilter.Images; + get => TypeFilter == GridFsTypeFilter.Images; set { if (value) @@ -201,7 +186,7 @@ public bool IsImages /// 芯片「文档」。 public bool IsDocuments { - get => _typeFilter == GridFsTypeFilter.Documents; + get => TypeFilter == GridFsTypeFilter.Documents; set { if (value) @@ -214,7 +199,7 @@ public bool IsDocuments /// 芯片「视频」。 public bool IsVideos { - get => _typeFilter == GridFsTypeFilter.Videos; + get => TypeFilter == GridFsTypeFilter.Videos; set { if (value) @@ -244,7 +229,7 @@ public string SearchText /// 列表视图。 public bool IsListView { - get => !_isGridView; + get => !IsGridView; set { if (value) @@ -257,10 +242,9 @@ public bool IsListView /// 缩略图视图。 public bool IsGridView { - get => _isGridView; - set + get; set { - if (SetProperty(ref _isGridView, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(IsListView)); if (value) @@ -274,13 +258,13 @@ public bool IsGridView // ── 排序 ─────────────────────────────────────────────────────────────── /// 排序列。 - public GridFsSort Sort => _sort; + public GridFsSort Sort { get; private set; } = GridFsSort.Uploaded; /// 倒序。 - public bool SortDescending => _sortDescending; + public bool SortDescending { get; private set; } = true; /// 列头箭头的图标(arrow-down / arrow-up)。 - public string SortIcon => _sortDescending ? "Mongo.arrow-down" : "Mongo.arrow-up"; + public string SortIcon => SortDescending ? "Mongo.arrow-down" : "Mongo.arrow-up"; private void SortBy(string column) { @@ -288,15 +272,15 @@ private void SortBy(string column) { return; } - if (sort == _sort) + if (sort == Sort) { - _sortDescending = !_sortDescending; + SortDescending = !SortDescending; } else { - _sort = sort; + Sort = sort; // 名字与类型默认正序;大小、时间、块数默认大的在前 —— 点一下就是人最常要的那个方向。 - _sortDescending = sort is GridFsSort.Size or GridFsSort.Uploaded or GridFsSort.Chunks; + SortDescending = sort is GridFsSort.Size or GridFsSort.Uploaded or GridFsSort.Chunks; } RaisePropertiesChanged(nameof(Sort), nameof(SortDescending), nameof(SortIcon)); ApplyEntries(); @@ -307,13 +291,13 @@ private IEnumerable Sorted(IEnumerable entries) // 目录永远在文件前面(资源管理器的习惯);目录没有类型与块数,按这两列排时目录按名字排。 IEnumerable folders = entries.Where(static e => e.IsFolder); IEnumerable files = entries.Where(static e => !e.IsFolder); - folders = _sort switch + folders = Sort switch { GridFsSort.Size => Order(folders, static e => e.Size), GridFsSort.Uploaded => Order(folders, static e => e.Uploaded ?? DateTime.MinValue), _ => folders.OrderBy(static e => e.Name, StringComparer.OrdinalIgnoreCase) }; - files = _sort switch + files = Sort switch { GridFsSort.Name => Order(files, static e => e.Name, StringComparer.OrdinalIgnoreCase), GridFsSort.Size => Order(files, static e => e.Size), @@ -325,7 +309,7 @@ private IEnumerable Sorted(IEnumerable entries) } private IOrderedEnumerable Order(IEnumerable source, Func key, IComparer? comparer = null) => - _sortDescending ? source.OrderByDescending(key, comparer) : source.OrderBy(key, comparer); + SortDescending ? source.OrderByDescending(key, comparer) : source.OrderBy(key, comparer); // ── 选择与勾选 ───────────────────────────────────────────────────────── @@ -414,7 +398,7 @@ public string ListingSummary { get { - string count = BsonText.Grouped(_listed.Count) + (_truncated ? "+" : ""); + string count = BsonText.Grouped(_listed.Count) + (IsTruncated ? "+" : ""); return Loc.Format("Fs_Items", count, BsonText.Bytes(_listed.Sum(static e => e.Size))); } } @@ -440,10 +424,9 @@ private void RaiseSelectionChanged() /// 正在加载。 public bool IsLoading { - get => _isLoading; - private set + get; private set { - if (SetProperty(ref _isLoading, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(EmptyText), nameof(HasEmptyText)); } @@ -453,10 +436,9 @@ private set /// 加载失败的原因;成功为 。 public string? LoadError { - get => _loadError; - private set + get; private set { - if (SetProperty(ref _loadError, value)) + if (SetProperty(ref field, value)) { RaisePropertiesChanged(nameof(EmptyText), nameof(HasEmptyText)); } @@ -464,19 +446,15 @@ private set } /// 被截断了(超过 项)。 - public bool IsTruncated - { - get => _truncated; - private set => SetProperty(ref _truncated, value); - } + public bool IsTruncated { get; private set => SetProperty(ref field, value); } /// 截断提示。 public string TruncatedText => Loc.Format("Fs_Truncated", BsonText.Grouped(GridFsService.ListLimit)); /// 空态文字(加载中 / 失败 / 空目录 / 搜索无结果);有内容时为空。 public string EmptyText => - _isLoading && _listed.Count == 0 ? Loc["Common_Loading"] - : _loadError is { } error ? Loc.Format("Common_Failed", error) + IsLoading && _listed.Count == 0 ? Loc["Common_Loading"] + : LoadError is { } error ? Loc.Format("Common_Failed", error) : _listed.Count > 0 ? "" : IsSearching ? Loc.Format("Fs_NoMatch", _searchText.Trim()) : Loc["Fs_Empty"]; @@ -503,7 +481,7 @@ public async Task ReloadAsync() try { string listPrefix = ListPrefix; - GridFsListing listing = await _service.ListAsync(listPrefix, _virtualDirs, _typeFilter, _searchText, cts.Token) + GridFsListing listing = await Service.ListAsync(listPrefix, _virtualDirs, TypeFilter, _searchText, cts.Token) .ConfigureAwait(true); if (cts.IsCancellationRequested) { @@ -521,7 +499,7 @@ public async Task ReloadAsync() IsTruncated = listing.Truncated; LoadError = null; ApplyEntries(); - if (_isGridView) + if (IsGridView) { _ = LoadThumbnailsAsync(); } @@ -568,9 +546,9 @@ private void ApplyEntries() } var rows = new List(); - if (_virtualDirs && _prefix.Length > 0 && !IsSearching) + if (_virtualDirs && Prefix.Length > 0 && !IsSearching) { - rows.Add(GridFsEntry.Parent(GridFsPaths.Parent(_prefix))); + rows.Add(GridFsEntry.Parent(GridFsPaths.Parent(Prefix))); } rows.AddRange(Sorted(_listed)); rows.AddRange(_pendingRows.Where(ShowsUploadRow)); @@ -619,7 +597,7 @@ private void ApplyEntries() /// 上传行是不是该出现在当前这一页。 private bool ShowsUploadRow(GridFsEntry row) => !IsSearching && (_virtualDirs - ? GridFsPaths.DirectoryOf(row.Path) == _prefix + ? GridFsPaths.DirectoryOf(row.Path) == Prefix : row.Path.StartsWith(ListPrefix, StringComparison.Ordinal)); // ── 导航 ─────────────────────────────────────────────────────────────── @@ -632,7 +610,7 @@ public async Task NavigateAsync(string? prefix) { entry.IsChecked = false; } - _prefix = target; + Prefix = target; _searchDelay.Stop(); if (_searchText.Length > 0) { @@ -671,9 +649,9 @@ private async Task OpenEntryAsync(GridFsEntry entry) private void UpdateCrumbs() { var crumbs = new List(); - if (_virtualDirs && _prefix.Length > 0) + if (_virtualDirs && Prefix.Length > 0) { - string[] parts = _prefix.TrimEnd('/').Split('/'); + string[] parts = Prefix.TrimEnd('/').Split('/'); string path = ""; for (int i = 0; i < parts.Length; i++) { diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsUploadDialogViewModel.cs b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsUploadDialogViewModel.cs index cfac3d6..d401956 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsUploadDialogViewModel.cs +++ b/plugins/VelaShell.Plugin.Mongo/Ui/GridFs/GridFsUploadDialogViewModel.cs @@ -22,7 +22,6 @@ internal sealed class GridFsUploadDialogViewModel : DialogViewModel, IViewFactor private readonly TaskCompletionSource _result = new(TaskCreationOptions.RunContinuationsAsynchronously); private string _target; private string _metadataText; - private string? _error; /// 构造。 /// 外壳服务。 @@ -39,7 +38,7 @@ public GridFsUploadDialogViewModel(IMongoWorkspace workspace, GridFsBucketInfo b Subtitle = $"{bucket.Database}.{bucket.Name}"; _target = prefix; _metadataText = BsonText.Pretty(metadata); - ConfirmCommand = new RelayCommand(Confirm, () => _error is null); + ConfirmCommand = new RelayCommand(Confirm, () => Error is null); } /// @@ -83,10 +82,10 @@ public string MetadataText /// metadata 解析错误。 public string? Error { - get => _error; + get; private set { - if (SetProperty(ref _error, value)) + if (SetProperty(ref field, value)) { RaisePropertyChanged(nameof(HasError)); ConfirmCommand.RaiseCanExecuteChanged(); @@ -95,7 +94,7 @@ private set } /// 有错误。 - public bool HasError => _error is not null; + public bool HasError => Error is not null; /// 确认按钮文字「上传 3 个文件」。 public string ConfirmLabel => Loc.Format("Fs_UploadConfirm", Items.Count); @@ -113,14 +112,14 @@ private void Confirm() Error = Loc.Format("Fs_MetadataInvalid", error); return; } - _result.TrySetResult(new(GridFsPaths.NormalizePrefix(_target), metadata)); + _ = _result.TrySetResult(new(GridFsPaths.NormalizePrefix(_target), metadata)); Close(); } /// internal override void OnClosed() { - _result.TrySetResult(null); + _ = _result.TrySetResult(null); base.OnClosed(); } diff --git a/plugins/VelaShell.Plugin.Mongo/Ui/MongoStyles.axaml b/plugins/VelaShell.Plugin.Mongo/Ui/MongoStyles.axaml index d3d3860..6a94f07 100644 --- a/plugins/VelaShell.Plugin.Mongo/Ui/MongoStyles.axaml +++ b/plugins/VelaShell.Plugin.Mongo/Ui/MongoStyles.axaml @@ -457,6 +457,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +