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 }