orbitalquark / textadept

Textadept is a fast, minimalist, and remarkably extensible cross-platform text editor for programmers.
https://orbitalquark.github.io/textadept
MIT License
636 stars 38 forks source link

Crash of Textadept while toggling wrapping with Ctrl+\ #459

Closed oOosys closed 11 months ago

oOosys commented 11 months ago

Assertion [line < model.pdoc->LinesTotal()] failed at .../build/_deps/scintilla-src/src/EditView.cxx 402 Aborted (core dumped)

Where is the core dumped?

This crash started to occur after implementation of "follow mode" in the local user "init.lua" file while trying to fix the issue with wrong values for line numbers shown in the view. Only querying of values was done to get some insight about line numbering because of weird results of adjusting the scrolling of another view showing same buffer. I can't query the values of insight about is not possible because of the crash.

My system: Linux Mint 21.2 Xfce, GTK-version.

Below some more context.

The output in the Terminal window from which the editor was started:

  >> adjust_scroll(): UPDATE_UI 53
  >> adjust_scroll(): UPDATE_UI -> scrolling
active_top_line:    333 shift:  17  active_last_line:   350
active_view.lines_on_screen:    24
  >> adjust_scroll(): for i,v in ipairs(views) do
  >>         new_scroll_y:  333 curr_scroll_y:  333
  >> adjust_scroll(): for i,v in ipairs(views) do
  >>         new_scroll_y:  349 curr_scroll_y:  324
  >> adjust_scroll(): UPDATE_UI 54
  >> adjust_scroll(): UPDATE_UI -> scrolling
active_top_line:    348 shift:  9   active_last_line:   357
active_view.lines_on_screen:    24
  >> adjust_scroll(): for i,v in ipairs(views) do
  >>         new_scroll_y:  348 curr_scroll_y:  348
  >> adjust_scroll(): for i,v in ipairs(views) do
  >>         new_scroll_y:  354 curr_scroll_y:  324
  >> adjust_scroll(): UPDATE_UI 55
  >> adjust_scroll(): UPDATE_UI -> scrolling
Assertion [line < model.pdoc->LinesTotal()] failed at .../build/_deps/scintilla-src/src/EditView.cxx 402
Aborted (core dumped)

Here the code in init.lua which is maybe the reason for the crash which occurs when there are two views showing the same buffer and in one of them Ctrl+\ is used to switch to wrapping mode to see what happens to the reported and calculated line numbers:

--                              FOLLOW MODE
-- (views of same buffer scroll along with it showing preceeding/following text sections) 
_="Known issues: does not correctly work in case of wrapped lines" 
local follow_mode               =  1    -- OFF -> 0   ON -> 1
local follow_mode_left_to_right =  1    -- set to -1 for right to left instead of left to right 
local last_top_line =  -1
adjust_scroll_counter = 0
local function adjust_scroll(event_bitmap)
    adjust_scroll_counter = adjust_scroll_counter + 1
    print("  >> adjust_scroll(): UPDATE_UI", adjust_scroll_counter)
    if event_bitmap & view.UPDATE_V_SCROLL== 0 then 
        print("  >> adjust_scroll(): UPDATE_UI other than scrolling")
        return
    end
    print("  >> adjust_scroll(): UPDATE_UI -> scrolling")
    local views = {}
    -- Collect all views with the same buffer
    for _, v in ipairs(_VIEWS) do
        if v.buffer == view.buffer then
            table.insert(views, v)
        end
    end
    if #views == 1 or #views > 3 then 
        --          #views <= 3 is a deliberate limit, so change it to 4 or more if you like
        -- The purpose of it is to switch the "follow mode" in case of 4 or more views OFF
        return 
    end
    --print("  >> adjust_scroll(): there a 2 or 3 views of same buffer ")

    -- Get the active view
    local active_view = view

    -- Calculate the shift amount for text continuation
    local active_top_line       = active_view.first_visible_line
    local no_of_lines_on_screen = active_view.lines_on_screen
    local active_last_line      = active_top_line + no_of_lines_on_screen
    local no_of_required_lines  = 0

    for line_no=active_top_line,  active_top_line+no_of_lines_on_screen-1 do
        no_of_required_lines = no_of_required_lines + view.wrap_count(line_no)
        active_last_line = line_no
        if no_of_required_lines > no_of_lines_on_screen then
            break
        end
    end

    local shift = active_last_line - active_top_line -- = no_of_lines_on_screen 
    print("active_top_line:", active_top_line, "shift:", shift, "active_last_line:", active_last_line)
    print("active_view.lines_on_screen:", active_view.lines_on_screen)
    -- Track the current visible lines in the active view
    if active_top_line == last_top_line then
        print("  >> just detected: scrolling without scrolling")
        return
    end

    last_top_line = active_top_line

    local active_bottom_line = active_last_line --active_top_line + active_view.lines_on_screen

    -- Find out the index of the current active buffer:
    for i, v in ipairs(views) do
        if v == active_view then
        curr_view_index = i
        break
    end
    end

    -- Adjust the scroll positions of columns for text continuation
    for i, v in ipairs(views) do
        print("  >> adjust_scroll(): for i,v in ipairs(views) do")
        -- Calculate the offset based on view order and shift
        local offset = (i - curr_view_index) * (shift-1) * follow_mode_left_to_right
        -- Calculate the new scroll position based on the active view's visible lines
        local new_scroll_y = active_top_line + offset
        -- Ensure that the new scroll position is within bounds
        new_scroll_y = math.max(0, math.min(new_scroll_y, v.buffer.line_count - v.lines_on_screen))
        local curr_scroll_y =  v.first_visible_line
        print("  >>         new_scroll_y:", new_scroll_y, "curr_scroll_y:", curr_scroll_y )
        -- Only set the scroll position if it has changed
        if new_scroll_y ~= curr_scroll_y then
            --v.first_visible_line = new_scroll_y
        return
        end
    --print("  >>          Scrolling occured, but no scrolling of this  buffer necessary")
    end
