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; /// /// 머지 주석(// [이름] 날짜)을 차이로 세지 않을지(6차 B-5). /// /// 주석은 소스에 남기되 비교에서는 무시한다. 줄 전체가 주석이면 줄 필터처럼 걸러지고, /// 줄 끝에 붙은 주석은 그 조각만 떼고 견준다 - 코드가 같으면 같은 줄이다. /// 보는 방식일 뿐 파일의 글은 건드리지 않는다. /// public bool IgnoreMergeNotes { get; set; } /// 머지 주석 기호를 고를 파일 경로. 확장자만 본다. public string? NotePath { get; set; } /// /// 주석만 있는 줄의 비교용 키. 줄마다 이름·날짜가 달라도 서로 같은 줄로 짝지어진다. /// 빈 줄과 짝지어지지 않도록 소스에 나올 수 없는 글자로 만든다. /// public const string MergeNoteKey = "\u0000merge-note"; private Regex[]? _compiled; private string? _compiledFrom; /// 어떤 규칙도 켜져 있지 않으면 비교용 변환을 건너뛸 수 있다. public bool IsPlain => Whitespace == WhitespaceMode.Exact && !IgnoreCase && !IgnoreMergeNotes; /// /// 정렬에서 "같은 줄인가"를 따질 때 쓰는 문자열. /// 원문은 그대로 두고 이 값만 비교하므로 화면 표시는 달라지지 않는다. /// public string KeyOf(string line) { if (line.Length == 0) return line; if (IgnoreMergeNotes) { if (MergeNote.LooksLikeNote(line, NotePath)) return MergeNoteKey; var cut = MergeNote.TailStart(line, NotePath); if (cut > 0) line = line.Substring(0, cut); } var text = Whitespace switch { WhitespaceMode.IgnoreAll => StripSpaces(line), WhitespaceMode.IgnoreAmount => CollapseSpaces(line), _ => line }; return IgnoreCase ? text.ToUpperInvariant() : text; } /// 줄 필터에 걸리는 줄인지. 걸리면 차이로 세지 않는다. public bool IsFiltered(string? line) { if (line is null) return false; // 머지 주석만 있는 줄은 줄 필터 켜짐과 따로 본다 - 설정이 둘이다(6차 B-5). if (IgnoreMergeNotes && MergeNote.LooksLikeNote(line, NotePath)) return true; if (!LineFilterEnabled) 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("대소문자"); if (IgnoreMergeNotes) 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(); } } }