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()); + } + } }