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 : Position, Range, 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(const Uri uri, const FormattingOptions options)
82     {
83         import dfmt.formatter : format;
84         import dls.protocol.logger : logger;
85         import dls.util.document : Document;
86         import std.outbuffer : OutBuffer;
87 
88         logger.info("Formatting %s", uri.path);
89 
90         const document = Document.get(uri);
91         auto contents = cast(ubyte[]) document.toString();
92         auto config = getFormatConfig(uri, options);
93         auto buffer = new OutBuffer();
94         format(uri.path, contents, buffer, &config);
95         return diff(uri, buffer.toString());
96     }
97 
98     TextEdit[] rangeFormatting(const Uri uri, const Range range, const FormattingOptions options)
99     {
100         import dls.util.document : Document;
101         import std.algorithm : filter;
102         import std.array : array;
103 
104         const document = Document.get(uri);
105         document.validatePosition(range.start);
106         document.validatePosition(range.end);
107 
108         return formatting(uri, options).filter!((edit) {
109             return document.byteAtPosition(edit.range.start) < document.byteAtPosition(range.end)
110                 && document.byteAtPosition(edit.range.end) > document.byteAtPosition(range.start);
111         }).array;
112     }
113 
114     TextEdit[] onTypeFormatting(const Uri uri, const Position position,
115             const FormattingOptions options)
116     {
117         import dls.util.document : Document;
118         import std.algorithm : filter;
119         import std.array : array;
120         import std..string : stripRight;
121 
122         const document = Document.get(uri);
123         document.validatePosition(position);
124 
125         if (position.character != stripRight(document.lines[position.line]).length)
126         {
127             return [];
128         }
129 
130         return formatting(uri, options).filter!(edit => edit.range.start.line == position.line
131                 || edit.range.end.line == position.line).array;
132     }
133 
134     private Config getFormatConfig(const Uri uri, const FormattingOptions options)
135     {
136         import dfmt.editorconfig : IndentStyle, OptionalBoolean, getConfigFor;
137         import dls.tools.symbol_tool : SymbolTool;
138 
139         static OptionalBoolean toOptBool(bool b)
140         {
141             return b ? OptionalBoolean.t : OptionalBoolean.f;
142         }
143 
144         auto conf = getConfig(SymbolTool.instance.getWorkspace(uri));
145         Config config;
146         config.initializeWithDefaults();
147         config.pattern = configPattern;
148         config.end_of_line = eolMap[conf.format.endOfLine];
149         config.indent_style = options.insertSpaces ? IndentStyle.space : IndentStyle.tab;
150         config.indent_size = cast(typeof(config.indent_size)) options.tabSize;
151         config.tab_width = config.indent_size;
152         config.max_line_length = conf.format.maxLineLength;
153         config.dfmt_align_switch_statements = toOptBool(conf.format.dfmtAlignSwitchStatements);
154         config.dfmt_brace_style = braceStyleMap[conf.format.dfmtBraceStyle];
155         config.dfmt_outdent_attributes = toOptBool(conf.format.dfmtOutdentAttributes);
156         config.dfmt_soft_max_line_length = conf.format.dfmtSoftMaxLineLength;
157         config.dfmt_space_after_cast = toOptBool(conf.format.dfmtSpaceAfterCast);
158         config.dfmt_space_after_keywords = toOptBool(conf.format.dfmtSpaceAfterKeywords);
159         config.dfmt_space_before_function_parameters = toOptBool(
160                 conf.format.dfmtSpaceBeforeFunctionParameters);
161         config.dfmt_split_operator_at_line_end = toOptBool(conf.format.dfmtSplitOperatorAtLineEnd);
162         config.dfmt_selective_import_space = toOptBool(conf.format.dfmtSelectiveImportSpace);
163         config.dfmt_compact_labeled_statements = conf.format.dfmtCompactLabeledStatements
164             ? OptionalBoolean.t : OptionalBoolean.f;
165         config.dfmt_template_constraint_style
166             = templateConstraintStyleMap[conf.format.dfmtTemplateConstraintStyle];
167         config.dfmt_single_template_constraint_indent = toOptBool(
168                 conf.format.dfmtSingleTemplateConstraintIndent);
169 
170         auto fileConfig = getConfigFor!Config(uri.path);
171         fileConfig.pattern = configPattern;
172         config.merge(fileConfig, configPattern);
173         return config;
174     }
175 
176     private TextEdit[] diff(const Uri uri, const string after)
177     {
178         import dls.util.document : Document;
179         import std.ascii : isWhite;
180         import std.utf : decode;
181 
182         const document = Document.get(uri);
183         immutable before = document.toString();
184         size_t i;
185         size_t j;
186         TextEdit[] result;
187 
188         size_t startIndex;
189         size_t stopIndex;
190         string text;
191 
192         bool pushTextEdit()
193         {
194             if (startIndex != stopIndex || text.length > 0)
195             {
196                 result ~= new TextEdit(new Range(document.positionAtByte(startIndex),
197                         document.positionAtByte(stopIndex)), text);
198                 return true;
199             }
200 
201             return false;
202         }
203 
204         while (i < before.length || j < after.length)
205         {
206             auto newI = i;
207             auto newJ = j;
208             dchar beforeChar;
209             dchar afterChar;
210 
211             if (newI < before.length)
212             {
213                 beforeChar = decode(before, newI);
214             }
215 
216             if (newJ < after.length)
217             {
218                 afterChar = decode(after, newJ);
219             }
220 
221             if (i < before.length && j < after.length && beforeChar == afterChar)
222             {
223                 i = newI;
224                 j = newJ;
225 
226                 if (pushTextEdit())
227                 {
228                     startIndex = stopIndex;
229                     text = "";
230                 }
231             }
232 
233             if (startIndex == stopIndex)
234             {
235                 startIndex = i;
236                 stopIndex = i;
237             }
238 
239             auto addition = !isWhite(beforeChar) && isWhite(afterChar);
240             immutable deletion = isWhite(beforeChar) && !isWhite(afterChar);
241 
242             if (!addition && !deletion)
243             {
244                 addition = before.length - i < after.length - j;
245             }
246 
247             if (addition && j < after.length)
248             {
249                 text ~= after[j .. newJ];
250                 j = newJ;
251             }
252             else if (i < before.length)
253             {
254                 stopIndex = newI;
255                 i = newI;
256             }
257         }
258 
259         pushTextEdit();
260         return result;
261     }
262 }