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 }