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 }