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 projectname, dstring filename, int line, int column);
29 }
30 
31 class ErrorPosition {
32     dstring projectname;
33     dstring filename;
34     int line;
35     int pos;
36     this(dstring pname, dstring fn, int l, int p) {
37         projectname = pname;
38         filename = fn;
39         line = l;
40         pos = p;
41     }
42 }
43 
44 /// Log widget with parsing of compiler output
45 class CompilerLogWidget : LogWidget {
46 
47     Signal!CompilerLogIssueClickHandler compilerLogIssueClickHandler;
48 
49     protected string _baseDirectory;
50     @property string baseDirectory() { return _baseDirectory; }
51     @property void baseDirectory(string dir) { _baseDirectory = dir; }
52 
53     //auto ctr = ctRegex!(r"(.+)\((\d+)\): (Error|Warning|Deprecation): (.+)"d);
54     auto ctr = ctRegex!(r"(.+)\((\d+)(?:,(\d+))?\): (Error|Warning|Deprecation): (.+)"d);
55 
56     /// forward to super c'tor
57     this(string ID) {
58         super(ID);
59         _hscrollbarMode = ScrollBarMode.Auto;
60         _vscrollbarMode = ScrollBarMode.Auto;
61         //auto match2 = matchFirst("file.d(123,234): Error: bla bla"d, ctr2);
62         //if (!match2.empty) {
63         //    Log.d("found");
64         //}
65     }
66 
67     protected uint _filenameColor = 0x0000C0;
68     protected uint _errorColor = 0xFF0000;
69     protected uint _warningColor = 0x606000;
70     protected uint _deprecationColor = 0x802040;
71 
72     /// handle theme change: e.g. reload some themed resources
73     override void onThemeChanged() {
74         super.onThemeChanged();
75         _filenameColor = style.customColor("build_log_filename_color", 0x0000C0);
76         _errorColor = style.customColor("build_log_error_color", 0xFF0000);
77         _warningColor = style.customColor("build_log_warning_color", 0x606000);
78         _deprecationColor = style.customColor("build_log_deprecation_color", 0x802040);
79     }
80 
81     /** 
82     Custom text color and style highlight (using text highlight) support.
83 
84     Return null if no syntax highlight required for line.
85     */
86     override protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) {
87         auto match = matchFirst(txt, ctr);
88         uint defColor = textColor;
89         uint flags = 0;
90         if(!match.empty) {
91             if (buf.length < txt.length)
92                 buf.length = txt.length;
93             CustomCharProps[] colors = buf[0..txt.length];
94             uint cl = _filenameColor;
95             flags = TextFlag.Underline;
96             for (int i = 0; i < txt.length; i++) {
97                 dstring rest = txt[i..$];
98                 if (rest.startsWith(" Error"d)) {
99                     cl = _errorColor;
100                     flags = 0;
101                 } else if (rest.startsWith(" Warning"d)) {
102                     cl = _warningColor;
103                     flags = 0;
104                 } else if (rest.startsWith(" Deprecation"d)) {
105                     cl = _deprecationColor;
106                     flags = 0;
107                 }
108                 colors[i].color = cl;
109                 colors[i].textFlags = flags;
110             }
111             return colors;
112         } else if (txt.startsWith("Building ")) {
113             CustomCharProps[] colors = new CustomCharProps[txt.length];
114             uint cl = defColor;
115             for (int i = 0; i < txt.length; i++) {
116                 dstring rest = txt[i..$];
117                 if (i == 9) {
118                     cl = _filenameColor;
119                     flags = TextFlag.Underline;
120                 } else if (rest.startsWith(" configuration"d)) {
121                     cl = defColor;
122                     flags = 0;
123                 }
124                 colors[i].color = cl;
125                 colors[i].textFlags = flags;
126             }
127             return colors;
128         } else if ((txt.startsWith("Performing ") && txt.indexOf(" build using ") > 0)
129                    || txt.startsWith("Upgrading project in ")
130                    ) {
131             CustomCharProps[] colors = new CustomCharProps[txt.length];
132             uint cl = defColor;
133             flags |= TextFlag.Underline;
134             for (int i = 0; i < txt.length; i++) {
135                 colors[i].color = cl;
136                 colors[i].textFlags = flags;
137             }
138             return colors;
139         } else if (txt.indexOf(": building configuration ") > 0) {
140             CustomCharProps[] colors = new CustomCharProps[txt.length];
141             uint cl = _filenameColor;
142             flags |= TextFlag.Underline;
143             for (int i = 0; i < txt.length; i++) {
144                 dstring rest = txt[i..$];
145                 if (rest.startsWith(": building configuration "d)) {
146                     //cl = defColor;
147                     flags &= ~TextFlag.Underline;
148                 }
149                 colors[i].color = cl;
150                 colors[i].textFlags = flags;
151             }
152             return colors;
153         }
154         return null;
155     }
156 
157     ErrorPosition errorFromLine(int line) {
158         if (line >= this.content.length || line < 0)
159             return null; // invalid line number
160         auto logLine = this.content.line(line);
161 
162         //src\tetris.d(49): Error: found 'return' when expecting ';' following statement
163 
164         auto match = matchFirst(logLine, ctr);
165 
166         if(!match.empty) {
167             dstring filename = match[1];
168             import std.conv:to;
169             int row = 0;
170             try {
171                 row = to!int(match[2]) - 1;
172             } catch (Exception e) {
173                 row = 0;
174             }
175             if (row < 0)
176                 row = 0;
177             int col = 0;
178             if (match[3] && match[3] != "") {
179                 try {
180                     col = to!int(match[3]) - 1;
181                 } catch (Exception e) {
182                     col = 0;
183                 }
184                 if (col < 0)
185                     col = 0;
186             }
187             dstring projectname = findProjectForLine(line).to!dstring;
188             if (filename.startsWith("../") || filename.startsWith("..\\")) {
189                 import dlangui.core.types : toUTF8;
190                 string fn = filename.toUTF8;
191                 resolveRelativePath(fn, line);
192                 filename = fn.toUTF32;
193             }
194             return new ErrorPosition(projectname, filename, row, col);
195         }
196     
197         return null;
198     }
199 
200     /// returns first error line info from log
201     ErrorPosition firstError() {
202         for (int i = 0; i < _content.length; i++) {
203             ErrorPosition err = errorFromLine(i);
204             if (err)
205                 return err;
206         }
207         return null;
208     }
209 
210     //dlangui ~master: building configuration "default"...
211     string findProjectForLine(int line) {
212         for (int i = line - 1; i >= 0; i--) {
213             dstring s = _content[i];
214             int p = cast(int)s.indexOf(": building configuration "d);
215             if (p >= 0) {
216                 int p0 = cast(int)s.indexOf(" ");
217                 if (p0 > 0 && p0 < p) {
218                     import dlangui.core.types : toUTF8;
219                     return s[0 .. p0].toUTF8;
220                 }
221             }
222         }
223         return null;
224     }
225     
226     void resolveRelativePath(ref string path, int line) {
227         import std.path : getcwd, absolutePath;
228         Log.d("resolveRelativePath ", path, " current directory: ", getcwd);
229         string prjName = findProjectForLine(line);
230         if (prjName) {
231             Log.d("Error is in project ", prjName);
232         }
233         string base = _baseDirectory;
234         if (!base)
235             base = getcwd;
236         // TODO: select proper base
237         path = absolutePath(path, base);
238         Log.d("converted to absolute path: ", path);
239     }
240     ///
241     override bool onMouseEvent(MouseEvent event) {
242 
243         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
244             super.onMouseEvent(event);
245 
246             auto errorPos = errorFromLine(_caretPos.line);
247             if (errorPos) {
248                 if (compilerLogIssueClickHandler.assigned) {
249                     compilerLogIssueClickHandler(errorPos.projectname, errorPos.filename, errorPos.line, errorPos.pos);
250                 }
251             }
252             return false;
253         }
254         return super.onMouseEvent(event);
255     }
256 }
257 
258 ///
259 class OutputPanel : DockWindow {
260 
261     Signal!CompilerLogIssueClickHandler compilerLogIssueClickHandler;
262 
263     protected CompilerLogWidget _logWidget;
264     protected TerminalWidget _terminalWidget;
265 
266     TabWidget _tabs;
267 
268     @property TabWidget getTabs() { return _tabs;}
269 
270     void setLogWidgetBaseDirectory(string baseDir) {
271         _logWidget.baseDirectory = baseDir;
272     }
273 
274     void activateLogTab() {
275         ensureLogVisible();
276         _tabs.selectTab("logwidget");
277     }
278 
279     void activateTerminalTab(bool clear = false) {
280         static if (ENABLE_INTERNAL_TERMINAL) {
281             ensureLogVisible();
282             _tabs.selectTab("TERMINAL");
283             if (clear)
284                 _terminalWidget.resetTerminal();
285         }
286     }
287 
288     this(string id) {
289         _showCloseButton = false;
290         dockAlignment = DockAlignment.Bottom;
291         super(id);
292     }
293 
294     /// terminal device for Console tab
295     @property string terminalDeviceName() {
296         static if (ENABLE_INTERNAL_TERMINAL) {
297             if (_terminalWidget)
298                 return _terminalWidget.deviceName;
299         }
300         return null;
301     }
302 
303     ErrorPosition firstError() {
304         if (_logWidget)
305             return _logWidget.firstError();
306         return null;
307     }
308 
309     void onTabClose(string tabId) {
310         Log.d("OutputPanel onTabClose ", tabId);
311         if (tabId == "search") {
312             _tabs.removeTab(tabId);
313         }
314     }
315 
316     bool onCloseButton(Widget source) {
317         visibility = Visibility.Gone;
318         return true;
319     }
320 
321     override protected Widget createBodyWidget() {
322         layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
323         _tabs = new TabWidget("OutputPanelTabs", Align.Bottom);
324         //_tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT);
325         _tabs.setStyles(STYLE_DOCK_WINDOW, STYLE_TAB_DOWN_DARK, STYLE_TAB_DOWN_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT, STYLE_DOCK_HOST_BODY);
326         _tabs.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
327         _tabs.tabHost.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
328         _tabs.tabClose = &onTabClose;
329         _tabs.tabControl.moreButtonIcon = "close";
330         _tabs.tabControl.enableMoreButton = true;
331         _tabs.tabControl.autoMoreButtonMenu = false;
332         _tabs.tabControl.moreButtonClick = &onCloseButton;
333 
334         _logWidget = new CompilerLogWidget("logwidget");
335         _logWidget.readOnly = true;
336         _logWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
337         _logWidget.compilerLogIssueClickHandler = &onIssueClick;
338         _logWidget.styleId = "EDIT_BOX_NO_FRAME";
339 
340         //_tabs.tabHost.styleId = STYLE_DOCK_WINDOW_BODY;
341         _tabs.addTab(_logWidget, "Compiler Log"d, null, false);
342         _tabs.selectTab("logwidget");
343 
344         static if (ENABLE_INTERNAL_TERMINAL) {
345             _terminalWidget = new TerminalWidget("TERMINAL");
346             _terminalWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
347             _tabs.addTab(_terminalWidget, "Output"d);
348             _terminalWidget.write("Hello\nSecond line\nTest\n"d);
349         }
350         static if (ENABLE_INTERNAL_TERMINAL_TEST) {
351             _terminalWidget.write("Hello\nSecond line\nTest\n"d);
352             _terminalWidget.write("SomeString 123456789\rAwesomeString\n"d); // test \r
353             // testing tabs
354             _terminalWidget.write("id\tname\tdescription\n"d);
355             _terminalWidget.write("1\tFoo\tFoo line\n"d);
356             _terminalWidget.write("2\tBar\tBar line\n"d);
357             _terminalWidget.write("3\tFoobar\tFoo bar line\n"d);
358             _terminalWidget.write("\n\n\n"d);
359             // testing line wrapping
360             _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);
361             // testing cursor position changes
362             _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);
363             //_terminalWidget.write("\x1b[Jerased down"d);
364             //_terminalWidget.write("\x1b[1Jerased up"d);
365             //_terminalWidget.write("\x1b[2Jerased screen"d);
366             //_terminalWidget.write("\x1b[Kerased eol"d);
367             //_terminalWidget.write("\x1b[1Kerased bol"d);
368             //_terminalWidget.write("\x1b[2Kerased line"d);
369             //_terminalWidget.write("Юникод Unicode"d);
370             _terminalWidget.write("\x1b[34;45m blue on magenta "d);
371             _terminalWidget.write("\x1b[31;46m red on cyan "d);
372             //_terminalWidget.write("\x1b[2Jerased screen"d);
373             //TerminalDevice term = new TerminalDevice();
374             //if (!term.create()) {
375             //    Log.e("Cannot create terminal device");
376             //}
377             _terminalWidget.write("\n\n\n\nDevice: "d ~ toUTF32(_terminalWidget.deviceName));
378             _terminalWidget.write("\x1b[0m\nnormal text\n"d);
379         }
380         return _tabs;
381     }
382 
383     override protected void initialize() {
384         
385         //styleId = STYLE_DOCK_WINDOW;
386         styleId = null;
387         _bodyWidget = createBodyWidget();
388         //_bodyWidget.styleId = STYLE_DOCK_WINDOW_BODY;
389         addChild(_bodyWidget);
390     }
391 
392     //TODO: Refactor OutputPanel to expose CompilerLogWidget
393 
394     void ensureLogVisible() {
395         if (visibility == Visibility.Gone) {
396             visibility = Visibility.Visible;
397             parent.layout(parent.pos);
398         }
399     }
400 
401     void appendText(string category, dstring msg) {
402         ensureLogVisible();
403         _logWidget.appendText(msg);
404     }
405 
406     void logLine(string category, dstring msg) {
407         appendText(category, msg ~ "\n");
408     }
409 
410     void logLine(dstring msg) {
411         logLine(null, msg);
412     }
413 
414     void logLine(string category, string msg) {
415         appendText(category, toUTF32(msg ~ "\n"));
416     }
417 
418     void logLine(string msg) {
419         logLine(null, msg);
420     }
421 
422     void clear(string category = null) {
423         _logWidget.text = ""d;
424     }
425 
426     private bool onIssueClick(dstring projectname, dstring fn, int line, int column)
427     {
428         if (compilerLogIssueClickHandler.assigned) {
429             compilerLogIssueClickHandler(projectname, fn, line, column);
430         }
431 
432         return true;
433     }
434 }