1 module dfmt.editorconfig; 2 import std.regex : ctRegex; 3 4 static if (__VERSION__ >= 2067) 5 public import std.traits : FieldNameTuple; 6 else 7 { 8 private enum NameOf(alias T) = T.stringof; 9 template FieldNameTuple(T) 10 { 11 import std.typetuple : staticMap; 12 import std.traits : isNested; 13 14 static if (is(T == struct) || is(T == union)) 15 alias FieldNameTuple = staticMap!(NameOf, T.tupleof[0 .. $ - isNested!T]); 16 else static if (is(T == class)) 17 alias FieldNameTuple = staticMap!(NameOf, T.tupleof); 18 else 19 alias FieldNameTuple = TypeTuple!""; 20 } 21 } 22 23 private auto headerRe = ctRegex!(`^\s*\[([^\n]+)\]\s*(:?#.*)?$`); 24 private auto propertyRe = ctRegex!(`^\s*([\w_]+)\s*=\s*([\w_]+)\s*[#;]?.*$`); 25 private auto commentRe = ctRegex!(`^\s*[#;].*$`); 26 27 enum OptionalBoolean : ubyte 28 { 29 unspecified = 3, 30 t = 1, 31 f = 0 32 } 33 34 enum IndentStyle : ubyte 35 { 36 unspecified, 37 tab, 38 space 39 } 40 41 enum EOL : ubyte 42 { 43 unspecified, 44 lf, 45 cr, 46 crlf 47 } 48 49 mixin template StandardEditorConfigFields() 50 { 51 string pattern; 52 OptionalBoolean root; 53 EOL end_of_line; 54 OptionalBoolean insert_final_newline; 55 string charset; 56 IndentStyle indent_style; 57 int indent_size = -1; 58 int tab_width = -1; 59 OptionalBoolean trim_trailing_whitespace; 60 int max_line_length = -1; 61 62 void merge(ref const typeof(this) other, const string fileName) 63 { 64 import dfmt.globmatch_editorconfig : globMatchEditorConfig; 65 import std.array : front, popFront, empty, save; 66 67 if (other.pattern is null || !ecMatch(fileName, other.pattern)) 68 return; 69 foreach (N; FieldNameTuple!(typeof(this))) 70 { 71 alias T = typeof(mixin(N)); 72 const otherN = mixin("other." ~ N); 73 auto thisN = &mixin("this." ~ N); 74 static if (N == "pattern") 75 continue; 76 else static if (is(T == enum)) 77 *thisN = otherN != T.unspecified ? otherN : *thisN; 78 else static if (is(T == int)) 79 *thisN = otherN != -1 ? otherN : *thisN; 80 else static if (is(T == string)) 81 *thisN = otherN !is null ? otherN : *thisN; 82 else 83 static assert(false); 84 } 85 } 86 87 private bool ecMatch(string fileName, string patt) 88 { 89 import std.algorithm : canFind; 90 import std.path : baseName; 91 import dfmt.globmatch_editorconfig : globMatchEditorConfig; 92 93 if (!pattern.canFind("/")) 94 fileName = fileName.baseName; 95 return fileName.globMatchEditorConfig(patt); 96 } 97 } 98 99 unittest 100 { 101 struct Config 102 { 103 mixin StandardEditorConfigFields; 104 } 105 106 Config config1; 107 Config config2; 108 config2.pattern = "test.d"; 109 config2.end_of_line = EOL.crlf; 110 assert(config1.end_of_line != config2.end_of_line); 111 config1.merge(config2, "a/b/test.d"); 112 assert(config1.end_of_line == config2.end_of_line); 113 } 114 115 /** 116 * Params: 117 * path = the path to the file 118 * Returns: 119 * The configuration for the file at the given path 120 */ 121 EC getConfigFor(EC)(string path) 122 { 123 import std.stdio : File; 124 import std.regex : regex, match; 125 import std.path : globMatch, dirName, baseName, pathSplitter, buildPath, 126 absolutePath; 127 import std.algorithm : reverse, map, filter; 128 import std.array : array; 129 import std.file : isDir; 130 131 EC result; 132 EC[][] configs; 133 immutable expanded = absolutePath(path); 134 immutable bool id = isDir(expanded); 135 immutable string fileName = id ? "dummy.d" : baseName(expanded); 136 string[] pathParts = cast(string[]) pathSplitter(expanded).array(); 137 138 for (size_t i = pathParts.length; i > 1; i--) 139 { 140 EC[] sections = parseConfig!EC(buildPath(pathParts[0 .. i])); 141 if (sections.length) 142 configs ~= sections; 143 if (!sections.map!(a => a.root).filter!(a => a == OptionalBoolean.t).empty) 144 break; 145 } 146 reverse(configs); 147 static if (__VERSION__ >= 2067) 148 { 149 import std.algorithm : each; 150 151 configs.each!(a => a.each!(b => result.merge(b, fileName)))(); 152 } 153 else 154 { 155 foreach (c; configs) 156 foreach (d; c) 157 result.merge(d, fileName); 158 } 159 return result; 160 } 161 162 private EC[] parseConfig(EC)(string dir) 163 { 164 import std.stdio : File; 165 import std.file : exists; 166 import std.path : buildPath; 167 import std.regex : matchAll; 168 import std.conv : to; 169 import std.uni : toLower; 170 171 EC section; 172 EC[] sections; 173 immutable string path = buildPath(dir, ".editorconfig"); 174 if (!exists(path)) 175 return sections; 176 177 File f = File(path); 178 foreach (line; f.byLine()) 179 { 180 auto l = line.idup; 181 auto headerMatch = l.matchAll(headerRe); 182 if (headerMatch) 183 { 184 sections ~= section; 185 section = EC.init; 186 auto c = headerMatch.captures; 187 c.popFront(); 188 section.pattern = c.front(); 189 } 190 else 191 { 192 auto propertyMatch = l.matchAll(propertyRe); 193 if (propertyMatch) 194 { 195 auto c = propertyMatch.captures; 196 c.popFront(); 197 immutable string propertyName = c.front(); 198 c.popFront(); 199 immutable string propertyValue = toLower(c.front()); 200 foreach (F; FieldNameTuple!EC) 201 { 202 enum configDot = "section." ~ F; 203 alias FieldType = typeof(mixin(configDot)); 204 if (F == propertyName) 205 { 206 static if (is(FieldType == OptionalBoolean)) 207 mixin(configDot) = propertyValue == "true" ? OptionalBoolean.t 208 : OptionalBoolean.f; 209 else 210 mixin(configDot) = to!(FieldType)(propertyValue); 211 } 212 } 213 } 214 } 215 } 216 sections ~= section; 217 return sections; 218 }