1/* 2 * This file is part of gitg 3 * 4 * Copyright (C) 2016 - Jesse van den Kieboom 5 * 6 * gitg is free software; you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation; either version 2 of the License, or 9 * (at your option) any later version. 10 * 11 * gitg is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with gitg. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20[GtkTemplate (ui = "/org/gnome/gitg/ui/gitg-diff-view-file-renderer-text.ui")] 21class Gitg.DiffViewFileRendererText : Gtk.SourceView, DiffSelectable, DiffViewFileRenderer 22{ 23 private enum RegionType 24 { 25 ADDED, 26 REMOVED, 27 CONTEXT 28 } 29 30 private struct Region 31 { 32 public RegionType type; 33 public int buffer_line_start; 34 public int source_line_start; 35 public int length; 36 } 37 38 public uint added { get; set; } 39 public uint removed { get; set; } 40 41 private int64 d_doffset; 42 43 private Gee.HashMap<int, PatchSet.Patch?> d_lines; 44 45 private DiffViewFileSelectable d_selectable; 46 private DiffViewLinesRenderer d_old_lines; 47 private DiffViewLinesRenderer d_new_lines; 48 private DiffViewLinesRenderer d_sym_lines; 49 50 private bool d_highlight; 51 52 private Cancellable? d_higlight_cancellable; 53 private Gtk.SourceBuffer? d_old_highlight_buffer; 54 private Gtk.SourceBuffer? d_new_highlight_buffer; 55 private bool d_old_highlight_ready; 56 private bool d_new_highlight_ready; 57 private Gtk.CssProvider css_provider; 58 59 private Region[] d_regions; 60 private bool d_constructed; 61 62 private Settings? d_stylesettings; 63 64 private Settings? d_fontsettings; 65 66 public bool new_is_workdir { get; construct set; } 67 68 public bool wrap_lines 69 { 70 get { return this.wrap_mode != Gtk.WrapMode.NONE; } 71 set 72 { 73 if (value) 74 { 75 this.wrap_mode = Gtk.WrapMode.WORD_CHAR; 76 } 77 else 78 { 79 this.wrap_mode = Gtk.WrapMode.NONE; 80 } 81 } 82 } 83 84 public new int tab_width 85 { 86 get { return (int)get_tab_width(); } 87 set { set_tab_width((uint)value); } 88 } 89 90 public int maxlines { get; set; } 91 92 public DiffViewFileInfo info { get; construct set; } 93 94 public Ggit.DiffDelta? delta 95 { 96 get { return info.delta; } 97 } 98 99 public Repository? repository 100 { 101 get { return info.repository; } 102 } 103 104 public bool highlight 105 { 106 get { return d_highlight; } 107 108 construct set 109 { 110 if (d_highlight != value) 111 { 112 d_highlight = value; 113 update_highlight(); 114 } 115 } 116 } 117 118 private bool d_has_selection; 119 120 public bool has_selection 121 { 122 get { return d_has_selection; } 123 } 124 125 public bool can_select { get; construct set; } 126 127 public PatchSet selection 128 { 129 owned get 130 { 131 var ret = new PatchSet(); 132 133 ret.filename = delta.get_new_file().get_path(); 134 135 var patches = new PatchSet.Patch[0]; 136 137 if (!can_select) 138 { 139 return ret; 140 } 141 142 var selected = d_selectable.get_selected_lines(); 143 144 for (var i = 0; i < selected.length; i++) 145 { 146 var line = selected[i]; 147 var pset = d_lines[line]; 148 149 if (i == 0) 150 { 151 patches += pset; 152 continue; 153 } 154 155 var last = patches[patches.length - 1]; 156 157 if (last.new_offset + last.length == pset.new_offset && 158 last.type == pset.type) 159 { 160 last.length += pset.length; 161 patches[patches.length - 1] = last; 162 } 163 else 164 { 165 patches += pset; 166 } 167 } 168 169 ret.patches = patches; 170 return ret; 171 } 172 } 173 174 public DiffViewFileRendererText(DiffViewFileInfo info, bool can_select) 175 { 176 Object(info: info, can_select: can_select); 177 } 178 179 construct 180 { 181 var gutter = this.get_gutter(Gtk.TextWindowType.LEFT); 182 183 d_old_lines = new DiffViewLinesRenderer(DiffViewLinesRenderer.Style.OLD); 184 d_new_lines = new DiffViewLinesRenderer(DiffViewLinesRenderer.Style.NEW); 185 d_sym_lines = new DiffViewLinesRenderer(DiffViewLinesRenderer.Style.SYMBOL); 186 187 this.bind_property("maxlines", d_old_lines, "maxlines", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE); 188 this.bind_property("maxlines", d_new_lines, "maxlines", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE); 189 190 d_old_lines.xpad = 8; 191 d_new_lines.xpad = 8; 192 d_sym_lines.xpad = 6; 193 194 gutter.insert(d_old_lines, 0); 195 gutter.insert(d_new_lines, 1); 196 gutter.insert(d_sym_lines, 2); 197 198 this.set_border_window_size(Gtk.TextWindowType.TOP, 1); 199 200 var settings = Gtk.Settings.get_default(); 201 settings.notify["gtk-application-prefer-dark-theme"].connect(update_theme); 202 203 css_provider = new Gtk.CssProvider(); 204 get_style_context().add_provider(css_provider,Gtk.STYLE_PROVIDER_PRIORITY_SETTINGS); 205 206 update_theme(); 207 208 if (can_select) 209 { 210 d_selectable = new DiffViewFileSelectable(this); 211 212 d_selectable.notify["has-selection"].connect(() => { 213 d_has_selection = d_selectable.has_selection; 214 notify_property("has-selection"); 215 }); 216 } 217 218 d_lines = new Gee.HashMap<int, PatchSet.Patch?>(); 219 220 highlight = true; 221 } 222 223 protected override void dispose() 224 { 225 base.dispose(); 226 227 if (d_higlight_cancellable != null) 228 { 229 d_higlight_cancellable.cancel(); 230 d_higlight_cancellable = null; 231 } 232 } 233 234 private void update_highlight() 235 { 236 if (!d_constructed) 237 { 238 return; 239 } 240 241 if (d_higlight_cancellable != null) 242 { 243 d_higlight_cancellable.cancel(); 244 d_higlight_cancellable = null; 245 } 246 247 d_old_highlight_buffer = null; 248 d_new_highlight_buffer = null; 249 250 d_old_highlight_ready = false; 251 d_new_highlight_ready = false; 252 253 if (highlight && repository != null && delta != null) 254 { 255 var cancellable = new Cancellable(); 256 d_higlight_cancellable = cancellable; 257 258 init_highlighting_buffer_old.begin(cancellable, (obj, res) => { 259 init_highlighting_buffer_old.end(res); 260 }); 261 262 init_highlighting_buffer_new.begin(cancellable, (obj, res) => { 263 init_highlighting_buffer_new.end(res); 264 }); 265 } 266 else 267 { 268 update_highlighting_ready(); 269 } 270 } 271 272 private async void init_highlighting_buffer_old(Cancellable cancellable) 273 { 274 var buffer = yield init_highlighting_buffer(delta.get_old_file(), false, cancellable); 275 276 if (!cancellable.is_cancelled()) 277 { 278 d_old_highlight_buffer = buffer; 279 d_old_highlight_ready = true; 280 281 update_highlighting_ready(); 282 } 283 } 284 285 private File? get_file_location(Ggit.DiffFile file) 286 { 287 var path = file.get_path(); 288 289 if (path == null) 290 { 291 return null; 292 } 293 294 var workdir = repository.get_workdir(); 295 296 if (workdir == null) 297 { 298 return null; 299 } 300 301 return workdir.get_child(path); 302 } 303 304 private async void init_highlighting_buffer_new(Cancellable cancellable) 305 { 306 Gtk.SourceBuffer? buffer; 307 308 var file = delta.get_new_file(); 309 310 if (info.new_file_input_stream != null) 311 { 312 // Use once 313 var stream = info.new_file_input_stream; 314 info.new_file_input_stream = null; 315 316 buffer = yield init_highlighting_buffer_from_stream(delta.get_new_file(), 317 get_file_location(file), 318 stream, 319 info.new_file_content_type, 320 cancellable); 321 } 322 else 323 { 324 buffer = yield init_highlighting_buffer(delta.get_new_file(), info.from_workdir, cancellable); 325 } 326 327 if (!cancellable.is_cancelled()) 328 { 329 d_new_highlight_buffer = buffer; 330 d_new_highlight_ready = true; 331 332 update_highlighting_ready(); 333 } 334 } 335 336 private async Gtk.SourceBuffer? init_highlighting_buffer(Ggit.DiffFile file, bool from_workdir, Cancellable cancellable) 337 { 338 var id = file.get_oid(); 339 var location = get_file_location(file); 340 341 if ((id.is_zero() && !from_workdir) || (location == null && from_workdir)) 342 { 343 return null; 344 } 345 346 uint8[] content; 347 348 if (!from_workdir) 349 { 350 Ggit.Blob blob; 351 352 try 353 { 354 blob = repository.lookup<Ggit.Blob>(id); 355 } 356 catch 357 { 358 return null; 359 } 360 361 content = blob.get_raw_content(); 362 } 363 else 364 { 365 // Try to read from disk 366 try 367 { 368 string etag; 369 370 // Read it all into a buffer so we can guess the content type from 371 // it. This isn't really nice, but it's simple. 372 yield location.load_contents_async(cancellable, out content, out etag); 373 } 374 catch 375 { 376 return null; 377 } 378 } 379 380 bool uncertain; 381 var content_type = GLib.ContentType.guess(location.get_basename(), content, out uncertain); 382 383 var stream = new GLib.MemoryInputStream.from_bytes(new Bytes(content)); 384 385 return yield init_highlighting_buffer_from_stream(file, location, stream, content_type, cancellable); 386 } 387 388 private async Gtk.SourceBuffer? init_highlighting_buffer_from_stream(Ggit.DiffFile file, File location, InputStream stream, string content_type, Cancellable cancellable) 389 { 390 var manager = Gtk.SourceLanguageManager.get_default(); 391 var language = manager.guess_language(location != null ? location.get_basename() : null, content_type); 392 393 if (language == null) 394 { 395 return null; 396 } 397 398 var buffer = new Gtk.SourceBuffer(this.buffer.tag_table); 399 400 var style_scheme_manager = Gtk.SourceStyleSchemeManager.get_default(); 401 402 buffer.language = language; 403 buffer.highlight_syntax = true; 404 d_fontsettings = try_settings("org.gnome.desktop.interface"); 405 if (d_fontsettings != null) 406 { 407 d_fontsettings.changed["monospace-font-name"].connect((s, k) => { 408 update_font(); 409 }); 410 411 update_font(); 412 } 413 d_stylesettings = try_settings(Gitg.Config.APPLICATION_ID + ".preferences.interface"); 414 if (d_stylesettings != null) 415 { 416 d_stylesettings.changed["style-scheme"].connect((s, k) => { 417 update_style(); 418 }); 419 420 update_style(); 421 } else { 422 buffer.style_scheme = style_scheme_manager.get_scheme("classic"); 423 } 424 425 var sfile = new Gtk.SourceFile(); 426 sfile.location = location; 427 428 var loader = new Gtk.SourceFileLoader.from_stream(buffer, sfile, stream); 429 430 try 431 { 432 yield loader.load_async(GLib.Priority.LOW, cancellable, null); 433 this.strip_carriage_returns(buffer); 434 } 435 catch (Error e) 436 { 437 if (!cancellable.is_cancelled()) 438 { 439 stderr.printf(@"ERROR: failed to load $(file.get_path()) for highlighting: $(e.message)\n"); 440 } 441 442 return null; 443 } 444 445 return buffer; 446 } 447 448 private void update_style() 449 { 450 var scheme = d_stylesettings.get_string("style-scheme"); 451 var manager = Gtk.SourceStyleSchemeManager.get_default(); 452 var s = manager.get_scheme(scheme); 453 454 if (s != null) 455 { 456 (buffer as Gtk.SourceBuffer).style_scheme = s; 457 } 458 } 459 460 private void update_font() 461 { 462 var fname = d_fontsettings.get_string("monospace-font-name"); 463 var font_desc = Pango.FontDescription.from_string(fname); 464 var css = "textview{%s}".printf(Dazzle.pango_font_description_to_css(font_desc)); 465 try 466 { 467 css_provider.load_from_data(css); 468 } 469 catch(Error e) 470 { 471 warning("Error applying font: %s", e.message); 472 } 473 } 474 475 private Settings? try_settings(string schema_id) 476 { 477 var source = SettingsSchemaSource.get_default(); 478 479 if (source == null) 480 { 481 return null; 482 } 483 484 if (source.lookup(schema_id, true) != null) 485 { 486 return new Settings(schema_id); 487 } 488 489 return null; 490 } 491 492 private void strip_carriage_returns(Gtk.SourceBuffer buffer) 493 { 494 var search_settings = new Gtk.SourceSearchSettings(); 495 496 search_settings.regex_enabled = true; 497 search_settings.search_text = "\\r"; 498 499 var search_context = new Gtk.SourceSearchContext(buffer, search_settings); 500 501 try 502 { 503 search_context.replace_all("", 0); 504 } catch (Error e) {} 505 } 506 507 private void update_highlighting_ready() 508 { 509 if (!d_old_highlight_ready && !d_new_highlight_ready) 510 { 511 // Remove highlights 512 return; 513 } 514 else if (!d_old_highlight_ready || !d_new_highlight_ready) 515 { 516 // Both need to be loaded 517 return; 518 } 519 520 var buffer = this.buffer; 521 522 // Go over all the source chunks and match up to old/new buffer. Then, 523 // apply the tags that are applied to the highlighted source buffers. 524 foreach (var region in d_regions) 525 { 526 Gtk.SourceBuffer? source; 527 528 if (region.type == RegionType.REMOVED) 529 { 530 source = d_old_highlight_buffer; 531 } 532 else 533 { 534 source = d_new_highlight_buffer; 535 } 536 537 if (source == null) 538 { 539 continue; 540 } 541 542 Gtk.TextIter buffer_iter, source_iter; 543 544 buffer.get_iter_at_line(out buffer_iter, region.buffer_line_start); 545 source.get_iter_at_line(out source_iter, region.source_line_start); 546 547 var source_end_iter = source_iter; 548 source_end_iter.forward_lines(region.length); 549 550 source.ensure_highlight(source_iter, source_end_iter); 551 552 var buffer_end_iter = buffer_iter; 553 buffer_end_iter.forward_lines(region.length); 554 555 var source_next_iter = source_iter; 556 var tags = source_iter.get_tags(); 557 558 while (source_next_iter.forward_to_tag_toggle(null) && source_next_iter.compare(source_end_iter) < 0) 559 { 560 var buffer_next_iter = buffer_iter; 561 buffer_next_iter.forward_chars(source_next_iter.get_offset() - source_iter.get_offset()); 562 563 foreach (var tag in tags) 564 { 565 buffer.apply_tag(tag, buffer_iter, buffer_next_iter); 566 } 567 568 source_iter = source_next_iter; 569 buffer_iter = buffer_next_iter; 570 571 tags = source_iter.get_tags(); 572 } 573 574 foreach (var tag in tags) 575 { 576 buffer.apply_tag(tag, buffer_iter, buffer_end_iter); 577 } 578 } 579 } 580 581 protected override bool draw(Cairo.Context cr) 582 { 583 base.draw(cr); 584 585 var win = this.get_window(Gtk.TextWindowType.LEFT); 586 587 if (!Gtk.cairo_should_draw_window(cr, win)) 588 { 589 return false; 590 } 591 592 var ctx = this.get_style_context(); 593 594 var old_lines_width = d_old_lines.size + d_old_lines.xpad * 2; 595 var new_lines_width = d_new_lines.size + d_new_lines.xpad * 2; 596 var sym_lines_width = d_sym_lines.size + d_sym_lines.xpad * 2; 597 598 ctx.save(); 599 Gtk.cairo_transform_to_window(cr, this, win); 600 ctx.add_class("diff-lines-separator"); 601 ctx.render_frame(cr, 0, 0, old_lines_width, win.get_height()); 602 ctx.restore(); 603 604 ctx.save(); 605 Gtk.cairo_transform_to_window(cr, this, win); 606 ctx.add_class("diff-lines-gutter-border"); 607 ctx.render_frame(cr, old_lines_width + new_lines_width, 0, sym_lines_width, win.get_height()); 608 ctx.restore(); 609 610 return false; 611 } 612 613 private void update_theme() 614 { 615 var header_attributes = new Gtk.SourceMarkAttributes(); 616 var added_attributes = new Gtk.SourceMarkAttributes(); 617 var removed_attributes = new Gtk.SourceMarkAttributes(); 618 619 var dark = new Theme().is_theme_dark(); 620 621 if (dark) 622 { 623 header_attributes.background = Gdk.RGBA() { red = 88.0 / 255.0, green = 88.0 / 255.0, blue = 88.0 / 255.0, alpha = 1.0 }; 624 added_attributes.background = Gdk.RGBA() { red = 32.0 / 255.0, green = 68.0 / 255.0, blue = 21.0 / 255.0, alpha = 1.0 }; 625 removed_attributes.background = Gdk.RGBA() { red = 130.0 / 255.0, green = 55.0 / 255.0, blue = 53.0 / 255.0, alpha = 1.0 }; 626 } 627 else 628 { 629 header_attributes.background = Gdk.RGBA() { red = 244.0 / 255.0, green = 247.0 / 255.0, blue = 251.0 / 255.0, alpha = 1.0 }; 630 added_attributes.background = Gdk.RGBA() { red = 220.0 / 255.0, green = 1.0, blue = 220.0 / 255.0, alpha = 1.0 }; 631 removed_attributes.background = Gdk.RGBA() { red = 1.0, green = 220.0 / 255.0, blue = 220.0 / 255.0, alpha = 1.0 }; 632 } 633 634 this.set_mark_attributes("header", header_attributes, 0); 635 this.set_mark_attributes("added", added_attributes, 0); 636 this.set_mark_attributes("removed", removed_attributes, 0); 637 } 638 639 protected override void constructed() 640 { 641 base.constructed(); 642 643 d_constructed = true; 644 update_highlight(); 645 } 646 647 public void add_hunk(Ggit.DiffHunk hunk, Gee.ArrayList<Ggit.DiffLine> lines) 648 { 649 var buffer = this.buffer as Gtk.SourceBuffer; 650 651 /* Diff hunk */ 652 var h = hunk.get_header(); 653 var pos = h.last_index_of("@@"); 654 655 if (pos >= 0) 656 { 657 h = h.substring(pos + 2).chug(); 658 } 659 660 h = h.chomp(); 661 662 Gtk.TextIter iter; 663 buffer.get_end_iter(out iter); 664 665 if (!iter.is_start()) 666 { 667 buffer.insert(ref iter, "\n", 1); 668 } 669 670 iter.set_line_offset(0); 671 buffer.create_source_mark(null, "header", iter); 672 673 var header = @"@@ -$(hunk.get_old_start()),$(hunk.get_old_lines()) +$(hunk.get_new_start()),$(hunk.get_new_lines()) @@ $h\n"; 674 buffer.insert(ref iter, header, -1); 675 676 int buffer_line = iter.get_line(); 677 678 /* Diff Content */ 679 var content = new StringBuilder(); 680 681 var region = Region() { 682 type = RegionType.CONTEXT, 683 buffer_line_start = 0, 684 source_line_start = 0, 685 length = 0 686 }; 687 688 this.freeze_notify(); 689 690 for (var i = 0; i < lines.size; i++) 691 { 692 var line = lines[i]; 693 var text = line.get_text().replace("\r", ""); 694 var added = false; 695 var removed = false; 696 var origin = line.get_origin(); 697 698 var rtype = RegionType.CONTEXT; 699 700 switch (origin) 701 { 702 case Ggit.DiffLineType.ADDITION: 703 added = true; 704 this.added++; 705 706 rtype = RegionType.ADDED; 707 break; 708 case Ggit.DiffLineType.DELETION: 709 removed = true; 710 this.removed++; 711 712 rtype = RegionType.REMOVED; 713 break; 714 case Ggit.DiffLineType.CONTEXT_EOFNL: 715 case Ggit.DiffLineType.ADD_EOFNL: 716 case Ggit.DiffLineType.DEL_EOFNL: 717 text = text.substring(1); 718 break; 719 } 720 721 if (i == 0 || rtype != region.type) 722 { 723 if (i != 0) 724 { 725 d_regions += region; 726 } 727 728 int source_line_start; 729 730 if (rtype == RegionType.REMOVED) 731 { 732 source_line_start = line.get_old_lineno() - 1; 733 } 734 else 735 { 736 source_line_start = line.get_new_lineno() - 1; 737 } 738 739 region = Region() { 740 type = rtype, 741 buffer_line_start = buffer_line, 742 source_line_start = source_line_start, 743 length = 0 744 }; 745 } 746 747 region.length++; 748 749 if (added || removed) 750 { 751 var offset = (size_t)line.get_content_offset(); 752 var bytes = line.get_content(); 753 754 var pset = PatchSet.Patch() { 755 type = added ? PatchSet.Type.ADD : PatchSet.Type.REMOVE, 756 old_offset = offset, 757 new_offset = offset, 758 length = bytes.length 759 }; 760 761 if (added) 762 { 763 pset.old_offset = (size_t)((int64)pset.old_offset - d_doffset); 764 } 765 else 766 { 767 pset.new_offset = (size_t)((int64)pset.new_offset + d_doffset); 768 } 769 770 d_lines[buffer_line] = pset; 771 d_doffset += added ? (int64)bytes.length : -(int64)bytes.length; 772 } 773 774 if (i == lines.size - 1 && text.length > 0 && text[text.length - 1] == '\n') 775 { 776 text = text.slice(0, text.length - 1); 777 } 778 779 content.append(text); 780 buffer_line++; 781 } 782 783 if (lines.size != 0) 784 { 785 d_regions += region; 786 } 787 788 int line_hunk_start = iter.get_line(); 789 790 buffer.insert(ref iter, (string)content.data, -1); 791 792 d_old_lines.add_hunk(line_hunk_start, iter.get_line(), hunk, lines); 793 d_new_lines.add_hunk(line_hunk_start, iter.get_line(), hunk, lines); 794 d_sym_lines.add_hunk(line_hunk_start, iter.get_line(), hunk, lines); 795 796 for (var i = 0; i < lines.size; i++) 797 { 798 var line = lines[i]; 799 string? category = null; 800 801 switch (line.get_origin()) 802 { 803 case Ggit.DiffLineType.ADDITION: 804 category = "added"; 805 break; 806 case Ggit.DiffLineType.DELETION: 807 category = "removed"; 808 break; 809 } 810 811 if (category != null) 812 { 813 buffer.get_iter_at_line(out iter, line_hunk_start + i); 814 buffer.create_source_mark(null, category, iter); 815 } 816 } 817 818 this.thaw_notify(); 819 820 sensitive = true; 821 } 822} 823 824// ex:ts=4 noet 825