1 module dlangide.ui.outputpanel;
2 
3 import dlangui;
4 import dlangide.workspace.workspace;
5 import dlangide.workspace.project;
6 import dlangide.ui.frame;
7 import dlangide.ui.terminal;
8 
9 import std.utf;
10 import std.regex;
11 import std.algorithm : startsWith;
12 import std.string;
13 
14 //static if (BACKEND_CONSOLE) {
15 //    enum ENABLE_INTERNAL_TERMINAL = true;
16 //} else {
17     version (Windows) {
18         enum ENABLE_INTERNAL_TERMINAL = false;
19     } else {
20         enum ENABLE_INTERNAL_TERMINAL = true;
21     }
22 //}
23 
24 enum ENABLE_INTERNAL_TERMINAL_TEST = false;
25 
26 /// event listener to navigate by error/warning position
27 interface CompilerLogIssueClickHandler {
28     bool onCompilerLogIssueClick(dstring filename, int line, int column);
29 }
30 
31 class ErrorPosition {
32     dstring filename;
33     int line;
34     int pos;
35     this(dstring fn, int l, int p) {
36         filename = fn;
37         line = l;
38         pos = p;
39     }
40 }
41 
42 /// Log widget with parsing of compiler output
43 class CompilerLogWidget : LogWidget {
44 
45     Signal!CompilerLogIssueClickHandler compilerLogIssueClickHandler;
46 
47     //auto ctr = ctRegex!(r"(.+)\((\d+)\): (Error|Warning|Deprecation): (.+)"d);
48     auto ctr = ctRegex!(r"(.+)\((\d+)(?:,(\d+))?\): (Error|Warning|Deprecation): (.+)"d);
49 
50     /// forward to super c'tor
51     this(string ID) {
52         super(ID);
53         //auto match2 = matchFirst("file.d(123,234): Error: bla bla"d, ctr2);
54         //if (!match2.empty) {
55         //    Log.d("found");
56         //}
57     }
58 
59     protected uint _filenameColor = 0x0000C0;
60     protected uint _errorColor = 0xFF0000;
61     protected uint _warningColor = 0x606000;
62     protected uint _deprecationColor = 0x802040;
63 
64     /// handle theme change: e.g. reload some themed resources
65     override void onThemeChanged() {
66         super.onThemeChanged();
67         _filenameColor = style.customColor("build_log_filename_color", 0x0000C0);
68         _errorColor = style.customColor("build_log_error_color", 0xFF0000);
69         _warningColor = style.customColor("build_log_warning_color", 0x606000);
70         _deprecationColor = style.customColor("build_log_deprecation_color", 0x802040);
71     }
72 
73     /** 
74     Custom text color and style highlight (using text highlight) support.
75 
76     Return null if no syntax highlight required for line.
77     */
78     override protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) {
79         auto match = matchFirst(txt, ctr);
80         uint defColor = textColor;
81         uint flags = 0;
82         if(!match.empty) {
83             if (buf.length < txt.length)
84                 buf.length = txt.length;
85             CustomCharProps[] colors = buf[0..txt.length];
86             uint cl = _filenameColor;
87             flags = TextFlag.Underline;
88             for (int i = 0; i < txt.length; i++) {
89                 dstring rest = txt[i..$];
90                 if (rest.startsWith(" Error"d)) {
91                     cl = _errorColor;
92                     flags = 0;
93                 } else if (rest.startsWith(" Warning"d)) {
94                     cl = _warningColor;
95                     flags = 0;
96                 } else if (rest.startsWith(" Deprecation"d)) {
97                     cl = _deprecationColor;
98                     flags = 0;
99                 }
100                 colors[i].color = cl;
101                 colors[i].textFlags = flags;
102             }
103             return colors;
104         } else if (txt.startsWith("Building ")) {
105             CustomCharProps[] colors = new CustomCharProps[txt.length];
106             uint cl = defColor;
107             for (int i = 0; i < txt.length; i++) {
108                 dstring rest = txt[i..$];
109                 if (i == 9) {
110                     cl = _filenameColor;
111                     flags = TextFlag.Underline;
112                 } else if (rest.startsWith(" configuration"d)) {
113                     cl = defColor;
114                     flags = 0;
115                 }
116                 colors[i].color = cl;
117                 colors[i].textFlags = flags;
118             }
119             return colors;
120         } else if ((txt.startsWith("Performing ") && txt.indexOf(" build using ") > 0)
121                    || txt.startsWith("Upgrading project in ")
122                    ) {
123             CustomCharProps[] colors = new CustomCharProps[txt.length];
124             uint cl = defColor;
125             flags |= TextFlag.Underline;
126             for (int i = 0; i < txt.length; i++) {
127                 colors[i].color = cl;
128                 colors[i].textFlags = flags;
129             }
130             return colors;
131         } else if (txt.indexOf(": building configuration ") > 0) {
132             CustomCharProps[] colors = new CustomCharProps[txt.length];
133             uint cl = _filenameColor;
134             flags |= TextFlag.Underline;
135             for (int i = 0; i < txt.length; i++) {
136                 dstring rest = txt[i..$];
137                 if (rest.startsWith(": building configuration "d)) {
138                     //cl = defColor;
139                     flags &= ~TextFlag.Underline;
140                 }
141                 colors[i].color = cl;
142                 colors[i].textFlags = flags;
143             }
144             return colors;
145         }
146         return null;
147     }
148 
149     ErrorPosition errorFromLine(int line) {
150         if (line >= this.content.length || line < 0)
151             return null; // invalid line number
152         auto logLine = this.content.line(line);
153 
154         //src\tetris.d(49): Error: found 'return' when expecting ';' following statement
155 
156         auto match = matchFirst(logLine, ctr);
157 
158         if(!match.empty) {
159             dstring filename = match[1];
160             import std.conv:to;
161             int row = to!int(match[2]) - 1;
162             if (row < 0)
163                 row = 0;
164             int col = 0;
165             if (match[3] && match[3] != "") {
166                 col = to!int(match[3]) - 1;
167                 if (col < 0)
168                     col = 0;
169             }
170             return new ErrorPosition(filename, row, col);
171         }
172         return null;
173     }
174 
175     /// returns first error line info from log
176     ErrorPosition firstError() {
177         for (int i = 0; i < _content.length; i++) {
178             ErrorPosition err = errorFromLine(i);
179             if (err)
180                 return err;
181         }
182         return null;
183     }
184 
185     ///
186     override bool onMouseEvent(MouseEvent event) {
187 
188         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
189             super.onMouseEvent(event);
190 
191             auto errorPos = errorFromLine(_caretPos.line);
192             if (errorPos) {
193                 if (compilerLogIssueClickHandler.assigned) {
194                     compilerLogIssueClickHandler(errorPos.filename, errorPos.line, errorPos.pos);
195                 }
196             }
197 
198             auto logLine = this.content.line(this._caretPos.line);
199 
200             //src\tetris.d(49): Error: found 'return' when expecting ';' following statement
201 
202             auto match = matchFirst(logLine, ctr);
203 
204             if(!match.empty) {
205                 if (compilerLogIssueClickHandler.assigned) {
206                     import std.conv:to;
207                     int row = to!int(match[2]) - 1;
208                     if (row < 0)
209                         row = 0;
210                     int col = 0;
211                     if (match[3]) {
212                         col = to!int(match[3]) - 1;
213                         if (col < 0)
214                             col = 0;
215                     }
216 
217                     compilerLogIssueClickHandler(match[1], row, col);
218                 }
219             }
220 
221             return true;
222         }
223 
224         return super.onMouseEvent(event);
225     }
226 }
227 
228 ///
229 class OutputPanel : DockWindow {
230 
231     Signal!CompilerLogIssueClickHandler compilerLogIssueClickHandler;
232 
233     protected CompilerLogWidget _logWidget;
234     protected TerminalWidget _terminalWidget;
235 
236     TabWidget _tabs;
237 
238     @property TabWidget getTabs() { return _tabs;}
239 
240     void activateLogTab() {
241         _tabs.selectTab("logwidget");
242     }
243 
244     void activateTerminalTab(bool clear = false) {
245         static if (ENABLE_INTERNAL_TERMINAL) {
246             _tabs.selectTab("TERMINAL");
247             if (clear)
248                 _terminalWidget.resetTerminal();
249         }
250     }
251 
252     this(string id) {
253         _showCloseButton = false;
254         dockAlignment = DockAlignment.Bottom;
255         super(id);
256     }
257 
258     /// terminal device for Console tab
259     @property string terminalDeviceName() {
260         static if (ENABLE_INTERNAL_TERMINAL) {
261             if (_terminalWidget)
262                 return _terminalWidget.deviceName;
263         }
264         return null;
265     }
266 
267     ErrorPosition firstError() {
268         if (_logWidget)
269             return _logWidget.firstError();
270         return null;
271     }
272 
273     override protected Widget createBodyWidget() {
274         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
275         _tabs = new TabWidget("OutputPanelTabs", Align.Bottom);
276         //_tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT);
277         _tabs.setStyles(STYLE_DOCK_WINDOW, STYLE_TAB_DOWN_DARK, STYLE_TAB_DOWN_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT, STYLE_DOCK_HOST_BODY);
278         _tabs.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
279         _tabs.tabHost.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
280 
281         _logWidget = new CompilerLogWidget("logwidget");
282         _logWidget.readOnly = true;
283         _logWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
284         _logWidget.compilerLogIssueClickHandler = &onIssueClick;
285         _logWidget.styleId = "EDIT_BOX_NO_FRAME";
286 
287         //_tabs.tabHost.styleId = STYLE_DOCK_WINDOW_BODY;
288         _tabs.addTab(_logWidget, "Compiler Log"d);
289         _tabs.selectTab("logwidget");
290 
291         static if (ENABLE_INTERNAL_TERMINAL) {
292             _terminalWidget = new TerminalWidget("TERMINAL");
293             _terminalWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
294             _tabs.addTab(_terminalWidget, "Output"d);
295             _terminalWidget.write("Hello\nSecond line\nTest\n"d);
296         }
297         static if (ENABLE_INTERNAL_TERMINAL_TEST) {
298             _terminalWidget.write("Hello\nSecond line\nTest\n"d);
299             _terminalWidget.write("SomeString 123456789\rAwesomeString\n"d); // test \r
300             // testing tabs
301             _terminalWidget.write("id\tname\tdescription\n"d);
302             _terminalWidget.write("1\tFoo\tFoo line\n"d);
303             _terminalWidget.write("2\tBar\tBar line\n"d);
304             _terminalWidget.write("3\tFoobar\tFoo bar line\n"d);
305             _terminalWidget.write("\n\n\n"d);
306             // testing line wrapping
307             _terminalWidget.write("Testing very long line. Юникод. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"c);
308             // testing cursor position changes
309             _terminalWidget.write("\x1b[4;4HCURSOR(4,4)\x1b[HHOME\x1b[B*A\x1b[B*B\x1b[5C\x1b[D***\x1b[A*UP\x1b[3B*DOWN"d);
310             //_terminalWidget.write("\x1b[Jerased down"d);
311             //_terminalWidget.write("\x1b[1Jerased up"d);
312             //_terminalWidget.write("\x1b[2Jerased screen"d);
313             //_terminalWidget.write("\x1b[Kerased eol"d);
314             //_terminalWidget.write("\x1b[1Kerased bol"d);
315             //_terminalWidget.write("\x1b[2Kerased line"d);
316             //_terminalWidget.write("Юникод Unicode"d);
317             _terminalWidget.write("\x1b[34;45m blue on magenta "d);
318             _terminalWidget.write("\x1b[31;46m red on cyan "d);
319             //_terminalWidget.write("\x1b[2Jerased screen"d);
320             //TerminalDevice term = new TerminalDevice();
321             //if (!term.create()) {
322             //    Log.e("Cannot create terminal device");
323             //}
324             _terminalWidget.write("\n\n\n\nDevice: "d ~ toUTF32(_terminalWidget.deviceName));
325             _terminalWidget.write("\x1b[0m\nnormal text\n"d);
326         }
327         return _tabs;
328     }
329 
330     override protected void initialize() {
331         
332         //styleId = STYLE_DOCK_WINDOW;
333         styleId = null;
334         _bodyWidget = createBodyWidget();
335         //_bodyWidget.styleId = STYLE_DOCK_WINDOW_BODY;
336         addChild(_bodyWidget);
337     }
338 
339     //TODO: Refactor OutputPanel to expose CompilerLogWidget
340 
341     void appendText(string category, dstring msg) {
342         _logWidget.appendText(msg);
343     }
344 
345     void logLine(string category, dstring msg) {
346         appendText(category, msg ~ "\n");
347     }
348 
349     void logLine(dstring msg) {
350         logLine(null, msg);
351     }
352 
353     void logLine(string category, string msg) {
354         appendText(category, toUTF32(msg ~ "\n"));
355     }
356 
357     void logLine(string msg) {
358         logLine(null, msg);
359     }
360 
361     void clear(string category = null) {
362         _logWidget.text = ""d;
363     }
364 
365     private bool onIssueClick(dstring fn, int line, int column)
366     {
367         if (compilerLogIssueClickHandler.assigned) {
368             compilerLogIssueClickHandler(fn, line, column);
369         }
370 
371         return true;
372     }
373 }