Files
hrynco-kswitcher/src/files_text_language.cpp
T

271 lines
17 KiB
C++

#include "files_text_language.h"
#include "core.h"
#include <Sci_Position.h>
#include <ILexer.h>
#include <Lexilla.h>
#include <Scintilla.h>
#include <SciLexer.h>
#include <algorithm>
#include <cwctype>
namespace ks::files
{
namespace
{
constexpr char cpp[] = "alignas alignof asm auto bool break case catch char char8_t char16_t char32_t class concept const "
"consteval constexpr constinit const_cast continue co_await co_return co_yield decltype default delete do double "
"dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace "
"new noexcept nullptr operator private protected public register reinterpret_cast requires return short signed "
"sizeof static static_assert static_cast struct switch template this thread_local throw true try typedef typeid "
"typename union unsigned using virtual void volatile wchar_t while import module";
constexpr char csharp[] = "abstract as async await base bool break byte case catch char checked class const continue "
"decimal default delegate do double else enum event explicit extern false file finally fixed float for foreach from "
"get global goto if implicit in init int interface internal is let lock long namespace new not null object operator "
"or out override partial private protected public readonly record ref required return sbyte sealed select set short "
"sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unmanaged unsafe "
"ushort using value var virtual void volatile when where while with yield";
constexpr char javascript[] = "abstract any as async await boolean break case catch class const continue debugger declare "
"default delete do else enum export extends false finally for from function get if implements import in infer "
"instanceof interface keyof let module namespace never new null number object of override private protected public "
"readonly return satisfies set static string super switch symbol this throw true try type typeof undefined unknown "
"using var void while with yield";
constexpr char java[] = "abstract assert boolean break byte case catch char class const continue default do double else "
"enum exports extends final finally float for if implements import instanceof int interface long module native new "
"non-sealed null open opens package permits private protected public record requires return sealed short static super "
"switch synchronized this throw throws transient true try var void volatile while yield false";
constexpr char sql[] = "add alter and any as asc begin between by case check column commit constraint create cross "
"database declare default delete desc distinct drop else end except exists fetch foreign from full function grant "
"group having in index inner insert intersect into is join left like limit merge not null offset on or order outer "
"over partition primary procedure references returning right rollback row select set table then top transaction "
"truncate union unique update using values view when where window with";
constexpr char python[] = "and as assert async await break class continue def del elif else except False finally for from "
"global if import in is lambda match case None nonlocal not or pass raise return True try while with yield";
constexpr char ps[] = "begin break catch class continue data define do dynamicparam else elseif end enum exit filter finally "
"for foreach from function if in param process return switch throw trap try until using var while workflow";
constexpr char shell[] = "alias bg break case cd continue declare do done echo elif else esac eval exec exit export "
"false fi for function if in local read readonly return select set shift source test then time trap true type "
"typeset ulimit umask unalias unset until wait while";
constexpr char rust[] = "as async await break const continue crate dyn else enum extern false fn for if impl in let loop "
"match mod move mut pub ref return self Self static struct super trait true type unsafe use where while";
constexpr char go[] = "break case chan const continue default defer else fallthrough for func go goto if import interface "
"map package range return select struct switch type var nil false true";
constexpr char lua[] = "and break do else elseif end false for function goto if in local nil not or repeat return then true until while";
constexpr char ruby[] = "BEGIN END alias and begin break case class def defined do else elsif end ensure false for if in "
"module next nil not or redo rescue retry return self super then true undef unless until when while yield";
constexpr char html[] = "a abbr address area article aside audio b base bdi bdo blockquote body br button canvas caption "
"cite code col colgroup data datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer "
"form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd label legend li link main map mark "
"menu meta meter nav noscript object ol optgroup option output p picture pre progress q rp rt ruby s samp script "
"search section select slot small source span strong style sub summary sup table tbody td template textarea tfoot "
"th thead time title tr track u ul var video wbr accept action alt aria-label async cellpadding charset checked cite "
"class content controls defer disabled for height href http-equiv id lang maxlength method multiple name placeholder "
"readonly rel required role rows scope selected src srcset style tabindex target type value width";
struct Mapping
{
std::wstring_view names;
const char *lexer;
const char *keywords = "";
};
constexpr Mapping languages[] = {
{L".c .h .cc .cpp .cxx .hh .hpp .hxx .inl .ipp .ixx .cppm .cu .cuh .m .mm .rc .idl .ino", "cpp", cpp},
{L".cs .csx .razor.cs", "cpp", csharp},
{L".js .jsx .mjs .cjs .ts .tsx .mts .cts .es", "cpp", javascript},
{L".java .pde", "cpp", java},
{L".kt .kts", "cpp", "abstract actual as break by catch class companion const constructor continue data do else enum "
"expect false finally for fun if import in init interface internal is lateinit null object open operator out "
"override package private protected public return sealed super suspend this throw true try typealias val var when while"},
{L".glsl .vert .frag .geom .hlsl .fx .metal", "cpp", cpp},
{L".html .htm .xhtml .shtml .vue .svelte .astro .cshtml .razor .aspx .jsp .php .phtml", "hypertext", html},
{L".xml .xsl .xslt .xsd .svg .resx .xaml .csproj .fsproj .vbproj .vcxproj .props .targets .config .nuspec "
L".wixproj .wxs .plist .rss .atom .gpx .kml .ui .iml .wsdl .manifest .slnx .axaml", "xml"},
{L".css .scss .sass .less", "css", "color background display position width height margin padding border font "
"grid flex content gap box-shadow transform animation opacity overflow text-align align-items justify-content"},
{L".sql .psql .ddl .dml .mysql", "sql", sql},
{L".py .pyw .pyi .rpy sconstruct sconscript", "python", python},
{L".json .jsonc .json5 .ipynb .webmanifest .geojson .topojson .code-workspace .sublime-settings .sublime-project",
"json", "false null true Infinity NaN"},
{L".yaml .yml .eyaml .clang-format .clang-tidy", "yaml", "true false null yes no on off"},
{L".toml pipfile uv.lock cargo.lock poetry.lock", "toml", "true false"},
{L".md .markdown .mdown .mkd .mdx readme", "markdown"},
{L".adoc .asciidoc", "asciidoc"},
{L".ini .cfg .conf .properties .url .inf .reg .editorconfig .gitconfig .gitattributes .gitmodules .npmrc .env", "props"},
{L".sh .bash .zsh .ksh .bats .bashrc .bash_profile .zshrc .profile configure gradlew mvnw", "bash", shell},
{L".ps1 .psm1 .psd1", "powershell", ps},
{L".bat .cmd", "batch", "call cd chdir cls copy del dir echo endlocal exit for goto if md mkdir move path pause popd "
"pushd rd rem ren rename set setlocal shift start title type"},
{L".cmake cmakelists.txt", "cmake", "add_executable add_library add_subdirectory cmake_minimum_required "
"configure_file else elseif endforeach endfunction endif endwhile file find_package foreach function if include "
"install list message option project set string target_compile_definitions target_include_directories "
"target_link_libraries unset while"},
{L".mk .mak makefile gnumakefile", "makefile"},
{L".rs", "rust", rust}, {L".go", "cpp", go}, {L".lua", "lua", lua}, {L".rb .rake .gemspec gemfile rakefile", "ruby", ruby},
{L".dart", "dart", "abstract as assert async await break case class const continue default do else enum extends "
"false final finally for if implements import in interface late mixin new null on override return sealed static "
"super switch this throw true try typedef var void while with yield"},
{L".swift", "cpp", "actor as associatedtype await break case catch class continue default defer deinit do else "
"enum extension false for func guard if import in init let nil private protocol public return self some "
"static struct super switch throw throws true try typealias var where while"},
{L".zig", "zig"}, {L".d .di", "d"}, {L".nim .nims .nimble", "nim"},
{L".fs .fsi .fsx", "fsharp"}, {L".ml .mli", "caml"}, {L".sml .sig", "SML"},
{L".vb .bas .frm .cls", "vb", "as byref byval class const dim each else elseif end enum for function if implements "
"imports inherits interface module new next nothing private property public return select shared sub then try while"},
{L".vbs", "vbscript"}, {L".pl .pm .t", "perl"}, {L".r .rprofile", "r"},
{L".jl", "julia"}, {L".matlab", "matlab"}, {L".oct", "octave"},
{L".tex .sty .ltx", "latex"}, {L".bib", "bib"}, {L".hs", "haskell"}, {L".lhs", "literatehaskell"},
{L".lisp .lsp .cl .el .scm .ss .rkt", "lisp"},
{L".s .asm .nasm .inc", "asm"}, {L".a68k", "a68k"},
{L".v .vh .sv .svh", "verilog"}, {L".vhd .vhdl", "vhdl"},
{L".f .for .f77", "f77"}, {L".f90 .f95 .f03 .f08", "fortran"},
{L".ada .adb .ads", "ada"}, {L".pas .pp .dpr", "pascal"}, {L".cob .cbl .cpy", "COBOL"},
{L".tcl .tk", "tcl"}, {L".coffee .litcoffee", "coffeescript"}, {L".au3", "au3"},
{L".st", "smalltalk"}, {L".erl .hrl", "erlang"},
{L".purs", "haskell"}, {L".diff .patch", "diff"}, {L".po .pot", "po"},
{L".iss", "inno"}, {L".nsi .nsh", "nsis"}, {L".idl3 .odl", "cpp", cpp},
{L".dmap", "DMAP"}, {L".dns .zone", "props"}, {L".ihex .hex", "ihex"}, {L".srec .s19 .s28 .s37", "srec"},
{L".edifact .edi", "edifact"}, {L".x12", "x12"}, {L".g4", "cpp", java},
{L".nix", "nix"}, {L".gd", "gdscript", python}, {L".il", "cil"}, {L".as", "cpp", javascript},
{L".frt .fth", "forth"}, {L".e .eif", "eiffel"}, {L".pov .povray", "pov"},
{L".rebol .reb", "rebol"}, {L".asn .asn1", "asn1"}, {L".asy", "asy"},
{L".raku .rakumod .rakutest .p6 .pm6", "raku"}, {L".bb", "blitzbasic"}, {L".bi", "freebasic"},
{L".t2t", "txt2tags"}, {L".troff .roff .man", "troff"},
{L".clw", "clarion"}, {L".spice .cir", "spice"},
};
bool containsWord(std::wstring_view words, std::wstring_view key)
{
if (key.empty()) return false;
for (size_t start = 0; start < words.size();)
{
const auto end = words.find(L' ', start);
if (words.substr(start, end == words.npos ? end : end - start) == key) return true;
if (end == words.npos) break;
start = end + 1;
}
return false;
}
}
TextLanguage detectLanguage(const std::filesystem::path &path, std::string_view firstPage)
{
auto filename = path.filename().wstring();
auto extension = path.extension().wstring();
for (auto *value : {&filename, &extension})
std::transform(value->begin(), value->end(), value->begin(), [](wchar_t ch) { return std::towlower(ch); });
for (const auto &language : languages)
if (containsWord(language.names, filename) || containsWord(language.names, extension))
return {language.lexer, language.keywords};
if (filename.starts_with(L".env.") || extension == L".env") return {"props"};
if (firstPage.starts_with("<?xml")) return {"xml"};
if (firstPage.starts_with("#!"))
{
const auto line = firstPage.substr(0, firstPage.find_first_of("\r\n"));
for (const auto &[needle, language] : {std::pair{"python", TextLanguage{"python", python}},
{"ruby", {"ruby", ruby}}, {"node", {"cpp", javascript}}, {"perl", {"perl"}},
{"pwsh", {"powershell", ps}}, {"lua", {"lua", lua}}, {"sh", {"bash", shell}}})
if (line.find(needle) != line.npos) return language;
}
return {};
}
std::wstring textLanguageLabel(const TextLanguage &language)
{
if (language.keywords == csharp) return L"C#";
if (language.keywords == javascript) return L"JavaScript / TypeScript";
if (language.keywords == java) return L"Java";
if (language.keywords == go) return L"Go";
if (language.lexer == "cpp") return L"C-family";
if (language.lexer == "hypertext") return L"HTML";
return language.lexer.empty() ? L"Plain text" : wide(language.lexer);
}
Scintilla::ILexer5 *createTextLexer(const TextLanguage &language)
{
if (language.lexer.empty()) return nullptr;
auto *lexer = CreateLexer(language.lexer.c_str());
if (!lexer) return nullptr;
const auto *keywords = language.keywords;
if (!*keywords)
for (const auto &mapping : languages)
if (language.lexer == mapping.lexer && *mapping.keywords)
{
keywords = mapping.keywords;
break;
}
if (*keywords) lexer->WordListSet(0, keywords);
if (language.lexer == "hypertext") lexer->WordListSet(1, javascript);
if (language.lexer == "json") lexer->PropertySet("lexer.json.allow.comments", "1");
if (language.lexer == "cpp" && (language.keywords == javascript || language.keywords == go))
{
lexer->PropertySet("lexer.cpp.backquoted.strings", language.keywords == javascript ? "2" : "1");
if (language.keywords == javascript) lexer->PropertySet("lexer.cpp.regex", "1");
}
if (language.lexer == "cpp" && language.keywords == csharp)
lexer->PropertySet("lexer.cpp.triplequoted.strings", "1");
return lexer;
}
TextColor syntaxColor(Scintilla::ILexer5 &lexer, std::string_view name, int style)
{
if (!style || (style >= STYLE_DEFAULT && style <= STYLE_LASTPREDEFINED)) return TextColor::text;
const auto *styleTags = lexer.TagsOfStyle(style);
const auto *styleName = lexer.NameOfStyle(style);
const std::string_view tags = styleTags ? styleTags : "";
const std::string_view token = styleName ? styleName : "";
const auto has = [&](std::string_view word) {
return tags.find(word) != tags.npos || token.find(word) != token.npos;
};
if (has("comment")) return TextColor::comment;
if (has("string") || has("character") || has("quoted") || has("regex")) return TextColor::string;
if (has("number") || has("numeric")) return TextColor::number;
if (has("keyword") || has("command") || has("tag") || has("heading") || has("header") ||
has("preprocessor") || has("keywordclass")) return TextColor::keyword;
if (has("operator") || has("attribute") || has("property") || has("variable") || has("label"))
return TextColor::symbol;
if (name == "markdown")
{
if (style >= SCE_MARKDOWN_HEADER1 && style <= SCE_MARKDOWN_HEADER6) return TextColor::keyword;
if (style >= SCE_MARKDOWN_CODE && style <= SCE_MARKDOWN_CODEBK) return TextColor::string;
return TextColor::symbol;
}
if (name == "diff")
{
if (style == SCE_DIFF_ADDED) return TextColor::string;
if (style == SCE_DIFF_DELETED) return TextColor::number;
if (style == SCE_DIFF_COMMENT) return TextColor::comment;
return TextColor::keyword;
}
if (name == "sql")
{
if (style >= SCE_SQL_COMMENT && style <= SCE_SQL_COMMENTDOC) return TextColor::comment;
if (style == SCE_SQL_NUMBER) return TextColor::number;
if (style == SCE_SQL_WORD || style == SCE_SQL_WORD2) return TextColor::keyword;
if (style == SCE_SQL_STRING || style == SCE_SQL_CHARACTER) return TextColor::string;
}
if (name == "json")
{
if (style == SCE_JSON_LINECOMMENT || style == SCE_JSON_BLOCKCOMMENT) return TextColor::comment;
if (style == SCE_JSON_STRING || style == SCE_JSON_STRINGEOL) return TextColor::string;
if (style == SCE_JSON_NUMBER) return TextColor::number;
if (style == SCE_JSON_KEYWORD || style == SCE_JSON_LDKEYWORD) return TextColor::keyword;
}
if (name == "css")
{
if (style == SCE_CSS_COMMENT) return TextColor::comment;
if (style == SCE_CSS_DOUBLESTRING || style == SCE_CSS_SINGLESTRING || style == SCE_CSS_VALUE)
return TextColor::string;
if (style == SCE_CSS_TAG || style == SCE_CSS_DIRECTIVE) return TextColor::keyword;
}
if (name == "yaml" || name == "toml")
{
// The two lexers assign the same IDs to these categories.
if (style == SCE_YAML_COMMENT) return TextColor::comment;
if (style == SCE_YAML_KEYWORD) return TextColor::keyword;
if (style == SCE_YAML_NUMBER) return TextColor::number;
}
if (token.empty()) return TextColor::symbol;
return TextColor::text;
}
}