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 void onTabClose(string tabId) { 274 Log.d("OutputPanel onTabClose ", tabId); 275 } 276 277 override protected Widget createBodyWidget() { 278 layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 279 _tabs = new TabWidget("OutputPanelTabs", Align.Bottom); 280 //_tabs.setStyles(STYLE_DOCK_HOST_BODY, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT); 281 _tabs.setStyles(STYLE_DOCK_WINDOW, STYLE_TAB_DOWN_DARK, STYLE_TAB_DOWN_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT, STYLE_DOCK_HOST_BODY); 282 _tabs.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 283 _tabs.tabHost.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 284 _tabs.tabClose = &onTabClose; 285 286 _logWidget = new CompilerLogWidget("logwidget"); 287 _logWidget.readOnly = true; 288 _logWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 289 _logWidget.compilerLogIssueClickHandler = &onIssueClick; 290 _logWidget.styleId = "EDIT_BOX_NO_FRAME"; 291 292 //_tabs.tabHost.styleId = STYLE_DOCK_WINDOW_BODY; 293 _tabs.addTab(_logWidget, "Compiler Log"d, null, true); 294 _tabs.selectTab("logwidget"); 295 296 static if (ENABLE_INTERNAL_TERMINAL) { 297 _terminalWidget = new TerminalWidget("TERMINAL"); 298 _terminalWidget.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT); 299 _tabs.addTab(_terminalWidget, "Output"d); 300 _terminalWidget.write("Hello\nSecond line\nTest\n"d); 301 } 302 static if (ENABLE_INTERNAL_TERMINAL_TEST) { 303 _terminalWidget.write("Hello\nSecond line\nTest\n"d); 304 _terminalWidget.write("SomeString 123456789\rAwesomeString\n"d); // test \r 305 // testing tabs 306 _terminalWidget.write("id\tname\tdescription\n"d); 307 _terminalWidget.write("1\tFoo\tFoo line\n"d); 308 _terminalWidget.write("2\tBar\tBar line\n"d); 309 _terminalWidget.write("3\tFoobar\tFoo bar line\n"d); 310 _terminalWidget.write("\n\n\n"d); 311 // testing line wrapping 312 _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); 313 // testing cursor position changes 314 _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); 315 //_terminalWidget.write("\x1b[Jerased down"d); 316 //_terminalWidget.write("\x1b[1Jerased up"d); 317 //_terminalWidget.write("\x1b[2Jerased screen"d); 318 //_terminalWidget.write("\x1b[Kerased eol"d); 319 //_terminalWidget.write("\x1b[1Kerased bol"d); 320 //_terminalWidget.write("\x1b[2Kerased line"d); 321 //_terminalWidget.write("Юникод Unicode"d); 322 _terminalWidget.write("\x1b[34;45m blue on magenta "d); 323 _terminalWidget.write("\x1b[31;46m red on cyan "d); 324 //_terminalWidget.write("\x1b[2Jerased screen"d); 325 //TerminalDevice term = new TerminalDevice(); 326 //if (!term.create()) { 327 // Log.e("Cannot create terminal device"); 328 //} 329 _terminalWidget.write("\n\n\n\nDevice: "d ~ toUTF32(_terminalWidget.deviceName)); 330 _terminalWidget.write("\x1b[0m\nnormal text\n"d); 331 } 332 return _tabs; 333 } 334 335 override protected void initialize() { 336 337 //styleId = STYLE_DOCK_WINDOW; 338 styleId = null; 339 _bodyWidget = createBodyWidget(); 340 //_bodyWidget.styleId = STYLE_DOCK_WINDOW_BODY; 341 addChild(_bodyWidget); 342 } 343 344 //TODO: Refactor OutputPanel to expose CompilerLogWidget 345 346 void appendText(string category, dstring msg) { 347 _logWidget.appendText(msg); 348 } 349 350 void logLine(string category, dstring msg) { 351 appendText(category, msg ~ "\n"); 352 } 353 354 void logLine(dstring msg) { 355 logLine(null, msg); 356 } 357 358 void logLine(string category, string msg) { 359 appendText(category, toUTF32(msg ~ "\n")); 360 } 361 362 void logLine(string msg) { 363 logLine(null, msg); 364 } 365 366 void clear(string category = null) { 367 _logWidget.text = ""d; 368 } 369 370 private bool onIssueClick(dstring fn, int line, int column) 371 { 372 if (compilerLogIssueClickHandler.assigned) { 373 compilerLogIssueClickHandler(fn, line, column); 374 } 375 376 return true; 377 } 378 }