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
      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
      vis_chrs += 1 if cur_chr =~ /\S/
    nxt_flag = sci.get_fold_level(cur_line) & ~STC_FOLDLEVELNUMBERMASK
    sci.set_fold_level(cur_line, prev_lvl | nxt_flag)

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}
  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)
    self.setFolding(sci, 0, sci.get_text_length)

. . .

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!

Custom folding in wxRuby’s Scintilla


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)
  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)

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.styleWithPos(0, @sci.get_text_length)

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)

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.toggleSaved(@sci.saved = true)

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)
  @old_pos = self.get_current_pos

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!

Related Posts

Tags: , ,