using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; namespace MergeHub.Services { /// 공백을 얼마나 따질지. public enum WhitespaceMode { /// 공백 하나까지 그대로 비교한다. Exact, /// 공백의 개수만 무시한다. 들여쓰기 폭을 바꿔도 같은 줄로 본다. IgnoreAmount, /// 공백을 아예 없는 셈 친다. 탭/스페이스 정리를 무시할 때. IgnoreAll } /// /// "무엇을 차이로 볼 것인가"를 정하는 규칙. /// /// 실무에서 diff 를 어지럽히는 것은 대개 내용이 아니라 형식이다 - 들여쓰기 정리, /// 대소문자, 매번 바뀌는 타임스탬프 줄. 이런 것을 걸러야 진짜 변경이 눈에 들어온다. /// /// 규칙은 두 갈래로 나뉘어 쓰인다. /// · — 정렬할 때 "같은 줄인가"를 판단하는 비교용 문자열. /// 화면에 보이는 원문은 건드리지 않는다. /// · — 줄 자체를 차이 계산에서 빼는 정규식 필터. /// 정렬을 마친 뒤에 적용한다. 필터에 걸린 줄까지 같은 것으로 묶어 정렬하면 /// 엉뚱한 줄끼리 짝지어지기 때문이다. /// public sealed class CompareRules { public WhitespaceMode Whitespace { get; set; } = WhitespaceMode.Exact; public bool IgnoreCase { get; set; } /// 이 정규식에 걸리는 줄은 차이로 세지 않는다. 잘못된 식은 조용히 버린다. public IReadOnlyList LinePatterns { get; set; } = Array.Empty(); /// 줄 필터를 켤지. 식은 그대로 두고 잠시 끄고 싶을 때가 있다. public bool LineFilterEnabled { get; set; } = true; private Regex[]? _compiled; private string? _compiledFrom; /// 어떤 규칙도 켜져 있지 않으면 비교용 변환을 건너뛸 수 있다. public bool IsPlain => Whitespace == WhitespaceMode.Exact && !IgnoreCase; /// /// 정렬에서 "같은 줄인가"를 따질 때 쓰는 문자열. /// 원문은 그대로 두고 이 값만 비교하므로 화면 표시는 달라지지 않는다. /// public string KeyOf(string line) { if (line.Length == 0) return line; var text = Whitespace switch { WhitespaceMode.IgnoreAll => StripSpaces(line), WhitespaceMode.IgnoreAmount => CollapseSpaces(line), _ => line }; return IgnoreCase ? text.ToUpperInvariant() : text; } /// 줄 필터에 걸리는 줄인지. 걸리면 차이로 세지 않는다. public bool IsFiltered(string? line) { if (!LineFilterEnabled || line is null) return false; foreach (var rule in Compiled()) { if (rule.IsMatch(line)) return true; } return false; } /// 상태바에 한 줄로 요약한다. 켜진 게 없으면 null. public string? Describe() { var parts = new List(3); if (Whitespace == WhitespaceMode.IgnoreAmount) parts.Add("공백 개수"); else if (Whitespace == WhitespaceMode.IgnoreAll) parts.Add("공백 전부"); if (IgnoreCase) parts.Add("대소문자"); var rules = Compiled().Length; if (rules > 0) parts.Add($"줄필터 {rules}"); return parts.Count == 0 ? null : "무시: " + string.Join(" · ", parts); } /// /// 정규식은 쓸 때마다 다시 만들면 느리므로 캐시한다. /// 사용자가 식을 고치면 목록 문자열이 달라지므로 그때만 다시 만든다. /// private Regex[] Compiled() { if (!LineFilterEnabled) return Array.Empty(); var signature = string.Join("", LinePatterns); if (_compiled is not null && _compiledFrom == signature) return _compiled; var list = new List(LinePatterns.Count); foreach (var pattern in LinePatterns) { if (string.IsNullOrWhiteSpace(pattern)) continue; try { list.Add(new Regex(pattern, RegexOptions.CultureInvariant)); } catch (ArgumentException) { // 사용자가 식을 고치는 중일 수 있다. 잘못된 식 하나 때문에 비교가 멈추면 안 된다. } } _compiledFrom = signature; _compiled = list.ToArray(); return _compiled; } /// 연속된 공백을 하나로 줄이고 앞뒤를 다듬는다. private static string CollapseSpaces(string line) { var sb = new StringBuilder(line.Length); var pendingSpace = false; foreach (var c in line) { if (char.IsWhiteSpace(c)) { if (sb.Length > 0) pendingSpace = true; continue; } if (pendingSpace) { sb.Append(' '); pendingSpace = false; } sb.Append(c); } return sb.ToString(); } private static string StripSpaces(string line) { var sb = new StringBuilder(line.Length); foreach (var c in line) if (!char.IsWhiteSpace(c)) sb.Append(c); return sb.ToString(); } } }