1 /*
2  *Copyright (C) 2018 Laurent Tréguier
3  *
4  *This file is part of DLS.
5  *
6  *DLS is free software: you can redistribute it and/or modify
7  *it under the terms of the GNU General Public License as published by
8  *the Free Software Foundation, either version 3 of the License, or
9  *(at your option) any later version.
10  *
11  *DLS is distributed in the hope that it will be useful,
12  *but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *GNU General Public License for more details.
15  *
16  *You should have received a copy of the GNU General Public License
17  *along with DLS.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 module dls.tools.format_tool;
22 
23 import dfmt.config : BraceStyle, TemplateConstraintStyle;
24 import dfmt.editorconfig : EOL;
25 import dls.tools.configuration : Configuration;
26 import dls.tools.tool : Tool;
27 
28 private immutable EOL[Configuration.FormatConfiguration.EndOfLine] eolMap;
29 private immutable BraceStyle[Configuration.FormatConfiguration.BraceStyle] braceStyleMap;
30 private immutable TemplateConstraintStyle[Configuration.FormatConfiguration.TemplateConstraintStyle] templateConstraintStyleMap;
31 private immutable configPattern = "dummy.d";
32 
33 shared static this()
34 {
35     //dfmt off
36     eolMap = [
37         Configuration.FormatConfiguration.EndOfLine.lf   : EOL.lf,
38         Configuration.FormatConfiguration.EndOfLine.cr   : EOL.cr,
39         Configuration.FormatConfiguration.EndOfLine.crlf : EOL.crlf
40     ];
41 
42     braceStyleMap = [
43         Configuration.FormatConfiguration.BraceStyle.allman     : BraceStyle.allman,
44         Configuration.FormatConfiguration.BraceStyle.otbs       : BraceStyle.otbs,
45         Configuration.FormatConfiguration.BraceStyle.stroustrup : BraceStyle.stroustrup
46     ];
47 
48     templateConstraintStyleMap = [
49         Configuration.FormatConfiguration.TemplateConstraintStyle.conditionalNewlineIndent : TemplateConstraintStyle.conditional_newline_indent,
50         Configuration.FormatConfiguration.TemplateConstraintStyle.conditionalNewline       : TemplateConstraintStyle.conditional_newline,
51         Configuration.FormatConfiguration.TemplateConstraintStyle.alwaysNewline            : TemplateConstraintStyle.always_newline,
52         Configuration.FormatConfiguration.TemplateConstraintStyle.alwaysNewlineIndent      : TemplateConstraintStyle.always_newline_indent
53     ];
54     //dfmt on
55 }
56 
57 class FormatTool : Tool
58 {
59     import dfmt.config : Config;
60     import dls.protocol.definitions : TextEdit;
61     import dls.protocol.interfaces : FormattingOptions;
62     import dls.util.uri : Uri;
63 
64     private static FormatTool _instance;
65 
66     static void initialize()
67     {
68         _instance = new FormatTool();
69     }
70 
71     static void shutdown()
72     {
73         destroy(_instance);
74     }
75 
76     @property static FormatTool instance()
77     {
78         return _instance;
79     }
80 
81     TextEdit[] formatting(in Uri uri, in FormattingOptions options)
82     {
83         import dfmt.formatter : format;
84         import dls.protocol.definitions : Position, Range;
85         import dls.util.document : Document;
86         import dls.util.logger : logger;
87         import std.outbuffer : OutBuffer;
88 
89         logger.infof("Formatting %s", uri.path);
90 
91         const document = Document.get(uri);
92         auto contents = cast(ubyte[]) document.toString();
93         auto config = getConfig(uri, options);
94         auto buffer = new OutBuffer();
95         format(uri.path, contents, buffer, &config);
96         return diff(uri, buffer.toString());
97     }
98 
99     private Config getConfig(in Uri uri, in FormattingOptions options)
100     {
101         import dfmt.editorconfig : IndentStyle, OptionalBoolean, getConfigFor;
102         import dls.tools.symbol_tool : SymbolTool;
103 
104         static OptionalBoolean toOptBool(bool b)
105         {
106             return b ? OptionalBoolean.t : OptionalBoolean.f;
107         }
108 
109         Config config;
110         config.pattern = configPattern;
111         config.initializeWithDefaults();
112         config.end_of_line = eolMap[configuration.format.endOfLine];
113         config.indent_style = options.insertSpaces ? IndentStyle.space : IndentStyle.tab;
114         config.indent_size = cast(typeof(config.indent_size)) options.tabSize;
115         config.tab_width = config.indent_size;
116         config.max_line_length = configuration.format.maxLineLength;
117         config.dfmt_align_switch_statements = toOptBool(
118                 configuration.format.dfmtAlignSwitchStatements);
119         config.dfmt_brace_style = braceStyleMap[configuration.format.dfmtBraceStyle];
120         config.dfmt_outdent_attributes = toOptBool(configuration.format.dfmtOutdentAttributes);
121         config.dfmt_soft_max_line_length = configuration.format.dfmtSoftMaxLineLength;
122         config.dfmt_space_after_cast = toOptBool(configuration.format.dfmtSpaceAfterCast);
123         config.dfmt_space_after_keywords = toOptBool(configuration.format.dfmtSpaceAfterKeywords);
124         config.dfmt_space_before_function_parameters = toOptBool(
125                 configuration.format.dfmtSpaceBeforeFunctionParameters);
126         config.dfmt_split_operator_at_line_end = toOptBool(
127                 configuration.format.dfmtSplitOperatorAtLineEnd);
128         config.dfmt_selective_import_space = toOptBool(
129                 configuration.format.dfmtSelectiveImportSpace);
130         config.dfmt_compact_labeled_statements = configuration.format.dfmtCompactLabeledStatements
131             ? OptionalBoolean.t : OptionalBoolean.f;
132         config.dfmt_template_constraint_style
133             = templateConstraintStyleMap[configuration.format.dfmtTemplateConstraintStyle];
134         config.dfmt_single_template_constraint_indent = toOptBool(
135                 configuration.format.dfmtSingleTemplateConstraintIndent);
136 
137         auto fileConfig = getConfigFor!Config(SymbolTool.instance.getWorkspace(uri).path);
138         fileConfig.pattern = configPattern;
139         config.merge(fileConfig, configPattern);
140         return config;
141     }
142 
143     private TextEdit[] diff(in Uri uri, in string after)
144     {
145         import dls.protocol.definitions : Range;
146         import dls.util.document : Document;
147         import std.ascii : isWhite;
148         import std.utf : decode;
149 
150         const document = Document.get(uri);
151         const before = document.toString();
152         size_t i;
153         size_t j;
154         TextEdit[] result;
155 
156         size_t startIndex;
157         size_t stopIndex;
158         string text;
159 
160         bool pushTextEdit()
161         {
162             if (startIndex != stopIndex || text.length > 0)
163             {
164                 result ~= new TextEdit(new Range(document.positionAtByte(startIndex),
165                         document.positionAtByte(stopIndex)), text);
166                 return true;
167             }
168 
169             return false;
170         }
171 
172         while (i < before.length || j < after.length)
173         {
174             auto newI = i;
175             auto newJ = j;
176             dchar beforeChar;
177             dchar afterChar;
178 
179             if (newI < before.length)
180             {
181                 beforeChar = decode(before, newI);
182             }
183 
184             if (newJ < after.length)
185             {
186                 afterChar = decode(after, newJ);
187             }
188 
189             if (i < before.length && j < after.length && beforeChar == afterChar)
190             {
191                 i = newI;
192                 j = newJ;
193 
194                 if (pushTextEdit())
195                 {
196                     startIndex = stopIndex;
197                     text = "";
198                 }
199             }
200             else
201             {
202                 if (startIndex == stopIndex)
203                 {
204                     startIndex = i;
205                     stopIndex = i;
206                 }
207 
208                 if (j < after.length && !isWhite(beforeChar)
209                         && (isWhite(afterChar) || before.length - i < after.length - j))
210                 {
211                     text ~= after[j .. newJ];
212                     j = newJ;
213                 }
214                 else if (i < before.length)
215                 {
216                     stopIndex = newI;
217                     i = newI;
218                 }
219             }
220         }
221 
222         pushTextEdit();
223         return result;
224     }
225 }