Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix crash when opening a large one-line file (500MB) on 32-bit, and improve loading time #329

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 79 additions & 35 deletions src/core/SpellChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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);
Expand Down Expand Up @@ -381,7 +423,7 @@ std::vector<SpellerWordData> 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<TextPosition> underline_buffer;
auto words_to_check = check_text(text_to_check);
for (auto &result : words_to_check) {
Expand All @@ -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<TextPosition>(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<std::wstring_view> SpellChecker::get_misspelled_words(const MappedWstring &text_to_check) const {
Expand Down Expand Up @@ -437,8 +481,8 @@ std::optional<std::array<TextPosition, 2>> 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() {
Expand Down
3 changes: 2 additions & 1 deletion src/core/SpellChecker.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpellerWordData> 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<std::wstring_view> get_misspelled_words(const MappedWstring &text_to_check) const;
std::optional<std::array<TextPosition, 2>> find_first_misspelling(const MappedWstring &text_to_check, TextPosition last_valid_position) const;
std::optional<std::array<TextPosition, 2>> find_last_misspelling(const MappedWstring &text_to_check, TextPosition last_valid_position) const;
Expand Down
8 changes: 0 additions & 8 deletions src/npp/EditorInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion src/npp/EditorInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions src/npp/NppInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ int NppInterface::get_indicator_value_at(int indicator_id, TextPosition position
return static_cast<int>(send_msg_to_scintilla(SCI_INDICATORVALUEAT, indicator_id, position));
}

int NppInterface::get_first_visible_column() const {
const int x_offset = static_cast<int>(send_msg_to_scintilla(SCI_GETXOFFSET));
const int pixel_width = static_cast<int>(send_msg_to_scintilla(SCI_TEXTWIDTH, STYLE_DEFAULT, reinterpret_cast<LPARAM>("P")));
return static_cast<int>(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 {
Expand Down
2 changes: 2 additions & 0 deletions src/npp/NppInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
8 changes: 8 additions & 0 deletions test/MockEditorInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,14 @@ void MockEditorInterface::set_mouse_cursor_pos(const std::optional<POINT> &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;
Expand Down
3 changes: 3 additions & 0 deletions test/MockEditorInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ class MockEditorInterface : public EditorInterface {
std::optional<POINT> get_mouse_cursor_pos() const override;
void set_mouse_cursor_pos(const std::optional<POINT> &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;
Expand All @@ -162,4 +164,5 @@ class MockEditorInterface : public EditorInterface {
mutable int m_target_view = -1;
RECT m_editor_rect;
std::optional<POINT> m_cursor_pos;
int m_first_visible_column = 0;
};
43 changes: 43 additions & 0 deletions test/SpellCheckerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>{"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<std::string>{"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<std::string>{"wrongword", "abaabs", "badword", "Badword", "Wrongword"});
editor.clear_indicator_info();
sc.recheck_visible_both_views();
CHECK(editor.get_underlined_words(indicator_id) == std::vector<std::string>{"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<std::string>{"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<std::string>{"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());
}
}
}
Loading