Custom folding in wxRuby’s Scintilla
Folding is of course a part of the lexer and when I made my own styling (described in the prior tutorial) it follows that I have to implement my own folding logic too. Well it’s as easily done as said, we just use the folding in lexLisp.cxx, here is our Ruby version of Alexey Yutkin’s C++ version in lexer.rb:
def setFolding(sci, start_pos, length)
vis_chrs = 0
cur_line = sci.line_from_position(start_pos)
cur_lvl = prev_lvl = sci.get_fold_level(cur_line) & STC_FOLDLEVELNUMBERMASK
(start_pos..(start_pos + length)).each do |n|
cur_chr = sci.get_char_at(n).chr
if sci.get_style_at(n) == @fold_info[:style]
if cur_chr == @fold_info[:left]
cur_lvl += 1
elsif cur_chr == @fold_info[:right]
cur_lvl -= 1
end
end
if ((cur_chr == "\n") || (sci.get_char_at(n+1).chr != "\n" && cur_chr == "\r"))
lvl = prev_lvl
lvl |= STC_FOLDLEVELWHITEFLAG if vis_chrs == 0
lvl |= STC_FOLDLEVELHEADERFLAG if ((cur_lvl > prev_lvl) && (vis_chrs > 0))
sci.set_fold_level(cur_line, lvl) if lvl != sci.get_fold_level(cur_line)
cur_line += 1
prev_lvl = cur_lvl
vis_chrs = 0
end
vis_chrs += 1 if cur_chr =~ /\S/
end
nxt_flag = sci.get_fold_level(cur_line) & ~STC_FOLDLEVELNUMBERMASK
sci.set_fold_level(cur_line, prev_lvl | nxt_flag)
end
Thanks Alexey! Note that we use @fold_info to enable each child (in this case the Pico class) to use this function on its own terms, let’s check out the differences is pico.rb:
class Pico < Lexer
attr_accessor :name
def initialize(project, proj_id, sci)
. . .
@fold_info = {:left => '(', :right => ')', :style => STC_LISP_OPERATOR}
end
def startStyling(sci)
(0..sci.get_line_count).each do |line_nbr|
sci.start_styling(sci.position_from_line(line_nbr), 31)
txt = sci.get_line(line_nbr)
self.styleMe(txt, sci)
end
self.setFolding(sci, 0, sci.get_text_length)
end
. . .
And that’s that, another more “normal” language might use {} instead of () like we do here. Note also the “wasteful” way we do things, basically coloring and folding the whole document on every style needed event, I haven’t noticed any lag yet though so we just run with it until we notice some. If that is the case something needs to be done in the Scintilla class to differentiate between different events and act accordingly, not just updating the whole document.
Finally, all is well!
Update:
Okay, it seems like I have to take back what I said about the performance above, pretty quickly too. The other day I tested in Windows for the first time and big files were not very responsive, think Eclipse on Linux with JRE5, yeah it was bad. Some optimizing was sorely needed.
Apparently there is a very nifty thing we can use in the styled event, get_position will return the last position in the document that needs to be styled, with the help of that position and the current position we can get the range we need to cover. It’s intelligent too, if we only work with the current line the range will only be the current line, if we create a new line the last position will now be the last position in the document instead since we need to style everything below our new line.
In scintilla.rb:
def onStyleNeeded(evt)
self.styleWithPos(self.get_current_pos - 1, evt.get_position)
end
def styleWithPos(start_pos, end_pos)
start_line = self.line_from_position(start_pos)
end_line = self.line_from_position(end_pos)
self.start_styling(start_pos, 31)
@cur_lexer.startStyling(self, start_line, end_line)
@cur_lexer.setFolding(self, start_pos, end_pos - start_pos)
end
There is only one problem with this, when we paste stuff the pasted stuff will not be styled because it will end up above the current position. Something had to be done in editor.rb:
. . .
evt_menu(6009) {onPaste}
. . .
fileMenu.append(6009, "Paste\tCtrl-V", "Paste")
. . .
def onPaste
@sci.cmd_key_execute(STC_CMD_PASTE)
@sci.styleWithPos(0, @sci.get_text_length)
end
Luckily this time it worked better than before with our botched attempt at changing the behavior of SHIFT+ALT. With cmd_key_execute and STC_CMD_PASTE we are able to programatically execute Scintilla’s paste behavior and do a call to the styling with the whole document range as the argument.
Update: Another performance boost was accomplished by simply creating this method in scintilla.rb:
def styleDocument
self.styleWithPos(0, self.get_text_length)
end
And then calling it in editor.rb:
def openFile(lexer, path, proj_id, line_nbr = 0)
if @notebook.get_selection == -1 || @sci == nil || @sci.file_path != path
@notebook.add_page( create_text_ctrl(path, proj_id, lexer), path.split(/[\/\\]/).last, true, @page_bmp )
self.setCurSci
@sci.load_file(path)
@sci.scroll_to_line(line_nbr)
self.toggleSaved(@sci.saved = true)
@sci.styleDocument
else
@sci.goto_line(line_nbr)
end
end
That initial call to styleDocument will prevent further styling calls when scrolling the document directly after opening it, that in turn will prevent the need to style the whole document continuously while scrolling.
I also had to change onStyleNeeded to look like this:
def onStyleNeeded(evt)
unless @old_pos == self.get_current_pos
position = evt.get_position.is_a?(Integer) ? evt.get_position : self.get_text_length
self.styleWithPos(self.get_current_pos - 1, position)
end
@old_pos = self.get_current_pos
end
When we are scrolling the current position will not change, hence we don’t need to style the document since we did that above, right at the point when we opened it. Without the above changes to the method the document would be styled over and over again during scrolling, after an initial change to the document takes place. Scrolling right away would still have worked since no change would’ve taken place after the initial styling event.
Finally, all is truly well!