UbisamBase 플랫폼 위에서 도는 플러그인. 지금까지 만든 것을 그대로 담는다. - 폴더 비교 / 파일 비교(부분 수용·워드랩·찾기) / 3-Way 병합 / Git 조작 - 비교 엔진: histogram 정렬 · 이동 블록 감지 · 무시 규칙 · 13개 언어 토크나이저 - doc/ 에 작업 지시서와 진행 기록 빌드 산출물(bin/obj)은 추적하지 않는다 - 플랫폼 런타임이 빌드 때마다 D:\UbisamPlatform\bin 에서 복사되어 들어오므로 저장소에 담을 이유가 없다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
238 lines
12 KiB
C#
238 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
|
|
namespace MergeHub.Services
|
|
{
|
|
/// <summary>
|
|
/// 한 가지 언어(또는 문서 형식)를 어떻게 읽을지 적어 둔 표.
|
|
///
|
|
/// 언어마다 토크나이저를 따로 만들지 않는 이유가 있다. 색칠에 필요한 것은
|
|
/// "주석은 어디서 시작하나 · 문자열은 무엇으로 묶나 · 예약어는 무엇인가" 이 셋뿐이고,
|
|
/// 나머지(숫자·식별자·연산자)는 어느 언어나 거의 같다. 그래서 <see cref="CodeTokenizer"/>
|
|
/// 하나가 이 표를 보고 동작한다.
|
|
///
|
|
/// C# 만 예외로 전용 토크나이저를 그대로 쓴다 - 축자/보간 문자열(@"" $"")과
|
|
/// 접근제한자 구분처럼 표로 적기 어려운 규칙이 있고, 이미 테스트로 굳어 있다.
|
|
/// </summary>
|
|
public sealed class LanguageProfile
|
|
{
|
|
public LanguageProfile(string name,
|
|
string[] extensions,
|
|
string[]? lineComments = null,
|
|
string? blockStart = null,
|
|
string? blockEnd = null,
|
|
char[]? quotes = null,
|
|
IEnumerable<string>? keywords = null,
|
|
IEnumerable<string>? controlKeywords = null,
|
|
IEnumerable<string>? typeKeywords = null,
|
|
bool caseSensitive = true,
|
|
bool isCode = true,
|
|
bool hasBraces = false,
|
|
bool useCSharpTokenizer = false)
|
|
{
|
|
Name = name;
|
|
Extensions = extensions;
|
|
LineComments = lineComments ?? Array.Empty<string>();
|
|
BlockStart = blockStart;
|
|
BlockEnd = blockEnd;
|
|
Quotes = quotes ?? Array.Empty<char>();
|
|
CaseSensitive = caseSensitive;
|
|
IsCode = isCode;
|
|
HasBraces = hasBraces;
|
|
UseCSharpTokenizer = useCSharpTokenizer;
|
|
|
|
var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
|
|
Keywords = new HashSet<string>(keywords ?? Array.Empty<string>(), comparer);
|
|
ControlKeywords = new HashSet<string>(controlKeywords ?? Array.Empty<string>(), comparer);
|
|
TypeKeywords = new HashSet<string>(typeKeywords ?? Array.Empty<string>(), comparer);
|
|
}
|
|
|
|
public string Name { get; }
|
|
public string[] Extensions { get; }
|
|
public string[] LineComments { get; }
|
|
public string? BlockStart { get; }
|
|
public string? BlockEnd { get; }
|
|
public char[] Quotes { get; }
|
|
public bool CaseSensitive { get; }
|
|
|
|
/// <summary>문법 색을 입힐 대상인지. 평문·표 형식 문서는 false 다.</summary>
|
|
public bool IsCode { get; }
|
|
|
|
/// <summary>중괄호로 블록을 여닫는지. 코드 검사(짝 맞추기)를 걸 수 있는 조건이다.</summary>
|
|
public bool HasBraces { get; }
|
|
|
|
/// <summary>C# 전용 토크나이저를 쓸지.</summary>
|
|
public bool UseCSharpTokenizer { get; }
|
|
|
|
public HashSet<string> Keywords { get; }
|
|
public HashSet<string> ControlKeywords { get; }
|
|
public HashSet<string> TypeKeywords { get; }
|
|
|
|
public bool HasBlockComment => BlockStart is { Length: > 0 } && BlockEnd is { Length: > 0 };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 확장자로 언어를 고른다. 모르는 확장자는 평문으로 본다 -
|
|
/// 이 도구는 소스만이 아니라 설정 파일·레시피·문서도 비교해야 한다.
|
|
/// </summary>
|
|
public static class LanguageCatalog
|
|
{
|
|
// 여러 언어가 나눠 쓰는 예약어 묶음. 표를 짧게 유지하려고 따로 뺐다.
|
|
private static readonly string[] CFamilyControl =
|
|
{
|
|
"break", "case", "catch", "continue", "default", "do", "else", "finally",
|
|
"for", "goto", "if", "return", "switch", "throw", "try", "while"
|
|
};
|
|
|
|
private static readonly string[] CFamilyTypes =
|
|
{
|
|
"auto", "bool", "char", "const", "double", "float", "int", "long",
|
|
"short", "signed", "unsigned", "void", "size_t", "wchar_t"
|
|
};
|
|
|
|
public static readonly LanguageProfile Plain = new LanguageProfile(
|
|
"평문", new[] { ".txt", ".log", ".csv", ".text" }, isCode: false);
|
|
|
|
public static readonly LanguageProfile CSharp = new LanguageProfile(
|
|
"C#", new[] { ".cs", ".csx" },
|
|
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '"', '\'' },
|
|
hasBraces: true, useCSharpTokenizer: true);
|
|
|
|
public static readonly LanguageProfile CFamily = new LanguageProfile(
|
|
"C / C++", new[] { ".c", ".h", ".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx", ".inl" },
|
|
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[]
|
|
{
|
|
"class", "struct", "enum", "union", "namespace", "template", "typename",
|
|
"public", "private", "protected", "virtual", "override", "static", "extern",
|
|
"inline", "operator", "new", "delete", "this", "nullptr", "true", "false",
|
|
"using", "typedef", "sizeof", "friend", "explicit", "mutable", "constexpr"
|
|
},
|
|
controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true);
|
|
|
|
public static readonly LanguageProfile Java = new LanguageProfile(
|
|
"Java", new[] { ".java" },
|
|
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[]
|
|
{
|
|
"abstract", "class", "extends", "final", "implements", "import", "instanceof",
|
|
"interface", "native", "new", "null", "package", "private", "protected",
|
|
"public", "static", "super", "synchronized", "this", "throws", "transient",
|
|
"true", "false", "volatile", "enum", "record", "var"
|
|
},
|
|
controlKeywords: CFamilyControl, typeKeywords: CFamilyTypes, hasBraces: true);
|
|
|
|
public static readonly LanguageProfile JavaScript = new LanguageProfile(
|
|
"JavaScript / TypeScript", new[] { ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs" },
|
|
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '"', '\'', '`' },
|
|
keywords: new[]
|
|
{
|
|
"class", "const", "let", "var", "function", "new", "this", "super", "extends",
|
|
"import", "export", "from", "as", "default", "async", "await", "null",
|
|
"undefined", "true", "false", "typeof", "instanceof", "in", "of", "delete",
|
|
"interface", "type", "enum", "implements", "public", "private", "readonly"
|
|
},
|
|
controlKeywords: CFamilyControl, typeKeywords: new[] { "number", "string", "boolean", "any", "void", "never", "unknown" },
|
|
hasBraces: true);
|
|
|
|
public static readonly LanguageProfile Python = new LanguageProfile(
|
|
"Python", new[] { ".py", ".pyw", ".pyi" },
|
|
lineComments: new[] { "#" },
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[]
|
|
{
|
|
"and", "as", "assert", "async", "await", "class", "def", "del", "from",
|
|
"global", "import", "in", "is", "lambda", "None", "nonlocal", "not", "or",
|
|
"pass", "True", "False", "with", "yield", "self"
|
|
},
|
|
controlKeywords: new[] { "break", "continue", "elif", "else", "except", "finally", "for", "if", "raise", "return", "try", "while" },
|
|
typeKeywords: new[] { "bool", "bytes", "dict", "float", "int", "list", "set", "str", "tuple" });
|
|
|
|
public static readonly LanguageProfile Sql = new LanguageProfile(
|
|
"SQL", new[] { ".sql" },
|
|
lineComments: new[] { "--" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '\'', '"' },
|
|
keywords: new[]
|
|
{
|
|
"select", "from", "where", "insert", "into", "values", "update", "set",
|
|
"delete", "create", "alter", "drop", "table", "view", "index", "join",
|
|
"inner", "left", "right", "outer", "on", "group", "order", "by", "having",
|
|
"union", "distinct", "as", "and", "or", "not", "null", "primary", "key",
|
|
"foreign", "references", "default", "constraint"
|
|
},
|
|
controlKeywords: new[] { "begin", "end", "if", "else", "while", "case", "when", "then", "return" },
|
|
typeKeywords: new[] { "int", "bigint", "smallint", "varchar", "nvarchar", "char", "nchar", "text", "date", "datetime", "decimal", "float", "bit" },
|
|
caseSensitive: false);
|
|
|
|
public static readonly LanguageProfile Shell = new LanguageProfile(
|
|
"셸 / 배치", new[] { ".sh", ".bash", ".zsh", ".bat", ".cmd", ".ps1", ".psm1" },
|
|
lineComments: new[] { "#", "::", "rem " },
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[] { "echo", "set", "export", "function", "local", "source", "param", "return" },
|
|
controlKeywords: new[] { "if", "then", "else", "elif", "fi", "for", "while", "do", "done", "case", "esac", "goto", "exit" },
|
|
caseSensitive: false);
|
|
|
|
public static readonly LanguageProfile Markup = new LanguageProfile(
|
|
"XML / HTML", new[] { ".xml", ".xaml", ".html", ".htm", ".svg", ".xsd", ".xsl", ".csproj", ".vbproj", ".config", ".props", ".targets", ".resx" },
|
|
blockStart: "<!--", blockEnd: "-->",
|
|
quotes: new[] { '"', '\'' },
|
|
caseSensitive: false);
|
|
|
|
public static readonly LanguageProfile Json = new LanguageProfile(
|
|
"JSON", new[] { ".json", ".jsonc" },
|
|
lineComments: new[] { "//" }, blockStart: "/*", blockEnd: "*/",
|
|
quotes: new[] { '"' },
|
|
keywords: new[] { "true", "false", "null" });
|
|
|
|
public static readonly LanguageProfile Yaml = new LanguageProfile(
|
|
"YAML", new[] { ".yml", ".yaml" },
|
|
lineComments: new[] { "#" },
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[] { "true", "false", "null", "yes", "no" },
|
|
caseSensitive: false);
|
|
|
|
public static readonly LanguageProfile Ini = new LanguageProfile(
|
|
"설정 / INI", new[] { ".ini", ".cfg", ".conf", ".properties", ".toml", ".editorconfig", ".gitconfig" },
|
|
lineComments: new[] { ";", "#" },
|
|
quotes: new[] { '"', '\'' },
|
|
keywords: new[] { "true", "false" },
|
|
caseSensitive: false);
|
|
|
|
public static readonly LanguageProfile Markdown = new LanguageProfile(
|
|
"Markdown", new[] { ".md", ".markdown" }, isCode: false);
|
|
|
|
/// <summary>화면에 고를 수 있게 늘어놓는 순서. 평문이 맨 앞이다(가장 넓게 쓰인다).</summary>
|
|
public static readonly IReadOnlyList<LanguageProfile> All = new[]
|
|
{
|
|
Plain, CSharp, CFamily, Java, JavaScript, Python, Sql, Shell, Markup, Json, Yaml, Ini, Markdown
|
|
};
|
|
|
|
/// <summary>경로의 확장자로 언어를 고른다. 모르면 평문.</summary>
|
|
public static LanguageProfile ForPath(string? path)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(path)) return Plain;
|
|
|
|
string extension;
|
|
try { extension = Path.GetExtension(path)!.ToLowerInvariant(); }
|
|
catch (ArgumentException) { return Plain; }
|
|
|
|
if (string.IsNullOrEmpty(extension)) return Plain;
|
|
|
|
foreach (var profile in All)
|
|
{
|
|
foreach (var candidate in profile.Extensions)
|
|
{
|
|
if (string.Equals(candidate, extension, StringComparison.OrdinalIgnoreCase)) return profile;
|
|
}
|
|
}
|
|
|
|
return Plain;
|
|
}
|
|
}
|
|
}
|