From 9f5980fe04ce7bd74df4d01086f7182d0b88170a Mon Sep 17 00:00:00 2001 From: jofon <70416966+jofon@users.noreply.github.com> Date: Tue, 5 Sep 2023 22:34:35 +0100 Subject: [PATCH] Merged and modified get_visible_text and underline_misspelled_words Doesn't get the entire line at once, instead it now works in blocks of 4096 characters Takes into account visible lines, horizontal scroll, and the end of the visible text in a line Changed is_word_under_cursor_correct to check prev token and next token from the current position instead of using the entire line Also added a protection for when the document isn't loaded by N++ --- src/core/SpellChecker.cpp | 114 ++++++++++++++++++++++++----------- src/core/SpellChecker.h | 3 +- src/npp/EditorInterface.cpp | 8 --- src/npp/EditorInterface.h | 2 +- src/npp/NppInterface.cpp | 6 ++ src/npp/NppInterface.h | 2 + test/MockEditorInterface.cpp | 8 +++ test/MockEditorInterface.h | 3 + test/SpellCheckerTests.cpp | 43 +++++++++++++ 9 files changed, 144 insertions(+), 45 deletions(-) diff --git a/src/core/SpellChecker.cpp b/src/core/SpellChecker.cpp index 62a2f9d..86b5b03 100644 --- a/src/core/SpellChecker.cpp +++ b/src/core/SpellChecker.cpp @@ -210,40 +210,76 @@ TextPosition SpellChecker::next_token_end_in_document(TextPosition end) const { return end; } -MappedWstring SpellChecker::get_visible_text() { - auto top_visible_line = m_editor.get_first_visible_line(); - auto top_visible_line_index = m_editor.get_document_line_from_visible(top_visible_line); - auto bottom_visible_line_index = m_editor.get_document_line_from_visible(top_visible_line + m_editor.get_lines_on_screen() - 1); - auto rect = m_editor.editor_rect(); - auto len = m_editor.get_active_document_length(); - MappedWstring result; +void SpellChecker::underline_misspelled_words_in_visible_text() { + const int optimal_range_len = 4096; + + const auto top_visible_line = m_editor.get_first_visible_line(); + const auto top_visible_line_index = m_editor.get_document_line_from_visible(top_visible_line); + const auto bottom_visible_line_index = m_editor.get_document_line_from_visible(top_visible_line + m_editor.get_lines_on_screen() - 1); + const auto rect = m_editor.editor_rect(); + const auto len = m_editor.get_active_document_length(); + + const auto first_visible_column = m_editor.get_first_visible_column(); + for (auto line = top_visible_line_index; line <= bottom_visible_line_index; ++line) { if (!m_editor.is_line_visible(line)) continue; auto start = m_editor.get_line_start_position(line); if (start >= len) // skipping possible empty lines when document is too short continue; - auto start_point = m_editor.get_point_from_position(start); - if (start_point.y < rect.top) { - start = m_editor.char_position_from_point({0, 0}); - start = prev_token_begin_in_document(start); - } else if (start_point.x < rect.left) { - start = m_editor.char_position_from_point({0, start_point.y}); - start = prev_token_begin_in_document(start); - } - auto end = m_editor.get_line_end_position(line); - auto end_point = m_editor.get_point_from_position(end); - if (end_point.y > rect.bottom - rect.top) { - end = m_editor.char_position_from_point({rect.right - rect.left, rect.bottom - rect.top}); - end = next_token_end_in_document(end); - } else if (end_point.x > rect.right) { - end = m_editor.char_position_from_point({rect.right - rect.left, end_point.y}); - end = next_token_end_in_document(end); + + if (start == -1) // end of document + break; + + const auto line_end = m_editor.get_line_end_position(line); + + const auto line_start_point = m_editor.get_point_from_position(start); + const auto line_end_point = m_editor.get_point_from_position(line_end); + + // If the line or file isn't being rendered, then all points will be at {0, 0}, so skip it + if (line_start_point.x == line_end_point.x && line_start_point.y == line_end_point.y) + continue; + + // scroll horizontally + start += first_visible_column; + + if (start > line_end) // Skip lines that ended before the current horizontal scroll position + continue; + + for (auto end = start + optimal_range_len; start < line_end; start = end + 1, end = start + optimal_range_len) { + const auto start_point = m_editor.get_point_from_position(start); + if (start_point.y < rect.top) { + start = m_editor.char_position_from_point({0, 0}); + start = prev_token_begin_in_document(start); + } else if (start_point.x < rect.left) { + start = m_editor.char_position_from_point({0, start_point.y}); + start = prev_token_begin_in_document(start); + } else if (first_visible_column > 0) { + start = prev_token_begin_in_document(start); + } + + if (end > line_end) { + end = line_end; + } + + const auto end_point = m_editor.get_point_from_position(end); + if (end_point.y > rect.bottom - rect.top) { + end = m_editor.char_position_from_point({rect.right - rect.left, rect.bottom - rect.top}); + end = next_token_end_in_document(end); + } else if (end_point.x > rect.right) { + end = m_editor.char_position_from_point({rect.right - rect.left, end_point.y}); + end = next_token_end_in_document(end); + } + + // Stop if the start of this range is not visible + if (start > end) + break; + + const auto new_str = m_editor.get_mapped_wstring_range(start, end); + + underline_misspelled_words(new_str, start); } - auto new_str = m_editor.get_mapped_wstring_range(start, end); - result.append(new_str); } - return result; } void SpellChecker::clear_all_underlines() const { @@ -267,6 +303,10 @@ bool SpellChecker::is_word_under_cursor_correct(TextPosition &pos, TextPosition length = 0; pos = -1; + const auto doc_length = m_editor.get_active_document_length(); + if (doc_length == 0) + return true; + if (!use_text_cursor) { auto p = m_editor.get_mouse_cursor_pos(); if (!p) @@ -282,8 +322,10 @@ bool SpellChecker::is_word_under_cursor_correct(TextPosition &pos, TextPosition init_char_pos = std::min(selection_start, selection_end); } - auto line = m_editor.line_from_position(init_char_pos); - auto mapped_str = m_editor.get_mapped_wstring_line(line); + const auto start = prev_token_begin_in_document(init_char_pos); + const auto end = next_token_end_in_document(start + 1); + + const auto mapped_str = m_editor.get_mapped_wstring_range(start, end); if (mapped_str.str.empty()) return true; auto word = get_word_at(init_char_pos, mapped_str); @@ -381,7 +423,7 @@ std::vector SpellChecker::check_text(const MappedWstring &text_ return words_to_check; } -void SpellChecker::underline_misspelled_words(const MappedWstring &text_to_check) const { +void SpellChecker::underline_misspelled_words(const MappedWstring &text_to_check, const TextPosition start_pos) const { std::vector underline_buffer; auto words_to_check = check_text(text_to_check); for (auto &result : words_to_check) { @@ -390,14 +432,16 @@ void SpellChecker::underline_misspelled_words(const MappedWstring &text_to_check std::array list{result.word_start, result.word_end}; underline_buffer.insert(underline_buffer.end(), list.begin(), list.end()); } - TextPosition prev_pos = 0; + + TextPosition prev_pos = start_pos; for (TextPosition i = 0; i < static_cast(underline_buffer.size()) - 1; i += 2) { - remove_underline(prev_pos, underline_buffer[i]); + remove_underline(prev_pos, underline_buffer[i]); // remove from end of last to start of new create_word_underline(underline_buffer[i], underline_buffer[i + 1]); - prev_pos = underline_buffer[i + 1]; + prev_pos = underline_buffer[i + 1]; // update end of last } + auto text_len = text_to_check.original_length(); - remove_underline(prev_pos, text_len); + remove_underline(prev_pos, text_len); // remove from end of last word to end of text } std::vector SpellChecker::get_misspelled_words(const MappedWstring &text_to_check) const { @@ -437,8 +481,8 @@ std::optional> SpellChecker::find_last_misspelling(c void SpellChecker::check_visible() { print_to_log(L"void SpellChecker::check_visible(NppViewType view)", m_editor.get_editor_hwnd()); - auto text = get_visible_text(); - underline_misspelled_words(text); + + underline_misspelled_words_in_visible_text(); } void SpellChecker::recheck_visible() { diff --git a/src/core/SpellChecker.h b/src/core/SpellChecker.h index 03104d8..e7fb9d4 100644 --- a/src/core/SpellChecker.h +++ b/src/core/SpellChecker.h @@ -66,8 +66,9 @@ class SpellChecker { TextPosition prev_token_begin_in_document(TextPosition start) const; TextPosition next_token_end_in_document(TextPosition end) const; MappedWstring get_visible_text(); + void underline_misspelled_words_in_visible_text(); std::vector check_text(const MappedWstring &text_to_check) const; - void underline_misspelled_words(const MappedWstring &text_to_check) const; + void underline_misspelled_words(const MappedWstring &text_to_check, const TextPosition start_pos) const; std::vector get_misspelled_words(const MappedWstring &text_to_check) const; std::optional> find_first_misspelling(const MappedWstring &text_to_check, TextPosition last_valid_position) const; std::optional> find_last_misspelling(const MappedWstring &text_to_check, TextPosition last_valid_position) const; diff --git a/src/npp/EditorInterface.cpp b/src/npp/EditorInterface.cpp index 08c9e89..220321e 100644 --- a/src/npp/EditorInterface.cpp +++ b/src/npp/EditorInterface.cpp @@ -74,11 +74,3 @@ MappedWstring EditorInterface::get_mapped_wstring_range(TextPosition from, TextP val += from; return result; } - -MappedWstring EditorInterface::get_mapped_wstring_line(TextPosition line) { - auto result = to_mapped_wstring(get_line(line));; - auto line_start = get_line_start_position(line); - for (auto &val : result.mapping) - val += line_start; - return result; -} diff --git a/src/npp/EditorInterface.h b/src/npp/EditorInterface.h index 6c2fa2b..979f070 100644 --- a/src/npp/EditorInterface.h +++ b/src/npp/EditorInterface.h @@ -132,8 +132,8 @@ class EditorInterface { TextPosition get_next_valid_end_pos(TextPosition pos) const; MappedWstring to_mapped_wstring(const std::string &str); MappedWstring get_mapped_wstring_range(TextPosition from, TextPosition to); - MappedWstring get_mapped_wstring_line(TextPosition line); std::string to_editor_encoding(std::wstring_view str) const; + virtual int get_first_visible_column() const = 0; virtual ~EditorInterface() = default; diff --git a/src/npp/NppInterface.cpp b/src/npp/NppInterface.cpp index 3e225c9..f13e8cf 100644 --- a/src/npp/NppInterface.cpp +++ b/src/npp/NppInterface.cpp @@ -53,6 +53,12 @@ int NppInterface::get_indicator_value_at(int indicator_id, TextPosition position return static_cast(send_msg_to_scintilla(SCI_INDICATORVALUEAT, indicator_id, position)); } +int NppInterface::get_first_visible_column() const { + const int x_offset = static_cast(send_msg_to_scintilla(SCI_GETXOFFSET)); + const int pixel_width = static_cast(send_msg_to_scintilla(SCI_TEXTWIDTH, STYLE_DEFAULT, reinterpret_cast("P"))); + return static_cast(x_offset / pixel_width); +} + LRESULT NppInterface::send_msg_to_npp(UINT Msg, WPARAM wParam, LPARAM lParam) const { return SendMessage(m_npp_data.npp_handle, Msg, wParam, lParam); } HWND NppInterface::get_view_hwnd() const { diff --git a/src/npp/NppInterface.h b/src/npp/NppInterface.h index 5e3ca73..2fd98f0 100644 --- a/src/npp/NppInterface.h +++ b/src/npp/NppInterface.h @@ -109,6 +109,8 @@ class NppInterface : public EditorInterface { HMENU get_menu_handle(int menu_type) const; int get_target_view() const override; int get_indicator_value_at(int indicator_id, TextPosition position) const override; + int get_first_visible_column() const override; + HWND get_view_hwnd() const override; std::wstring get_editor_directory() const override; diff --git a/test/MockEditorInterface.cpp b/test/MockEditorInterface.cpp index cfdb9c6..7c2b307 100644 --- a/test/MockEditorInterface.cpp +++ b/test/MockEditorInterface.cpp @@ -718,6 +718,14 @@ void MockEditorInterface::set_mouse_cursor_pos(const std::optional &pos) std::wstring MockEditorInterface::get_editor_directory() const { return {}; } +int MockEditorInterface::get_first_visible_column() const { + return m_first_visible_column; +} + +void MockEditorInterface::scroll_horizontally(const int scroll_amount) { + m_first_visible_column += scroll_amount; +} + MockedDocumentInfo *MockEditorInterface::active_document() { if (m_documents[m_target_view].empty()) return nullptr; diff --git a/test/MockEditorInterface.h b/test/MockEditorInterface.h index 8648997..a937a42 100644 --- a/test/MockEditorInterface.h +++ b/test/MockEditorInterface.h @@ -142,6 +142,8 @@ class MockEditorInterface : public EditorInterface { std::optional get_mouse_cursor_pos() const override; void set_mouse_cursor_pos(const std::optional &pos); std::wstring get_editor_directory() const override; + int get_first_visible_column() const override; + void scroll_horizontally(const int scroll_amount); private: void set_target_view(int view_index) const override; @@ -162,4 +164,5 @@ class MockEditorInterface : public EditorInterface { mutable int m_target_view = -1; RECT m_editor_rect; std::optional m_cursor_pos; + int m_first_visible_column = 0; }; diff --git a/test/SpellCheckerTests.cpp b/test/SpellCheckerTests.cpp index 51c5936..5ed0fec 100644 --- a/test/SpellCheckerTests.cpp +++ b/test/SpellCheckerTests.cpp @@ -526,4 +526,47 @@ test_test SECTION("Not called normally") { CHECK_FALSE (SpellCheckerHelpers::is_word_spell_checking_needed(settings, editor, L"", 0)); } + SECTION("Horizontally scrolled") { + editor.set_active_document_text(LR"(wrongword This is test document abaabs +This is test document +badword +Badword Wrongword)"); + { + editor.make_all_visible(); + sc.recheck_visible_both_views(); + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"wrongword", "abaabs", "badword", "Badword", "Wrongword"}); + } + { + editor.scroll_horizontally(1); // Scroll one column to the right + sc.recheck_visible_both_views(); + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"wrongword", "abaabs", "badword", "Badword", "Wrongword"}); + } + { + editor.scroll_horizontally(7); // Scroll until the end of the 3rd line ("badword" + newline character) + sc.recheck_visible_both_views(); + // previously underlined words behind the new start column are still underlined + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"wrongword", "abaabs", "badword", "Badword", "Wrongword"}); + editor.clear_indicator_info(); + sc.recheck_visible_both_views(); + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"wrongword", "abaabs", "Wrongword"}); + } + { + editor.scroll_horizontally(6); // Scroll until "document" on the second line isn't completely visible (becomes "ocument") + editor.clear_indicator_info(); + sc.recheck_visible_both_views(); + // "document" is correct, so it's not underlined + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"abaabs", "Wrongword"}); + } + { + editor.scroll_horizontally(-14); // Scroll back to the start + sc.recheck_visible_both_views(); + CHECK(editor.get_underlined_words(indicator_id) == std::vector{"wrongword", "abaabs", "badword", "Badword", "Wrongword"}); + } + { + editor.scroll_horizontally(50); // Scroll enough to hide all words + editor.clear_indicator_info(); + sc.recheck_visible_both_views(); + CHECK(editor.get_underlined_words(indicator_id).empty()); + } + } }