import Foundation /// A new review comment on lines of a merge request's diff: where it is /// anchored, and the ```suggestion block that proposes replacement lines. /// `mr diff-comment` decides whether the anchor is in the diff; this only /// builds what the command is asked. nonisolated struct DiffCommentDraft: Sendable, Hashable, Identifiable { let path: String /// The old side is a removed line; everything else anchors on the new file. let old: Bool /// The last line of the range, the one the comment was started from. let line: Int /// The lines on the same side of the hunk, in order, that a range may /// start from: the tapped line and those above it, back to the first /// line of the hunk on that side. let candidates: [(number: Int, text: String)] var id: String { "\(path):\(old ? "old" : "new"):\(line)" } static func == (a: Self, b: Self) -> Bool { a.id == b.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } /// A suggestion replaces new-file lines; a removed line has nothing /// to replace. var canSuggest: Bool { !old } /// The earliest line a range may start on. var firstLine: Int { candidates.first?.number ?? line } /// The draft for a line of the diff, or nil for a line no side can /// anchor (none in a well-formed diff). init?(file: UnifiedDiff.File, hunk: UnifiedDiff.Hunk, line target: UnifiedDiff.Line) { let old = target.kind == .deletion guard let number = old ? target.oldNumber : target.newNumber else { return nil } // The server checks --path against the diff's file names, which // list a rename under its new name only, on either side. self.path = file.displayPath self.old = old self.line = number var lines: [(Int, String)] = [] for l in hunk.lines { let n = old ? (l.kind == .addition ? nil : l.oldNumber) : (l.kind == .deletion ? nil : l.newNumber) guard let n else { continue } if n > number { break } lines.append((n, l.text)) } self.candidates = lines } /// `mr diff-comment --path

--line [--start-line ] [--old] --file -`. func argv(repo: String, number: Int64, startLine: Int) -> [String] { var argv = ["mr", "diff-comment", repo, String(number), "--path", path, "--line", String(line)] if startLine < line { argv += ["--start-line", String(startLine)] } if old { argv.append("--old") } argv += ["--file", "-"] return argv } /// The ```suggestion block for lines `startLine` through `line`, with /// the current text in it to edit. func suggestion(from startLine: Int) -> String { let text = candidates.filter { $0.number >= startLine }.map(\.text) return "```suggestion\n" + text.map { $0 + "\n" }.joined() + "```\n" } /// The body after the range's start moves from `old` to `new`: an /// untouched suggestion is rebuilt to cover the new range, since /// sending the old one would replace the whole range with fewer /// lines. Text the user changed is theirs and stays. func rebuilt(_ text: String, from old: Int, to new: Int) -> String { text == suggestion(from: old) ? suggestion(from: new) : text } }