end
-- Track scrolling in the active buffer and adjust columns showing same buffer accordingly
if follow_mode == 1 then 
    events.connect(events.UPDATE_UI, adjust_scroll)
end
oOosys commented 11 months ago

The crash is caused by view.wrap_count(line_no) call when given too large line_no. The original reason for this is that in wrapping mode active_view.first_visible_line does not report the line number seen as line number in the line numbering margin, but some other value which is much higher and changes when scrolling one line at a time by values like for example 10 or 15 at a time.

In other words the crash occurs because view.first_visible_line does not report the actual first visible line number seen in the line numbering margin with switched on line wrapping.

Is it a bug or a feature that view.first_visible_line does not provide the right line number if wrapping is switched on? How to detect if wrapping is active from within Lua code in user init.lua?

orbitalquark commented 11 months ago

I haven't looked at your code, but my first instinct is that you need to call view:doc_line_from_visible(view.first_visible_line) to get the actual document/buffer line number from the one displayed/visible. This is a feature of Scintilla, not a bug.

oOosys commented 11 months ago

OK ... So view.first_visible_line gives a line number which is the result of counting lines as they would appear in the view on screen. i.e. If an actual document line N (counted by newlines in the file of the loaded viewed text if the text was not yet changed) is wrapped into five lines, navigating to the last of these lines will give view.first_visible_line==N+4 where view:doc_line_from_visible(view.first_visible_line)==N will still give the in the line numbering margin shown line number N. In other words, switching line wrapping ON results in converting the entire viewed buffer text into wrapped text, not only the part which is visible in the view, right? So wrapping lines in case of very large documents will result in making the CPU busy with recalculating all the line numbers in the entire document and should be avoided as not practicable, right? Further in other words, if I want wrapping of long lines in texts with a huge amount of long lines I need to implement it on my own and take full control of what is displayed in the view, no matter the actual content of the viewed text, right?

orbitalquark commented 11 months ago

Your understanding is correct. I doubt you can "tak[e] full control of what is displayed in the view" though. Scintilla (via GUI widget) has its own way of displaying/laying out text that Textadept does not interfere with.

oOosys commented 11 months ago

Thanks for the hint towards view:doc_line_from_visible(view.first_visible_line). This is a perfect starting point for "print()-based" debugging and understanding what is going on with the side-effect of solving the problem which was the reason for opening this issue. That the application shouldn't crash given a wrong value for a line number is another topic I am currently not ready to cope with.