diff options
Diffstat (limited to 'scene/gui/code_edit.cpp')
-rw-r--r-- | scene/gui/code_edit.cpp | 1014 |
1 files changed, 730 insertions, 284 deletions
diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index ba1534ed5c..e7769f9372 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -46,46 +46,74 @@ void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { - set_gutter_width(main_gutter, get_row_height()); - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); - set_gutter_width(fold_gutter, get_row_height() / 1.2); + style_normal = get_theme_stylebox(SNAME("normal")); - breakpoint_color = get_theme_color("breakpoint_color"); - breakpoint_icon = get_theme_icon("breakpoint"); + font = get_theme_font(SNAME("font")); + font_size = get_theme_font_size(SNAME("font_size")); - bookmark_color = get_theme_color("bookmark_color"); - bookmark_icon = get_theme_icon("bookmark"); + line_spacing = get_theme_constant(SNAME("line_spacing")); - executing_line_color = get_theme_color("executing_line_color"); - executing_line_icon = get_theme_icon("executing_line"); + set_gutter_width(main_gutter, get_line_height()); + set_gutter_width(line_number_gutter, (line_number_digits + 1) * font->get_char_size('0', 0, font_size).width); + set_gutter_width(fold_gutter, get_line_height() / 1.2); - line_number_color = get_theme_color("line_number_color"); + breakpoint_color = get_theme_color(SNAME("breakpoint_color")); + breakpoint_icon = get_theme_icon(SNAME("breakpoint")); - folding_color = get_theme_color("code_folding_color"); - can_fold_icon = get_theme_icon("can_fold"); - folded_icon = get_theme_icon("folded"); + bookmark_color = get_theme_color(SNAME("bookmark_color")); + bookmark_icon = get_theme_icon(SNAME("bookmark")); - code_completion_max_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x').x; - code_completion_max_lines = get_theme_constant("completion_lines"); - code_completion_scroll_width = get_theme_constant("completion_scroll_width"); - code_completion_scroll_color = get_theme_color("completion_scroll_color"); - code_completion_background_color = get_theme_color("completion_background_color"); - code_completion_selected_color = get_theme_color("completion_selected_color"); - code_completion_existing_color = get_theme_color("completion_existing_color"); + executing_line_color = get_theme_color(SNAME("executing_line_color")); + executing_line_icon = get_theme_icon(SNAME("executing_line")); + + line_number_color = get_theme_color(SNAME("line_number_color")); + + folding_color = get_theme_color(SNAME("code_folding_color")); + can_fold_icon = get_theme_icon(SNAME("can_fold")); + folded_icon = get_theme_icon(SNAME("folded")); + + code_completion_max_width = get_theme_constant(SNAME("completion_max_width")); + code_completion_max_lines = get_theme_constant(SNAME("completion_lines")); + code_completion_scroll_width = get_theme_constant(SNAME("completion_scroll_width")); + code_completion_scroll_color = get_theme_color(SNAME("completion_scroll_color")); + code_completion_background_color = get_theme_color(SNAME("completion_background_color")); + code_completion_selected_color = get_theme_color(SNAME("completion_selected_color")); + code_completion_existing_color = get_theme_color(SNAME("completion_existing_color")); + + line_length_guideline_color = get_theme_color(SNAME("line_length_guideline_color")); } break; case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); + const Size2 size = get_size(); const bool caret_visible = is_caret_visible(); const bool rtl = is_layout_rtl(); - const int row_height = get_row_height(); + const int row_height = get_line_height(); + + if (line_length_guideline_columns.size() > 0) { + const int xmargin_beg = style_normal->get_margin(SIDE_LEFT) + get_total_gutter_width(); + const int xmargin_end = size.width - style_normal->get_margin(SIDE_RIGHT) - (is_drawing_minimap() ? get_minimap_width() : 0); + const int char_size = (int)font->get_char_size('0', 0, font_size).width; + + for (int i = 0; i < line_length_guideline_columns.size(); i++) { + const int xoffset = xmargin_beg + char_size * (int)line_length_guideline_columns[i] - get_h_scroll(); + if (xoffset > xmargin_beg && xoffset < xmargin_end) { + Color guideline_color = (i == 0) ? line_length_guideline_color : line_length_guideline_color * Color(1, 1, 1, 0.5); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - xoffset, 0), Point2(size.width - xoffset, size.height), guideline_color); + continue; + } + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(xoffset, 0), Point2(xoffset, size.height), guideline_color); + } + } + } bool code_completion_below = false; if (caret_visible && code_completion_active && code_completion_options.size() > 0) { - Ref<StyleBox> csb = get_theme_stylebox("completion"); + Ref<StyleBox> csb = get_theme_stylebox(SNAME("completion")); const int code_completion_options_count = code_completion_options.size(); const int lines = MIN(code_completion_options_count, code_completion_max_lines); - const int icon_hsep = get_theme_constant("hseparation", "ItemList"); + const int icon_hsep = get_theme_constant(SNAME("hseparation"), SNAME("ItemList")); const Size2 icon_area_size(row_height, row_height); code_completion_rect.size.width = code_completion_longest_line + icon_hsep + icon_area_size.width + 2; @@ -94,14 +122,14 @@ void CodeEdit::_notification(int p_what) { const Point2 caret_pos = get_caret_draw_pos(); const int total_height = csb->get_minimum_size().y + code_completion_rect.size.height; if (caret_pos.y + row_height + total_height > get_size().height) { - code_completion_rect.position.y = (caret_pos.y - total_height - row_height) + cache.line_spacing; + code_completion_rect.position.y = (caret_pos.y - total_height - row_height) + line_spacing; } else { - code_completion_rect.position.y = caret_pos.y + (cache.line_spacing / 2.0f); + code_completion_rect.position.y = caret_pos.y + (line_spacing / 2.0f); code_completion_below = true; } const int scroll_width = code_completion_options_count > code_completion_max_lines ? code_completion_scroll_width : 0; - const int code_completion_base_width = cache.font->get_string_size(code_completion_base).width; + const int code_completion_base_width = font->get_string_size(code_completion_base).width; if (caret_pos.x - code_completion_base_width + code_completion_rect.size.width + scroll_width > get_size().width) { code_completion_rect.position.x = get_size().width - code_completion_rect.size.width - scroll_width; } else { @@ -123,7 +151,7 @@ void CodeEdit::_notification(int p_what) { Ref<TextLine> tl; tl.instantiate(); - tl->add_string(code_completion_options[l].display, cache.font, cache.font_size); + tl->add_string(code_completion_options[l].display, font, font_size); int yofs = (row_height - tl->get_size().y) / 2; Point2 title_pos(code_completion_rect.position.x, code_completion_rect.position.y + i * row_height + yofs); @@ -162,59 +190,65 @@ void CodeEdit::_notification(int p_what) { /* Code hint */ if (caret_visible && code_hint != "" && (!code_completion_active || (code_completion_below != code_hint_draw_below))) { - const Ref<Font> font = cache.font; - const int font_height = font->get_height(cache.font_size); - Ref<StyleBox> sb = get_theme_stylebox("panel", "TooltipPanel"); - Color font_color = get_theme_color("font_color", "TooltipLabel"); + const int font_height = font->get_height(font_size); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel"), SNAME("TooltipPanel")); + Color font_color = get_theme_color(SNAME("font_color"), SNAME("TooltipLabel")); Vector<String> code_hint_lines = code_hint.split("\n"); int line_count = code_hint_lines.size(); int max_width = 0; for (int i = 0; i < line_count; i++) { - max_width = MAX(max_width, font->get_string_size(code_hint_lines[i], cache.font_size).x); + max_width = MAX(max_width, font->get_string_size(code_hint_lines[i], font_size).x); } - Size2 minsize = sb->get_minimum_size() + Size2(max_width, line_count * font_height + (cache.line_spacing * line_count - 1)); + Size2 minsize = sb->get_minimum_size() + Size2(max_width, line_count * font_height + (line_spacing * line_count - 1)); - int offset = font->get_string_size(code_hint_lines[0].substr(0, code_hint_lines[0].find(String::chr(0xFFFF))), cache.font_size).x; + int offset = font->get_string_size(code_hint_lines[0].substr(0, code_hint_lines[0].find(String::chr(0xFFFF))), font_size).x; if (code_hint_xpos == -0xFFFF) { code_hint_xpos = get_caret_draw_pos().x - offset; } Point2 hint_ofs = Vector2(code_hint_xpos, get_caret_draw_pos().y); if (code_hint_draw_below) { - hint_ofs.y += cache.line_spacing / 2.0f; + hint_ofs.y += line_spacing / 2.0f; } else { - hint_ofs.y -= (minsize.y + row_height) - cache.line_spacing; + hint_ofs.y -= (minsize.y + row_height) - line_spacing; } draw_style_box(sb, Rect2(hint_ofs, minsize)); - int line_spacing = 0; + int yofs = 0; for (int i = 0; i < line_count; i++) { const String &line = code_hint_lines[i]; int begin = 0; int end = 0; if (line.find(String::chr(0xFFFF)) != -1) { - begin = font->get_string_size(line.substr(0, line.find(String::chr(0xFFFF))), cache.font_size).x; - end = font->get_string_size(line.substr(0, line.rfind(String::chr(0xFFFF))), cache.font_size).x; + begin = font->get_string_size(line.substr(0, line.find(String::chr(0xFFFF))), font_size).x; + end = font->get_string_size(line.substr(0, line.rfind(String::chr(0xFFFF))), font_size).x; } - Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent() + font_height * i + line_spacing); + Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent() + font_height * i + yofs); round_ofs = round_ofs.round(); - draw_string(font, round_ofs, line.replace(String::chr(0xFFFF), ""), HALIGN_LEFT, -1, cache.font_size, font_color); + draw_string(font, round_ofs, line.replace(String::chr(0xFFFF), ""), HALIGN_LEFT, -1, font_size, font_color); if (end > 0) { - Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font_height + font_height * i + line_spacing - 1); - draw_line(b, b + Vector2(end - begin, 0), font_color); + // Draw an underline for the currently edited function parameter. + const Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font_height + font_height * i + line_spacing); + draw_line(b, b + Vector2(end - begin, 0), font_color, 2); + + // Draw a translucent text highlight as well. + const Rect2 highlight_rect = Rect2( + hint_ofs + sb->get_offset() + Vector2(begin, 0), + Vector2(end - begin, font_height)); + draw_rect(highlight_rect, font_color * Color(1, 1, 1, 0.2)); } - line_spacing += cache.line_spacing; + yofs += line_spacing; } } } break; } } -void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { +void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventMouseButton> mb = p_gui_input; if (mb.is_valid()) { @@ -242,7 +276,7 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } break; case MOUSE_BUTTON_LEFT: { - code_completion_current_selected = CLAMP(code_completion_line_ofs + (mb->get_position().y - code_completion_rect.position.y) / get_row_height(), 0, code_completion_options.size() - 1); + code_completion_current_selected = CLAMP(code_completion_line_ofs + (mb->get_position().y - code_completion_rect.position.y) / get_line_height(), 0, code_completion_options.size() - 1); if (mb->is_double_click()) { confirm_code_completion(); } @@ -262,14 +296,15 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { mpos.x = get_size().x - mpos.x; } - int line, col; - _get_mouse_pos(Point2i(mpos.x, mpos.y), line, col); + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int line = pos.y; + int col = pos.x; if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (is_line_folded(line)) { - int wrap_index = get_line_wrap_index_at_col(line, col); - if (wrap_index == times_line_wraps(line)) { - int eol_icon_width = cache.folded_eol_icon->get_width(); + int wrap_index = get_line_wrap_index_at_column(line, col); + if (wrap_index == get_line_wrap_count(line)) { + int eol_icon_width = folded_eol_icon->get_width(); int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll(); if (mpos.x > left_margin && mpos.x <= left_margin + eol_icon_width + 3) { unfold_line(line); @@ -278,13 +313,67 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } } + } else { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { + if (mb->is_command_pressed() && symbol_lookup_word != String()) { + Vector2i mpos = mb->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int line = pos.y; + int col = pos.x; + + emit_signal(SNAME("symbol_lookup"), symbol_lookup_word, line, col); + return; + } + } + } + } + + Ref<InputEventMouseMotion> mm = p_gui_input; + if (mm.is_valid()) { + Vector2i mpos = mm->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + + if (symbol_lookup_on_click_enabled) { + if (mm->is_command_pressed() && mm->get_button_mask() == 0 && !is_dragging_cursor()) { + symbol_lookup_new_word = get_word_at_pos(mpos); + if (symbol_lookup_new_word != symbol_lookup_word) { + emit_signal(SNAME("symbol_validate"), symbol_lookup_new_word); + } + } else { + set_symbol_lookup_word_as_valid(false); + } } } Ref<InputEventKey> k = p_gui_input; bool update_code_completion = false; if (!k.is_valid()) { - TextEdit::_gui_input(p_gui_input); + TextEdit::gui_input(p_gui_input); + return; + } + + /* Ctrl + Hover symbols */ +#ifdef OSX_ENABLED + if (k->get_keycode() == KEY_META) { +#else + if (k->get_keycode() == KEY_CTRL) { +#endif + if (symbol_lookup_on_click_enabled) { + if (k->is_pressed() && !is_dragging_cursor()) { + symbol_lookup_new_word = get_word_at_pos(get_local_mouse_pos()); + if (symbol_lookup_new_word != symbol_lookup_word) { + emit_signal(SNAME("symbol_validate"), symbol_lookup_new_word); + } + } else { + set_symbol_lookup_word_as_valid(false); + } + } return; } @@ -361,7 +450,7 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } if (k->is_action("ui_text_backspace", true)) { backspace(); - _filter_code_completion_candidates(); + _filter_code_completion_candidates_impl(); accept_event(); return; } @@ -430,25 +519,31 @@ void CodeEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { set_code_hint(""); } - TextEdit::_gui_input(p_gui_input); + TextEdit::gui_input(p_gui_input); if (update_code_completion) { - _filter_code_completion_candidates(); + _filter_code_completion_candidates_impl(); } } +/* General overrides */ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { - if ((code_completion_active && code_completion_rect.has_point(p_pos)) || (is_readonly() && (!is_selecting_enabled() || get_line_count() == 0))) { + if (symbol_lookup_word != String()) { + return CURSOR_POINTING_HAND; + } + + if ((code_completion_active && code_completion_rect.has_point(p_pos)) || (!is_editable() && (!is_selecting_enabled() || get_line_count() == 0))) { return CURSOR_ARROW; } - int line, col; - _get_mouse_pos(p_pos, line, col); + Point2i pos = get_line_column_at_pos(p_pos); + int line = pos.y; + int col = pos.x; if (is_line_folded(line)) { - int wrap_index = get_line_wrap_index_at_col(line, col); - if (wrap_index == times_line_wraps(line)) { - int eol_icon_width = cache.folded_eol_icon->get_width(); + int wrap_index = get_line_wrap_index_at_column(line, col); + if (wrap_index == get_line_wrap_count(line)) { + int eol_icon_width = folded_eol_icon->get_width(); int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll(); if (p_pos.x > left_margin && p_pos.x <= left_margin + eol_icon_width + 3) { return CURSOR_POINTING_HAND; @@ -459,6 +554,118 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { return TextEdit::get_cursor_shape(p_pos); } +/* Text manipulation */ + +// Overridable actions +void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { + bool had_selection = has_selection(); + if (had_selection) { + begin_complex_operation(); + delete_selection(); + } + + // Remove the old character if in overtype mode and no selection. + if (is_overtype_mode_enabled() && !had_selection) { + begin_complex_operation(); + + /* Make sure we don't try and remove empty space. */ + if (get_caret_column() < get_line(get_caret_line()).length()) { + remove_text(get_caret_line(), get_caret_column(), get_caret_line(), get_caret_column() + 1); + } + } + + const char32_t chr[2] = { (char32_t)p_unicode, 0 }; + + if (auto_brace_completion_enabled) { + int cl = get_caret_line(); + int cc = get_caret_column(); + int caret_move_offset = 1; + + int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; + + if (has_string_delimiter(chr) && cc > 0 && _is_char(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + insert_text_at_caret(chr); + } else if (cc < get_line(cl).length() && _is_char(get_line(cl)[cc])) { + insert_text_at_caret(chr); + } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { + caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); + } else if (is_in_comment(cl, cc) != -1 || (is_in_string(cl, cc) != -1 && has_string_delimiter(chr))) { + insert_text_at_caret(chr); + } else { + insert_text_at_caret(chr); + + int pre_brace_pair = _get_auto_brace_pair_open_at_pos(cl, cc + 1); + if (pre_brace_pair != -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); + } + } + set_caret_column(cc + caret_move_offset); + } else { + insert_text_at_caret(chr); + } + + if ((is_overtype_mode_enabled() && !had_selection) || (had_selection)) { + end_complex_operation(); + } +} + +void CodeEdit::_backspace_internal() { + if (!is_editable()) { + return; + } + + int cc = get_caret_column(); + int cl = get_caret_line(); + + if (cc == 0 && cl == 0) { + return; + } + + if (has_selection()) { + delete_selection(); + return; + } + + if (cl > 0 && _is_line_hidden(cl - 1)) { + unfold_line(get_caret_line() - 1); + } + + int prev_line = cc ? cl : cl - 1; + int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + + merge_gutters(prev_line, cl); + + if (auto_brace_completion_enabled && cc > 0) { + int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (idx != -1) { + prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + + if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { + remove_text(prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length()); + } else { + remove_text(prev_line, prev_column, cl, cc); + } + set_caret_line(prev_line, false, true); + set_caret_column(prev_column); + return; + } + } + + // For space indentation we need to do a simple unindent if there are no chars to the left, acting in the + // same way as tabs. + if (indent_using_spaces && cc != 0) { + if (get_first_non_whitespace_column(cl) >= cc) { + prev_column = cc - _calculate_spaces_till_next_left_indent(cc); + prev_line = cl; + } + } + + remove_text(prev_line, prev_column, cl, cc); + + set_caret_line(prev_line, false, true); + set_caret_column(prev_column); +} + /* Indent management */ void CodeEdit::set_indent_size(const int p_size) { ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); @@ -517,28 +724,28 @@ TypedArray<String> CodeEdit::get_auto_indent_prefixes() const { } void CodeEdit::do_indent() { - if (is_readonly()) { + if (!is_editable()) { return; } - if (is_selection_active()) { + if (has_selection()) { indent_lines(); return; } if (!indent_using_spaces) { - _insert_text_at_cursor("\t"); + insert_text_at_caret("\t"); return; } - int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor_get_column()); + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column()); if (spaces_to_add > 0) { - _insert_text_at_cursor(String(" ").repeat(spaces_to_add)); + insert_text_at_caret(String(" ").repeat(spaces_to_add)); } } void CodeEdit::indent_lines() { - if (is_readonly()) { + if (!is_editable()) { return; } @@ -548,9 +755,9 @@ void CodeEdit::indent_lines() { /* Default is 1 for tab indentation. */ int selection_offset = 1; - int start_line = cursor_get_line(); + int start_line = get_caret_line(); int end_line = start_line; - if (is_selection_active()) { + if (has_selection()) { start_line = get_selection_from_line(); end_line = get_selection_to_line(); @@ -563,7 +770,7 @@ void CodeEdit::indent_lines() { for (int i = start_line; i <= end_line; i++) { const String line_text = get_line(i); - if (line_text.size() == 0 && is_selection_active()) { + if (line_text.size() == 0 && has_selection()) { continue; } @@ -580,32 +787,32 @@ void CodeEdit::indent_lines() { } /* Fix selection and caret being off after shifting selection right.*/ - if (is_selection_active()) { + if (has_selection()) { select(start_line, get_selection_from_column() + selection_offset, get_selection_to_line(), get_selection_to_column() + selection_offset); } - cursor_set_column(cursor_get_column() + selection_offset, false); + set_caret_column(get_caret_column() + selection_offset, false); end_complex_operation(); } void CodeEdit::do_unindent() { - if (is_readonly()) { + if (!is_editable()) { return; } - int cc = cursor_get_column(); + int cc = get_caret_column(); - if (is_selection_active() || cc <= 0) { + if (has_selection() || cc <= 0) { unindent_lines(); return; } - int cl = cursor_get_line(); + int cl = get_caret_line(); const String &line = get_line(cl); if (line[cc - 1] == '\t') { - _remove_text(cl, cc - 1, cl, cc); - cursor_set_column(MAX(0, cc - 1)); + remove_text(cl, cc - 1, cl, cc); + set_caret_column(MAX(0, cc - 1)); return; } @@ -621,13 +828,13 @@ void CodeEdit::do_unindent() { break; } } - _remove_text(cl, cc - spaces_to_remove, cl, cc); - cursor_set_column(MAX(0, cc - spaces_to_remove)); + remove_text(cl, cc - spaces_to_remove, cl, cc); + set_caret_column(MAX(0, cc - spaces_to_remove)); } } void CodeEdit::unindent_lines() { - if (is_readonly()) { + if (!is_editable()) { return; } @@ -638,11 +845,11 @@ void CodeEdit::unindent_lines() { /* therefore we just remember initial values and at the end of the operation offset them by number of removed characters. */ int removed_characters = 0; int initial_selection_end_column = 0; - int initial_cursor_column = cursor_get_column(); + int initial_cursor_column = get_caret_column(); - int start_line = cursor_get_line(); + int start_line = get_caret_line(); int end_line = start_line; - if (is_selection_active()) { + if (has_selection()) { start_line = get_selection_from_line(); end_line = get_selection_to_line(); @@ -685,7 +892,7 @@ void CodeEdit::unindent_lines() { } } - if (is_selection_active()) { + if (has_selection()) { /* Fix selection being off by one on the first line. */ if (first_line_edited) { select(get_selection_from_line(), get_selection_from_column() - removed_characters, get_selection_to_line(), initial_selection_end_column); @@ -696,7 +903,7 @@ void CodeEdit::unindent_lines() { select(get_selection_from_line(), get_selection_from_column(), get_selection_to_line(), initial_selection_end_column - removed_characters); } } - cursor_set_column(initial_cursor_column - removed_characters, false); + set_caret_column(initial_cursor_column - removed_characters, false); end_complex_operation(); } @@ -713,41 +920,13 @@ int CodeEdit::_calculate_spaces_till_next_right_indent(int p_column) const { return indent_size - p_column % indent_size; } -/* TODO: remove once brace completion is refactored. */ -static char32_t _get_right_pair_symbol(char32_t c) { - if (c == '"') { - return '"'; - } - if (c == '\'') { - return '\''; - } - if (c == '(') { - return ')'; - } - if (c == '[') { - return ']'; - } - if (c == '{') { - return '}'; - } - return 0; -} - -static bool _is_pair_left_symbol(char32_t c) { - return c == '"' || - c == '\'' || - c == '(' || - c == '[' || - c == '{'; -} - void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { - if (is_readonly()) { + if (!is_editable()) { return; } - const int cc = cursor_get_column(); - const int cl = cursor_get_line(); + const int cc = get_caret_column(); + const int cl = get_caret_line(); const String line = get_line(cl); String ins = "\n"; @@ -803,16 +982,15 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { if (should_indent) { ins += indent_text; - /* TODO: Change when brace completion is refactored. */ - char32_t closing_char = _get_right_pair_symbol(indent_char); - if (closing_char != 0 && closing_char == line[cc]) { + String closing_pair = get_auto_brace_completion_close_key(String::chr(indent_char)); + if (!closing_pair.is_empty() && line.find(closing_pair, cc) == cc) { /* No need to move the brace below if we are not taking the text with us. */ if (p_split_current_line) { brace_indent = true; - ins += "\n" + ins.substr(1, ins.length() - 2); + ins += "\n" + ins.substr(indent_text.size(), ins.length() - 2); } else { brace_indent = false; - ins = "\n" + ins.substr(1, ins.length() - 2); + ins = "\n" + ins.substr(indent_text.size(), ins.length() - 2); } } } @@ -824,76 +1002,114 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { if (!p_split_current_line) { if (p_above) { if (cl > 0) { - cursor_set_line(cl - 1, false); - cursor_set_column(get_line(cursor_get_line()).length()); + set_caret_line(cl - 1, false); + set_caret_column(get_line(get_caret_line()).length()); } else { - cursor_set_column(0); + set_caret_column(0); first_line = true; } } else { - cursor_set_column(line.length()); + set_caret_column(line.length()); } } - insert_text_at_cursor(ins); + insert_text_at_caret(ins); if (first_line) { - cursor_set_line(0); + set_caret_line(0); } else if (brace_indent) { - cursor_set_line(cursor_get_line() - 1, false); - cursor_set_column(get_line(cursor_get_line()).length()); + set_caret_line(get_caret_line() - 1, false); + set_caret_column(get_line(get_caret_line()).length()); } end_complex_operation(); } -void CodeEdit::backspace() { - if (is_readonly()) { - return; - } +/* Auto brace completion */ +void CodeEdit::set_auto_brace_completion_enabled(bool p_enabled) { + auto_brace_completion_enabled = p_enabled; +} - int cc = cursor_get_column(); - int cl = cursor_get_line(); +bool CodeEdit::is_auto_brace_completion_enabled() const { + return auto_brace_completion_enabled; +} - if (cc == 0 && cl == 0) { - return; - } +void CodeEdit::set_highlight_matching_braces_enabled(bool p_enabled) { + highlight_matching_braces_enabled = p_enabled; + update(); +} - if (is_selection_active()) { - delete_selection(); - return; +bool CodeEdit::is_highlight_matching_braces_enabled() const { + return highlight_matching_braces_enabled; +} + +void CodeEdit::add_auto_brace_completion_pair(const String &p_open_key, const String &p_close_key) { + ERR_FAIL_COND_MSG(p_open_key.is_empty(), "auto brace completion open key cannot be empty"); + ERR_FAIL_COND_MSG(p_close_key.is_empty(), "auto brace completion close key cannot be empty"); + + for (int i = 0; i < p_open_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_open_key[i]), "auto brace completion open key must be a symbol"); + } + for (int i = 0; i < p_close_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_close_key[i]), "auto brace completion close key must be a symbol"); } - if (cl > 0 && is_line_hidden(cl - 1)) { - unfold_line(cursor_get_line() - 1); + int at = 0; + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + ERR_FAIL_COND_MSG(auto_brace_completion_pairs[i].open_key == p_open_key, "auto brace completion open key '" + p_open_key + "' already exists."); + if (p_open_key.length() < auto_brace_completion_pairs[i].open_key.length()) { + at++; + } } - int prev_line = cc ? cl : cl - 1; - int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + BracePair brace_pair; + brace_pair.open_key = p_open_key; + brace_pair.close_key = p_close_key; + auto_brace_completion_pairs.insert(at, brace_pair); +} - merge_gutters(cl, prev_line); +void CodeEdit::set_auto_brace_completion_pairs(const Dictionary &p_auto_brace_completion_pairs) { + auto_brace_completion_pairs.clear(); - /* TODO: Change when brace completion is refactored. */ - if (auto_brace_completion_enabled && cc > 0 && _is_pair_left_symbol(get_line(cl)[cc - 1])) { - _consume_backspace_for_pair_symbol(prev_line, prev_column); - cursor_set_line(prev_line, false, true); - cursor_set_column(prev_column); - return; + Array keys = p_auto_brace_completion_pairs.keys(); + for (int i = 0; i < keys.size(); i++) { + add_auto_brace_completion_pair(keys[i], p_auto_brace_completion_pairs[keys[i]]); } +} - /* For space indentation we need to do a simple unindent if there are no chars to the left, acting in the */ - /* same way as tabs. */ - if (indent_using_spaces && cc != 0) { - if (get_first_non_whitespace_column(cl) > cc) { - prev_column = cc - _calculate_spaces_till_next_left_indent(cc); - prev_line = cl; +Dictionary CodeEdit::get_auto_brace_completion_pairs() const { + Dictionary brace_pairs; + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + brace_pairs[auto_brace_completion_pairs[i].open_key] = auto_brace_completion_pairs[i].close_key; + } + return brace_pairs; +} + +bool CodeEdit::has_auto_brace_completion_open_key(const String &p_open_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].open_key == p_open_key) { + return true; } } + return false; +} - _remove_text(prev_line, prev_column, cl, cc); +bool CodeEdit::has_auto_brace_completion_close_key(const String &p_close_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].close_key == p_close_key) { + return true; + } + } + return false; +} - cursor_set_line(prev_line, false, true); - cursor_set_column(prev_column); +String CodeEdit::get_auto_brace_completion_close_key(const String &p_open_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].open_key == p_open_key) { + return auto_brace_completion_pairs[i].close_key; + } + } + return String(); } /* Main Gutter */ @@ -962,6 +1178,8 @@ void CodeEdit::_main_gutter_draw_callback(int p_line, int p_gutter, const Rect2 // Breakpoints void CodeEdit::set_line_as_breakpoint(int p_line, bool p_breakpointed) { + ERR_FAIL_INDEX(p_line, get_line_count()); + int mask = get_line_gutter_metadata(p_line, main_gutter); set_line_gutter_metadata(p_line, main_gutter, p_breakpointed ? mask | MAIN_GUTTER_BREAKPOINT : mask & ~MAIN_GUTTER_BREAKPOINT); if (p_breakpointed) { @@ -969,7 +1187,7 @@ void CodeEdit::set_line_as_breakpoint(int p_line, bool p_breakpointed) { } else if (breakpointed_lines.has(p_line)) { breakpointed_lines.erase(p_line); } - emit_signal("breakpoint_toggled", p_line); + emit_signal(SNAME("breakpoint_toggled"), p_line); update(); } @@ -1075,8 +1293,8 @@ void CodeEdit::_line_number_draw_callback(int p_line, int p_gutter, const Rect2 String fc = TS->format_number(String::num(p_line + 1).lpad(line_number_digits, line_number_padding)); Ref<TextLine> tl; tl.instantiate(); - tl->add_string(fc, cache.font, cache.font_size); - int yofs = p_region.position.y + (get_row_height() - tl->get_size().y) / 2; + tl->add_string(fc, font, font_size); + int yofs = p_region.position.y + (get_line_height() - tl->get_size().y) / 2; Color number_color = get_line_gutter_item_color(p_line, line_number_gutter); if (number_color == Color(1, 1, 1)) { number_color = line_number_color; @@ -1116,7 +1334,7 @@ void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_regi /* Line Folding */ void CodeEdit::set_line_folding_enabled(bool p_enabled) { line_folding_enabled = p_enabled; - set_hiding_enabled(p_enabled); + _set_hiding_enabled(p_enabled); } bool CodeEdit::is_line_folding_enabled() const { @@ -1133,7 +1351,7 @@ bool CodeEdit::can_fold_line(int p_line) const { return false; } - if (is_line_hidden(p_line) || is_line_folded(p_line)) { + if (_is_line_hidden(p_line) || is_line_folded(p_line)) { return false; } @@ -1182,15 +1400,21 @@ void CodeEdit::fold_line(int p_line) { } /* Find the last line to be hidden. */ - int end_line = get_line_count(); + const int line_count = get_line_count() - 1; + int end_line = line_count; int in_comment = is_in_comment(p_line); int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; if (in_string != -1 || in_comment != -1) { end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; - /* End line is the same therefore we have a block. */ + /* End line is the same therefore we have a block of single line delimiters. */ if (end_line == p_line) { - for (int i = p_line + 1; i < get_line_count(); i++) { + for (int i = p_line + 1; i <= line_count; i++) { + if (i == line_count) { + end_line = line_count; + break; + } + if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { end_line = i - 1; break; @@ -1199,45 +1423,58 @@ void CodeEdit::fold_line(int p_line) { } } else { int start_indent = get_indent_level(p_line); - for (int i = p_line + 1; i < get_line_count(); i++) { + for (int i = p_line + 1; i <= line_count; i++) { if (get_line(p_line).strip_edges().size() == 0 || is_in_string(i) != -1 || is_in_comment(i) != -1) { end_line = i; continue; } - if (get_indent_level(i) <= start_indent && get_line(i).strip_edges().size() != 0) { + if (i == line_count) { + /* Do not fold empty last line of script if any */ + end_line = i; + if (get_line(i).strip_edges().size() == 0) { + end_line--; + } + break; + } + + if ((get_indent_level(i) <= start_indent && get_line(i).strip_edges().size() != 0)) { + /* Keep an empty line unfolded if any */ end_line = i - 1; + if (get_line(i - 1).strip_edges().size() == 0 && i - 2 > p_line) { + end_line = i - 2; + } break; } } } for (int i = p_line + 1; i <= end_line; i++) { - set_line_as_hidden(i, true); + _set_line_as_hidden(i, true); } /* Fix selection. */ - if (is_selection_active()) { - if (is_line_hidden(get_selection_from_line()) && is_line_hidden(get_selection_to_line())) { + if (has_selection()) { + if (_is_line_hidden(get_selection_from_line()) && _is_line_hidden(get_selection_to_line())) { deselect(); - } else if (is_line_hidden(get_selection_from_line())) { + } else if (_is_line_hidden(get_selection_from_line())) { select(p_line, 9999, get_selection_to_line(), get_selection_to_column()); - } else if (is_line_hidden(get_selection_to_line())) { + } else if (_is_line_hidden(get_selection_to_line())) { select(get_selection_from_line(), get_selection_from_column(), p_line, 9999); } } /* Reset caret. */ - if (is_line_hidden(cursor_get_line())) { - cursor_set_line(p_line, false, false); - cursor_set_column(get_line(p_line).length(), false); + if (_is_line_hidden(get_caret_line())) { + set_caret_line(p_line, false, false); + set_caret_column(get_line(p_line).length(), false); } update(); } void CodeEdit::unfold_line(int p_line) { ERR_FAIL_INDEX(p_line, get_line_count()); - if (!is_line_folded(p_line) && !is_line_hidden(p_line)) { + if (!is_line_folded(p_line) && !_is_line_hidden(p_line)) { return; } @@ -1250,10 +1487,10 @@ void CodeEdit::unfold_line(int p_line) { fold_start = is_line_folded(fold_start) ? fold_start : p_line; for (int i = fold_start + 1; i < get_line_count(); i++) { - if (!is_line_hidden(i)) { + if (!_is_line_hidden(i)) { break; } - set_line_as_hidden(i, false); + _set_line_as_hidden(i, false); } update(); } @@ -1266,7 +1503,7 @@ void CodeEdit::fold_all_lines() { } void CodeEdit::unfold_all_lines() { - unhide_all_lines(); + _unhide_all_lines(); } void CodeEdit::toggle_foldable_line(int p_line) { @@ -1280,7 +1517,7 @@ void CodeEdit::toggle_foldable_line(int p_line) { bool CodeEdit::is_line_folded(int p_line) const { ERR_FAIL_INDEX_V(p_line, get_line_count(), false); - return p_line + 1 < get_line_count() && !is_line_hidden(p_line) && is_line_hidden(p_line + 1); + return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1); } TypedArray<int> CodeEdit::get_folded_lines() const { @@ -1385,7 +1622,8 @@ Point2 CodeEdit::get_delimiter_start_position(int p_line, int p_column) const { } /* Region was found on this line and is not a multiline continuation. */ - if (start_position.x != -1 && start_position.x != get_line(p_line).length() + 1) { + int line_length = get_line(p_line).length(); + if (start_position.x != -1 && line_length > 0 && start_position.x != line_length + 1) { start_position.y = p_line; return start_position; } @@ -1404,7 +1642,8 @@ Point2 CodeEdit::get_delimiter_start_position(int p_line, int p_column) const { start_position.x = delimiter_cache[i].back()->key(); /* Make sure it's not a multiline continuation. */ - if (start_position.x != get_line(i).length() + 1) { + line_length = get_line(i).length(); + if (line_length > 0 && start_position.x != line_length + 1) { break; } } @@ -1504,11 +1743,11 @@ String CodeEdit::get_text_for_code_completion() const { for (int i = 0; i < text_size; i++) { String line = get_line(i); - if (i == cursor_get_line()) { - completion_text += line.substr(0, cursor_get_column()); + if (i == get_caret_line()) { + completion_text += line.substr(0, get_caret_column()); /* Not unicode, represents the caret. */ completion_text += String::chr(0xFFFF); - completion_text += line.substr(cursor_get_column(), line.size()); + completion_text += line.substr(get_caret_column(), line.size()); } else { completion_text += line; } @@ -1521,9 +1760,7 @@ String CodeEdit::get_text_for_code_completion() const { } void CodeEdit::request_code_completion(bool p_force) { - ScriptInstance *si = get_script_instance(); - if (si && si->has_method("_request_code_completion")) { - si->call("_request_code_completion", p_force); + if (GDVIRTUAL_CALL(_request_code_completion, p_force)) { return; } @@ -1551,17 +1788,17 @@ void CodeEdit::request_code_completion(bool p_force) { } if (p_force) { - emit_signal("request_code_completion"); + emit_signal(SNAME("request_code_completion")); return; } - String line = get_line(cursor_get_line()); - int ofs = CLAMP(cursor_get_column(), 0, line.length()); + String line = get_line(get_caret_line()); + int ofs = CLAMP(get_caret_column(), 0, line.length()); - if (ofs > 0 && (is_in_string(cursor_get_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(String::chr(line[ofs - 1])))) { - emit_signal("request_code_completion"); + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(String::chr(line[ofs - 1])))) { + emit_signal(SNAME("request_code_completion")); } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(String::chr(line[ofs - 2]))) { - emit_signal("request_code_completion"); + emit_signal(SNAME("request_code_completion")); } } @@ -1580,7 +1817,7 @@ void CodeEdit::update_code_completion_options(bool p_forced) { code_completion_forced = p_forced; code_completion_option_sources = code_completion_option_submitted; code_completion_option_submitted.clear(); - _filter_code_completion_candidates(); + _filter_code_completion_candidates_impl(); } TypedArray<Dictionary> CodeEdit::get_code_completion_options() const { @@ -1633,18 +1870,17 @@ void CodeEdit::set_code_completion_selected_index(int p_index) { } void CodeEdit::confirm_code_completion(bool p_replace) { - if (is_readonly() || !code_completion_active) { + if (!is_editable() || !code_completion_active) { return; } - ScriptInstance *si = get_script_instance(); - if (si && si->has_method("_confirm_code_completion")) { - si->call("_confirm_code_completion", p_replace); + if (GDVIRTUAL_CALL(_confirm_code_completion, p_replace)) { return; } + begin_complex_operation(); - int caret_line = cursor_get_line(); + int caret_line = get_caret_line(); const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; const String &display_text = code_completion_options[code_completion_current_selected].display; @@ -1652,7 +1888,7 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (p_replace) { /* Find end of current section */ const String line = get_line(caret_line); - int caret_col = cursor_get_column(); + int caret_col = get_caret_column(); int caret_remove_line = caret_line; bool merge_text = true; @@ -1675,13 +1911,13 @@ void CodeEdit::confirm_code_completion(bool p_replace) { } /* Replace. */ - _remove_text(caret_line, cursor_get_column() - code_completion_base.length(), caret_remove_line, caret_col); - cursor_set_column(cursor_get_column() - code_completion_base.length(), false); - insert_text_at_cursor(insert_text); + remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_remove_line, caret_col); + set_caret_column(get_caret_column() - code_completion_base.length(), false); + insert_text_at_caret(insert_text); } else { /* Get first non-matching char. */ const String line = get_line(caret_line); - int caret_col = cursor_get_column(); + int caret_col = get_caret_column(); int matching_chars = code_completion_base.length(); for (; matching_chars <= insert_text.length(); matching_chars++) { if (caret_col >= line.length() || line[caret_col] != insert_text[matching_chars]) { @@ -1691,44 +1927,49 @@ void CodeEdit::confirm_code_completion(bool p_replace) { } /* Remove base completion text. */ - _remove_text(caret_line, cursor_get_column() - code_completion_base.length(), caret_line, cursor_get_column()); - cursor_set_column(cursor_get_column() - code_completion_base.length(), false); + remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_line, get_caret_column()); + set_caret_column(get_caret_column() - code_completion_base.length(), false); /* Merge with text. */ - insert_text_at_cursor(insert_text.substr(0, code_completion_base.length())); - cursor_set_column(caret_col, false); - insert_text_at_cursor(insert_text.substr(matching_chars)); + insert_text_at_caret(insert_text.substr(0, code_completion_base.length())); + set_caret_column(caret_col, false); + insert_text_at_caret(insert_text.substr(matching_chars)); } - /* TODO: merge with autobrace completion, when in CodeEdit. */ /* Handle merging of symbols eg strings, brackets. */ const String line = get_line(caret_line); - char32_t next_char = line[cursor_get_column()]; + char32_t next_char = line[get_caret_column()]; char32_t last_completion_char = insert_text[insert_text.length() - 1]; char32_t last_completion_char_display = display_text[display_text.length() - 1]; - if ((last_completion_char == '"' || last_completion_char == '\'') && (last_completion_char == next_char || last_completion_char_display == next_char)) { - _remove_text(caret_line, cursor_get_column(), caret_line, cursor_get_column() + 1); + int pre_brace_pair = get_caret_column() > 0 ? _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column()) : -1; + int post_brace_pair = get_caret_column() < get_line(caret_line).length() ? _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column()) : -1; + + if (post_brace_pair != -1 && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); } - if (last_completion_char == '(') { - if (next_char == last_completion_char) { - _remove_text(caret_line, cursor_get_column() - 1, caret_line, cursor_get_column()); - } else if (auto_brace_completion_enabled) { - insert_text_at_cursor(")"); - cursor_set_column(cursor_get_column() - 1); - } - } else if (last_completion_char == ')' && next_char == '(') { - _remove_text(caret_line, cursor_get_column() - 2, caret_line, cursor_get_column()); - if (line[cursor_get_column() + 1] != ')') { - cursor_set_column(cursor_get_column() - 1); + if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); + } else if (auto_brace_completion_enabled && pre_brace_pair != -1 && post_brace_pair == -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); + set_caret_column(get_caret_column() - auto_brace_completion_pairs[pre_brace_pair].close_key.length()); + } + + if (pre_brace_pair == -1 && post_brace_pair == -1 && get_caret_column() > 0 && get_caret_column() < get_line(caret_line).length()) { + pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column() + 1); + if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1)) { + remove_text(caret_line, get_caret_column() - 2, caret_line, get_caret_column()); + if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1) != pre_brace_pair) { + set_caret_column(get_caret_column() - 1); + } } } end_complex_operation(); cancel_code_completion(); - if (last_completion_char == '(') { + if (code_completion_prefixes.has(String::chr(last_completion_char))) { request_code_completion(); } } @@ -1742,6 +1983,62 @@ void CodeEdit::cancel_code_completion() { update(); } +/* Line length guidelines */ +void CodeEdit::set_line_length_guidelines(TypedArray<int> p_guideline_columns) { + line_length_guideline_columns = p_guideline_columns; + update(); +} + +TypedArray<int> CodeEdit::get_line_length_guidelines() const { + return line_length_guideline_columns; +} + +/* Symbol lookup */ +void CodeEdit::set_symbol_lookup_on_click_enabled(bool p_enabled) { + symbol_lookup_on_click_enabled = p_enabled; + set_symbol_lookup_word_as_valid(false); +} + +bool CodeEdit::is_symbol_lookup_on_click_enabled() const { + return symbol_lookup_on_click_enabled; +} + +String CodeEdit::get_text_for_symbol_lookup() { + Point2i mp = get_local_mouse_pos(); + + Point2i pos = get_line_column_at_pos(mp); + int line = pos.y; + int col = pos.x; + + StringBuilder lookup_text; + const int text_size = get_line_count(); + for (int i = 0; i < text_size; i++) { + String text = get_line(i); + + if (i == line) { + lookup_text += text.substr(0, col); + /* Not unicode, represents the cursor. */ + lookup_text += String::chr(0xFFFF); + lookup_text += text.substr(col, text.size()); + } else { + lookup_text += text; + } + + if (i != text_size - 1) { + lookup_text += "\n"; + } + } + return lookup_text.as_string(); +} + +void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { + symbol_lookup_word = p_valid ? symbol_lookup_new_word : ""; + symbol_lookup_new_word = ""; + if (lookup_symbol_word != symbol_lookup_word) { + _set_symbol_lookup_word(symbol_lookup_word); + } +} + void CodeEdit::_bind_methods() { /* Indent management */ ClassDB::bind_method(D_METHOD("set_indent_size", "size"), &CodeEdit::set_indent_size); @@ -1762,6 +2059,22 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("indent_lines"), &CodeEdit::indent_lines); ClassDB::bind_method(D_METHOD("unindent_lines"), &CodeEdit::unindent_lines); + /* Auto brace completion */ + ClassDB::bind_method(D_METHOD("set_auto_brace_completion_enabled", "enable"), &CodeEdit::set_auto_brace_completion_enabled); + ClassDB::bind_method(D_METHOD("is_auto_brace_completion_enabled"), &CodeEdit::is_auto_brace_completion_enabled); + + ClassDB::bind_method(D_METHOD("set_highlight_matching_braces_enabled", "enable"), &CodeEdit::set_highlight_matching_braces_enabled); + ClassDB::bind_method(D_METHOD("is_highlight_matching_braces_enabled"), &CodeEdit::is_highlight_matching_braces_enabled); + + ClassDB::bind_method(D_METHOD("add_auto_brace_completion_pair", "start_key", "end_key"), &CodeEdit::add_auto_brace_completion_pair); + ClassDB::bind_method(D_METHOD("set_auto_brace_completion_pairs", "pairs"), &CodeEdit::set_auto_brace_completion_pairs); + ClassDB::bind_method(D_METHOD("get_auto_brace_completion_pairs"), &CodeEdit::get_auto_brace_completion_pairs); + + ClassDB::bind_method(D_METHOD("has_auto_brace_completion_open_key", "open_key"), &CodeEdit::has_auto_brace_completion_open_key); + ClassDB::bind_method(D_METHOD("has_auto_brace_completion_close_key", "close_key"), &CodeEdit::has_auto_brace_completion_close_key); + + ClassDB::bind_method(D_METHOD("get_auto_brace_completion_close_key", "open_key"), &CodeEdit::get_auto_brace_completion_close_key); + /* Main Gutter */ ClassDB::bind_method(D_METHOD("_main_gutter_draw_callback"), &CodeEdit::_main_gutter_draw_callback); @@ -1886,24 +2199,41 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_code_comletion_prefixes"), &CodeEdit::get_code_completion_prefixes); // Overridable - BIND_VMETHOD(MethodInfo("_confirm_code_completion", PropertyInfo(Variant::BOOL, "replace"))); - BIND_VMETHOD(MethodInfo("_request_code_completion", PropertyInfo(Variant::BOOL, "force"))); - BIND_VMETHOD(MethodInfo(Variant::ARRAY, "_filter_code_completion_candidates", PropertyInfo(Variant::ARRAY, "candidates"))); - /* Inspector */ - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_breakpoints_gutter"), "set_draw_breakpoints_gutter", "is_drawing_breakpoints_gutter"); + GDVIRTUAL_BIND(_confirm_code_completion, "replace") + GDVIRTUAL_BIND(_request_code_completion, "force") + GDVIRTUAL_BIND(_filter_code_completion_candidates, "candidates") - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter"); + /* Line length guidelines */ + ClassDB::bind_method(D_METHOD("set_line_length_guidelines", "guideline_columns"), &CodeEdit::set_line_length_guidelines); + ClassDB::bind_method(D_METHOD("get_line_length_guidelines"), &CodeEdit::get_line_length_guidelines); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_executing_lines"), "set_draw_executing_lines_gutter", "is_drawing_executing_lines_gutter"); + /* Symbol lookup */ + ClassDB::bind_method(D_METHOD("set_symbol_lookup_on_click_enabled", "enable"), &CodeEdit::set_symbol_lookup_on_click_enabled); + ClassDB::bind_method(D_METHOD("is_symbol_lookup_on_click_enabled"), &CodeEdit::is_symbol_lookup_on_click_enabled); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_line_numbers"), "set_draw_line_numbers", "is_draw_line_numbers_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "zero_pad_line_numbers"), "set_line_numbers_zero_padded", "is_line_numbers_zero_padded"); + ClassDB::bind_method(D_METHOD("get_text_for_symbol_lookup"), &CodeEdit::get_text_for_symbol_lookup); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_gutter"); + ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid); + /* Inspector */ + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symbol_lookup_on_click"), "set_symbol_lookup_on_click_enabled", "is_symbol_lookup_on_click_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "line_folding"), "set_line_folding_enabled", "is_line_folding_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "line_length_guidelines"), "set_line_length_guidelines", "get_line_length_guidelines"); + + ADD_GROUP("Gutters", "gutters_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_breakpoints_gutter"), "set_draw_breakpoints_gutter", "is_drawing_breakpoints_gutter"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_executing_lines"), "set_draw_executing_lines_gutter", "is_drawing_executing_lines_gutter"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_line_numbers"), "set_draw_line_numbers", "is_draw_line_numbers_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_zero_pad_line_numbers"), "set_line_numbers_zero_padded", "is_line_numbers_zero_padded"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_gutter"); + ADD_GROUP("Delimiters", "delimiter_"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_strings"), "set_string_delimiters", "get_string_delimiters"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_comments"), "set_comment_delimiters", "get_comment_delimiters"); @@ -1918,11 +2248,74 @@ void CodeEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_automatic"), "set_auto_indent_enabled", "is_auto_indent_enabled"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "indent_automatic_prefixes"), "set_auto_indent_prefixes", "get_auto_indent_prefixes"); + ADD_GROUP("Auto brace completion", "auto_brace_completion_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_brace_completion_enabled"), "set_auto_brace_completion_enabled", "is_auto_brace_completion_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_brace_completion_highlight_matching"), "set_highlight_matching_braces_enabled", "is_highlight_matching_braces_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "auto_brace_completion_pairs"), "set_auto_brace_completion_pairs", "get_auto_brace_completion_pairs"); + /* Signals */ + /* Gutters */ ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line"))); + + /* Code Completion */ ADD_SIGNAL(MethodInfo("request_code_completion")); + + /* Symbol lookup */ + ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "column"))); + ADD_SIGNAL(MethodInfo("symbol_validate", PropertyInfo(Variant::STRING, "symbol"))); } +/* Auto brace completion */ +int CodeEdit::_get_auto_brace_pair_open_at_pos(int p_line, int p_col) { + const String &line = get_line(p_line); + + /* Should be fast enough, expecting low amount of pairs... */ + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + const String &open_key = auto_brace_completion_pairs[i].open_key; + if (p_col - open_key.length() < 0) { + continue; + } + + bool is_match = true; + for (int j = 0; j < open_key.length(); j++) { + if (line[(p_col - 1) - j] != open_key[(open_key.length() - 1) - j]) { + is_match = false; + break; + } + } + + if (is_match) { + return i; + } + } + return -1; +} + +int CodeEdit::_get_auto_brace_pair_close_at_pos(int p_line, int p_col) { + const String &line = get_line(p_line); + + /* Should be fast enough, expecting low amount of pairs... */ + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (p_col + auto_brace_completion_pairs[i].close_key.length() > line.length()) { + continue; + } + + bool is_match = true; + for (int j = 0; j < auto_brace_completion_pairs[i].close_key.length(); j++) { + if (line[p_col + j] != auto_brace_completion_pairs[i].close_key[j]) { + is_match = false; + break; + } + } + + if (is_match) { + return i; + } + } + return -1; +} + +/* Gutters */ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == main_gutter) { if (draw_breakpoints) { @@ -1934,8 +2327,8 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == line_number_gutter) { set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); select(p_line, 0, p_line + 1, 0); - cursor_set_line(p_line + 1); - cursor_set_column(0); + set_caret_line(p_line + 1); + set_caret_column(0); return; } @@ -2128,7 +2521,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c int region = (p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value(); bool in_region = region != -1 && delimiters[region].type == p_type; for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) { - /* If column is specified, loop untill the key is larger then the column. */ + /* If column is specified, loop until the key is larger then the column. */ if (p_column != -1) { if (E->key() > p_column) { break; @@ -2138,7 +2531,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c continue; } - /* If no column, calulate if the entire line is a region */ + /* If no column, calculate if the entire line is a region */ /* excluding whitespace. */ const String line = get_line(p_line); if (!in_region) { @@ -2178,7 +2571,10 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c } void CodeEdit::_add_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only, DelimiterType p_type) { - if (p_start_key.length() > 0) { + // If we are the editor allow "null" as a valid start key, otherwise users cannot add delimiters via the inspector. + if (!(Engine::get_singleton()->is_editor_hint() && p_start_key == "null")) { + ERR_FAIL_COND_MSG(p_start_key.is_empty(), "delimiter start key cannot be empty"); + for (int i = 0; i < p_start_key.length(); i++) { ERR_FAIL_COND_MSG(!is_symbol(p_start_key[i]), "delimiter must start with a symbol"); } @@ -2243,7 +2639,11 @@ void CodeEdit::_set_delimiters(const TypedArray<String> &p_delimiters, Delimiter _clear_delimiters(p_type); for (int i = 0; i < p_delimiters.size(); i++) { - String key = p_delimiters[i].is_null() ? "" : p_delimiters[i]; + String key = p_delimiters[i]; + + if (key.is_empty()) { + continue; + } const String start_key = key.get_slice(" ", 0); const String end_key = key.get_slice_count(" ") > 1 ? key.get_slice(" ", 1) : String(); @@ -2278,9 +2678,10 @@ TypedArray<String> CodeEdit::_get_delimiters(DelimiterType p_type) const { } /* Code Completion */ -void CodeEdit::_filter_code_completion_candidates() { - ScriptInstance *si = get_script_instance(); - if (si && si->has_method("_filter_code_completion_candidates")) { +void CodeEdit::_filter_code_completion_candidates_impl() { + int line_height = get_line_height(); + + if (GDVIRTUAL_IS_OVERRIDDEN(_filter_code_completion_candidates)) { code_completion_options.clear(); code_completion_base = ""; @@ -2288,19 +2689,21 @@ void CodeEdit::_filter_code_completion_candidates() { TypedArray<Dictionary> completion_options_sources; completion_options_sources.resize(code_completion_option_sources.size()); int i = 0; - for (List<ScriptCodeCompletionOption>::Element *E = code_completion_option_sources.front(); E; E = E->next()) { + for (const ScriptCodeCompletionOption &E : code_completion_option_sources) { Dictionary option; - option["kind"] = E->get().kind; - option["display_text"] = E->get().display; - option["insert_text"] = E->get().insert_text; - option["font_color"] = E->get().font_color; - option["icon"] = E->get().icon; - option["default_value"] = E->get().default_value; + option["kind"] = E.kind; + option["display_text"] = E.display; + option["insert_text"] = E.insert_text; + option["font_color"] = E.font_color; + option["icon"] = E.icon; + option["default_value"] = E.default_value; completion_options_sources[i] = option; i++; } - TypedArray<Dictionary> completion_options = si->call("_filter_code_completion_candidates", completion_options_sources); + Array completion_options; + + GDVIRTUAL_CALL(_filter_code_completion_candidates, completion_options_sources, completion_options); /* No options to complete, cancel. */ if (completion_options.size() == 0) { @@ -2319,19 +2722,24 @@ void CodeEdit::_filter_code_completion_candidates() { option.icon = completion_options[i].get("icon"); option.default_value = completion_options[i].get("default_value"); - max_width = MAX(max_width, cache.font->get_string_size(option.display).width); + int offset = 0; + if (option.default_value.get_type() == Variant::COLOR) { + offset = line_height; + } + + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); code_completion_options.push_back(option); } - code_completion_longest_line = MIN(max_width, code_completion_max_width); + code_completion_longest_line = MIN(max_width, code_completion_max_width * font_size); code_completion_current_selected = 0; code_completion_active = true; update(); return; } - const int caret_line = cursor_get_line(); - const int caret_column = cursor_get_column(); + const int caret_line = get_caret_line(); + const int caret_column = get_caret_column(); const String line = get_line(caret_line); if (caret_column > 0 && line[caret_column - 1] == '(' && !code_completion_forced) { @@ -2366,7 +2774,7 @@ void CodeEdit::_filter_code_completion_candidates() { /* If we have a space, previous word might be a keyword. eg "func |". */ } else if (cofs > 0 && line[cofs - 1] == ' ') { int ofs = cofs - 1; - while (ofs >= 0 && line[ofs] == ' ') { + while (ofs > 0 && line[ofs] == ' ') { ofs--; } prev_is_word = _is_char(line[ofs]); @@ -2406,13 +2814,16 @@ void CodeEdit::_filter_code_completion_candidates() { int max_width = 0; String string_to_complete_lower = string_to_complete.to_lower(); - for (List<ScriptCodeCompletionOption>::Element *E = code_completion_option_sources.front(); E; E = E->next()) { - ScriptCodeCompletionOption &option = E->get(); - + for (ScriptCodeCompletionOption &option : code_completion_option_sources) { if (single_quote && option.display.is_quoted()) { option.display = option.display.unquote().quote("'"); } + int offset = 0; + if (option.default_value.get_type() == Variant::COLOR) { + offset = line_height; + } + if (in_string != -1) { String quote = single_quote ? "'" : "\""; option.display = option.display.unquote().quote(quote); @@ -2425,7 +2836,7 @@ void CodeEdit::_filter_code_completion_candidates() { if (string_to_complete.length() == 0) { code_completion_options.push_back(option); - max_width = MAX(max_width, cache.font->get_string_size(option.display).width); + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); continue; } @@ -2474,7 +2885,7 @@ void CodeEdit::_filter_code_completion_candidates() { } else { completion_options_subseq.push_back(option); } - max_width = MAX(max_width, cache.font->get_string_size(option.display).width); + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); /* Matched the whole subsequence in s_lower. */ } else if (!*ssq_lower) { /* Finished matching in the first s.length() characters. */ @@ -2483,7 +2894,7 @@ void CodeEdit::_filter_code_completion_candidates() { } else { completion_options_subseq_casei.push_back(option); } - max_width = MAX(max_width, cache.font->get_string_size(option.display).width); + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); } } @@ -2503,7 +2914,7 @@ void CodeEdit::_filter_code_completion_candidates() { return; } - code_completion_longest_line = MIN(max_width, code_completion_max_width); + code_completion_longest_line = MIN(max_width, code_completion_max_width * font_size); code_completion_current_selected = 0; code_completion_active = true; update(); @@ -2516,31 +2927,53 @@ void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) { return; } + lines_edited_from = (lines_edited_from == -1) ? MIN(p_from_line, p_to_line) : MIN(lines_edited_from, MIN(p_from_line, p_to_line)); + lines_edited_to = (lines_edited_to == -1) ? MAX(p_from_line, p_to_line) : MAX(lines_edited_from, MAX(p_from_line, p_to_line)); +} + +void CodeEdit::_text_set() { + lines_edited_from = 0; + lines_edited_to = 9999; + _text_changed(); +} + +void CodeEdit::_text_changed() { + if (lines_edited_from == -1) { + return; + } + int lc = get_line_count(); line_number_digits = 1; while (lc /= 10) { line_number_digits++; } - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); - int from_line = MIN(p_from_line, p_to_line); - int line_count = (p_to_line - p_from_line); + if (font.is_valid()) { + set_gutter_width(line_number_gutter, (line_number_digits + 1) * font->get_char_size('0', 0, font_size).width); + } + + lc = get_line_count(); + int line_change_size = (lines_edited_to - lines_edited_from); List<int> breakpoints; breakpointed_lines.get_key_list(&breakpoints); - for (const List<int>::Element *E = breakpoints.front(); E; E = E->next()) { - int line = E->get(); - if (line <= from_line) { + for (const int &line : breakpoints) { + if (line < lines_edited_from || (line < lc && is_line_breakpointed(line))) { continue; } + breakpointed_lines.erase(line); + emit_signal(SNAME("breakpoint_toggled"), line); - emit_signal("breakpoint_toggled", line); - if (line_count > 0 || line >= p_from_line) { - emit_signal("breakpoint_toggled", line + line_count); - breakpointed_lines[line + line_count] = true; + int next_line = line + line_change_size; + if (next_line < lc && is_line_breakpointed(next_line)) { + emit_signal(SNAME("breakpoint_toggled"), next_line); + breakpointed_lines[next_line] = true; continue; } } + + lines_edited_from = -1; + lines_edited_to = -1; } CodeEdit::CodeEdit() { @@ -2550,6 +2983,17 @@ CodeEdit::CodeEdit() { auto_indent_prefixes.insert('['); auto_indent_prefixes.insert('('); + /* Auto brace completion */ + add_auto_brace_completion_pair("(", ")"); + add_auto_brace_completion_pair("{", "}"); + add_auto_brace_completion_pair("[", "]"); + add_auto_brace_completion_pair("\"", "\""); + add_auto_brace_completion_pair("\'", "\'"); + + /* Delimiter traking */ + add_string_delimiter("\"", "\"", false); + add_string_delimiter("\'", "\'", false); + /* Text Direction */ set_layout_direction(LAYOUT_DIRECTION_LTR); set_text_direction(TEXT_DIRECTION_LTR); @@ -2562,7 +3006,7 @@ CodeEdit::CodeEdit() { set_gutter_name(gutter_idx, "main_gutter"); set_gutter_draw(gutter_idx, false); set_gutter_overwritable(gutter_idx, true); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_main_gutter_draw_callback"); gutter_idx++; @@ -2570,7 +3014,7 @@ CodeEdit::CodeEdit() { add_gutter(); set_gutter_name(gutter_idx, "line_numbers"); set_gutter_draw(gutter_idx, false); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_line_number_draw_callback"); gutter_idx++; @@ -2578,13 +3022,15 @@ CodeEdit::CodeEdit() { add_gutter(); set_gutter_name(gutter_idx, "fold_gutter"); set_gutter_draw(gutter_idx, false); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_fold_gutter_draw_callback"); gutter_idx++; connect("lines_edited_from", callable_mp(this, &CodeEdit::_lines_edited_from)); - connect("gutter_clicked", callable_mp(this, &CodeEdit::_gutter_clicked)); + connect("text_set", callable_mp(this, &CodeEdit::_text_set)); + connect("text_changed", callable_mp(this, &CodeEdit::_text_changed)); + connect("gutter_clicked", callable_mp(this, &CodeEdit::_gutter_clicked)); connect("gutter_added", callable_mp(this, &CodeEdit::_update_gutter_indexes)); connect("gutter_removed", callable_mp(this, &CodeEdit::_update_gutter_indexes)); _update_gutter_indexes(); |