diff --git a/src/MainWindow.vala b/src/MainWindow.vala index bcf37aeb6..4a244c2ea 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -80,6 +80,7 @@ namespace Scratch { public const string ACTION_COLLAPSE_ALL_FOLDERS = "action-collapse-all-folders"; public const string ACTION_ORDER_FOLDERS = "action-order-folders"; public const string ACTION_GO_TO = "action-go-to"; + public const string ACTION_GO_TO_MATCHING = "action-go-to-matching"; public const string ACTION_SORT_LINES = "action-sort-lines"; public const string ACTION_NEW_TAB = "action-new-tab"; public const string ACTION_NEW_FROM_CLIPBOARD = "action-new-from-clipboard"; @@ -149,6 +150,7 @@ namespace Scratch { { ACTION_TOGGLE_SHOW_FIND, action_toggle_show_find, null, "false" }, { ACTION_TEMPLATES, action_templates }, { ACTION_GO_TO, action_go_to }, + { ACTION_GO_TO_MATCHING, action_go_to_matching }, { ACTION_SORT_LINES, action_sort_lines }, { ACTION_NEW_TAB, action_new_tab }, { ACTION_NEW_FROM_CLIPBOARD, action_new_tab_from_clipboard }, @@ -214,6 +216,7 @@ namespace Scratch { action_accelerators.set (ACTION_SAVE, "s"); action_accelerators.set (ACTION_SAVE_AS, "s"); action_accelerators.set (ACTION_GO_TO, "i"); + action_accelerators.set (ACTION_GO_TO_MATCHING, "i"); action_accelerators.set (ACTION_SORT_LINES, "F5"); action_accelerators.set (ACTION_NEW_TAB, "n"); action_accelerators.set (ACTION_DUPLICATE_TAB, "k" ); @@ -1371,6 +1374,15 @@ namespace Scratch { toolbar.format_bar.line_menubutton.active = true; } + private void action_go_to_matching () { + var doc = document_view.current_document; + if (doc == null) { + return; + } else { + doc.source_view.goto_matching (); + } + } + private void action_templates () { plugins.plugin_iface.template_manager.show_window (this); } diff --git a/src/Services/CommentToggler.vala b/src/Services/CommentToggler.vala index 56e721f44..cbfca86f7 100644 --- a/src/Services/CommentToggler.vala +++ b/src/Services/CommentToggler.vala @@ -21,7 +21,8 @@ public class Scratch.CommentToggler { private enum CommentType { NONE, LINE, - BLOCK + BLOCK, + EMPTY } private static CommentType get_comment_tags_for_lang (Gtk.SourceLanguage lang, @@ -73,6 +74,20 @@ public class Scratch.CommentToggler { return type != CommentType.NONE; } + public static bool line_is_commented_or_empty ( + Gtk.SourceBuffer buffer, + int line_index, + Gtk.SourceLanguage lang) { + + bool commented = false; + Gtk.TextIter start_iter, end_iter; + buffer.get_iter_at_line_index (out start_iter, line_index, 0); + buffer.get_iter_at_line_index (out end_iter, line_index, int.MAX); //Returns end of line iter + return lines_already_commented ( + buffer, start_iter, end_iter, 1, lang + ) != CommentType.NONE; + } + // Returns whether or not all lines within a region are already commented. // This is to detect whether to toggle comments on or off. If all lines are commented, then we want to remove // those comments. If only some are commented, then the user likely selected a chunk of code that already contained @@ -86,6 +101,10 @@ public class Scratch.CommentToggler { string start_tag, end_tag; var type = get_comment_tags_for_lang (lang, CommentType.BLOCK, out start_tag, out end_tag); var selection = buffer.get_slice (start, end, true); + if (selection.length == 0) { + return CommentType.EMPTY; + } + if (type == CommentType.BLOCK) { var regex_string = """^\s*(?:%s)+[\s\S]*(?:%s)+$"""; regex_string = regex_string.printf (Regex.escape_string (start_tag), Regex.escape_string (end_tag)); diff --git a/src/Widgets/SourceView.vala b/src/Widgets/SourceView.vala index d85e048e2..7234c7f16 100644 --- a/src/Widgets/SourceView.vala +++ b/src/Widgets/SourceView.vala @@ -610,6 +610,13 @@ namespace Scratch.Widgets { } menu.add (comment_item); + + if (true) {//TODO Check preceding char is bracket + var match_item = new Gtk.MenuItem.with_label (_("Goto Matching Bracket")) { + action_name = MainWindow.ACTION_PREFIX + MainWindow.ACTION_GO_TO_MATCHING + }; + menu.add (match_item); + } } menu.show_all (); @@ -704,5 +711,143 @@ namespace Scratch.Widgets { return Source.REMOVE; }); } + + private const string OPEN_BRACKETS = "{(["; + private const string CLOSE_BRACKETS = "})]"; + private bool is_open_bracket (unichar c, out unichar matching) { + var index = OPEN_BRACKETS.index_of_char (c); + if (index >= 0) { + matching = CLOSE_BRACKETS[index]; + return true; + } + + return false; + } + + private bool is_close_bracket (unichar c, out unichar matching) { + var index = CLOSE_BRACKETS.index_of_char (c); + if (index >= 0) { + matching = OPEN_BRACKETS[index]; + return true; + } + + return false; + } + + private uint get_indent_spaces (Gtk.TextIter t) { + var indent_iter = t.copy (); + if (indent_iter.backward_line ()) { + indent_iter.forward_line (); + }; + uint spaces = 0; + while (indent_iter.forward_char () && indent_iter.get_char ().isspace ()) { + spaces++; + } + + return spaces; + } + + // Return index of next uncommented, not empty line + private bool skip_commented_lines (out int new_index, int start_index, int step = 1) { + new_index = start_index; + while (CommentToggler.line_is_commented_or_empty ( + (Gtk.SourceBuffer)buffer, new_index, language + )) { + new_index += step; + } + + return new_index != start_index; + } + + public void goto_matching () { + uint start_indent = 0, end_indent = 0, same = 0; + int start_line = -1, end_line = -1, new_line = -1; + unichar c; + bool found = false; + var insert_mark = buffer.get_mark ("insert"); + Gtk.TextIter insert_iter, start_iter, end_iter; + buffer.get_iter_at_mark (out insert_iter, insert_mark); + start_line = insert_iter.get_line (); + if (skip_commented_lines (out new_line, start_line)) { + return; + } + + start_line++; // Change from index to visible number + insert_iter.backward_char (); + var insert_char = insert_iter.get_char (); + var end = insert_iter.copy (); + unichar matching; + if (is_open_bracket (insert_char, out matching)) { + start_indent = get_indent_spaces (insert_iter); + end_line = buffer.get_line_count (); + while (end.forward_char ()) { + if (end.starts_line () && skip_commented_lines (out new_line, end.get_line ())) { + end.set_line (new_line); + continue; + } + c = end.get_char (); + + if (is_open_bracket (c, out matching)) { + same++; + } else if (is_close_bracket (c, out matching)) { + if (same > 0) { + same--; + } else { + end_indent = get_indent_spaces (end); + end_line = end.get_line () + 1; + warning ("end line %i", end_line); + end.forward_char (); + buffer.place_cursor (end); + scroll_to_iter (end, 0.1, false, 0.0, 0.0); + found = true; + break; + } + } + } + } else if (is_close_bracket (insert_char, out matching)) { + start_indent = get_indent_spaces (insert_iter); + end_line = 0; + while (end.backward_char ()) { + if (end.ends_line () && skip_commented_lines (out new_line, end.get_line (), -1)) { + end.set_line (new_line); + end.forward_to_line_end (); + continue; + } + c = end.get_char (); + if (is_close_bracket (c, out matching)) { + same++; + } else if (is_open_bracket (c, out matching)) { + if (same > 0) { + same--; + } else { + end_indent = get_indent_spaces (end); + end_line = end.get_line () + 1; + end.forward_char (); + buffer.place_cursor (end); + scroll_to_iter (end, 0.1, false, 0.0, 0.0); + found = true; + break; + } + } + } + } else { + return; + } + + if (start_indent != end_indent || !found) { + var min_line = int.min (start_line, end_line); + var max_line = int.max (start_line, end_line); + var parent_window = get_toplevel () as Gtk.Window; + var dialog = new Granite.MessageDialog ( + found ? _("Matching bracket has incorrect indent") : _("No matching bracket found"), + _("You may have omitted a required bracket or inserted an extra bracket between lines %i and %i").printf (min_line, max_line), + new ThemedIcon ("dialog-warning"), + Gtk.ButtonsType.CLOSE + ); + dialog.transient_for = parent_window; + dialog.response.connect (dialog.destroy); + dialog.present (); + } + } } }