//
// ruby_mode
//
// v 1.1.1
// 2006-Oct-05
//
// Timothy Byrd
// timbyrd@pobox.com
//
// (Please contact me with comments/corrections.)
//
// This is a mode for Ruby files
//
// History:
//     Note: This file is set up to work with Ruby 1.8.2.
//         Don't know what will change in 2.0...
//
//     2006-10-05 v1.1.1 - expand # in backtick strings.
//
//     2006-09-25 Jim added more stuff :)
//		- ALT-SHIFT-F11 will run the specific test case the cursor is in
//		- Added a subroutine that executes a ruby script passed as a string
//		- changed the show_table function to have the ruby script in-line
//		- use args to determine if the output buffer for ALT-F11 (run
//        ruby buffer) will be switch to, and also prompt for paramters
//		  to pass to the script args of 1 will switch to buffer, args of
//        2 will prompt for parameters args of 3 will do both
//		- Added environment variables to any call to a ruby script
//		- added ruby_pound_sign command on #, which will expand #{} when
//		  in a string or regex can be disabled by setting
//		  ruby_expand_pound to 0
//
//	   2006-09-22 - v1.1.0 - Jim Morris added:
//		 - ri popup of currently selected word, or prompt for ri search
//		   on ALT-F1
//       - ctrl-alt-v toggles between rails view and controller
//		 - alt-F11 runs the selected buffer through ruby and displays
//		   results in pop-up
//       - Made it so end reindents when the d is typed rather than
//         when return is typed
//       - Added a show_table function for rails
//
//     2005-Apr-13 - v1.0.1 - Give backquoted strings own color classes.
//         Add color class for built-in classes (e.g. Array)
//
//     2005-Apr-06 - Added ruby_delete_trailing_spaces variable
//
//     2005-Apr-05 - Initial version: Syntax coloring and smart
//         indenting (shift-tab to unindent)
//
//
// See the user variables below for info on how the behavior of
// ruby-mode can be customized.
//

#if 0
// Add these lines to colclass.txt to get descriptions in set-color

ruby-brace	Square and curly braces in Ruby, i.e., [ ] { }.
ruby-class	A pre-defined class or module in Ruby, e.g., Array.
ruby-class-var	A class variable in Ruby, e.g., @length
ruby-comment	A comment in Ruby.
ruby-global	A standard global constant in Ruby, e.g., ARGV.
ruby-global-var	A global variable in Ruby, e.g., $myGlobal
ruby-instance-var	A class instance variable in Ruby, e.g., @length
ruby-keyword	A keyword in Ruby, e.g., def, class or module.
ruby-number	A number or :symbol in Ruby.
ruby-perl-var	A Perl-style variable in Ruby, e.g., $&.
ruby-punctuation	Punctuation in Ruby.
ruby-regexp	A regular expression in Ruby.
ruby-shell-cmd	A shell command string in Ruby, e.g. `cd mySubdir`.
ruby-shell-subst	A substitution in a Ruby shell command, e.g. `cd #{subDir}`.
ruby-str-subst	A string substitution in Ruby, e.g., "i = #{i}".
ruby-string	A string in Ruby.

#endif

//
// Here are the colors I (Timothy) am using at the moment:
//
//     color_class ruby_brace 0x725ceb on black;
//     color_class ruby_class 0xd9d240 on black;
//     color_class ruby_comment grey on black;
//     color_class ruby_global 0xffb737 on black;
//     color_class ruby_keyword 0xff8000 on black;
//     color_class ruby_number 0xff9090 on black;
//     color_class ruby_punctuation yellow on black;
//     color_class ruby_regexp 0x007fff on black;
//     color_class ruby_shell_cmd = 0x8fff2f on black;
//     color_class ruby_shell_subst = 0x8fff8f on black;
//     color_class ruby_str_subst 0xffc0c8 on black;
//     color_class ruby_string cyan on black;
//     color_class ruby_perl_var red on black;
//

#include "eel.h"
#include "proc.h"
#include "colcode.h"
#include "perl.h"
#include "c.h"

user buffer short ruby_indent = 4;       /* indent by this amt; 0 means use tab size */

user char ruby_indent_with_tabs = 0;
user char ruby_reindent_previous_line = 1;
user int reindent_after_ruby_yank = 20000;
user int ruby_expand_pound = 1; // set to 0 to disable the # expansion in strings

// Should a lone } or ] be unindented?
//
// Closeback = 1:
//     def foo
//         a = [
//             1,
//             2,
//             3
//         ]
//         a.each {|x|
//             puts x
//         }
//     end
//
// Closeback = 0:
//     def foo
//         a = [
//             1,
//             2,
//             3
//             ]
//         a.each {|x|
//             puts x
//             }
//     end
//
user char ruby_closeback = 1;

user char compile_ruby_cmd[128] = "ruby \"%r\""; // run Ruby on this file.

user char auto_show_ruby_delimiters = 1;
user char ruby_auto_show_delim_chars[20] = "{[()]}";

user char ruby_delete_trailing_spaces = 1;

/* Use the c-mode variables for now */
user char Matchdelim = 1;
buffer char in_shell_buffer;

//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////

char _ruby_mode_name[] = "Ruby";
keytable ruby_tab;                       /* key table for ruby mode */

color_class ruby_brace = color_class perl_constant;
color_class ruby_class = color_class perl_variable;
color_class ruby_class_var = color_class perl_variable;
color_class ruby_comment = color_class perl_comment;
color_class ruby_global = color_class perl_variable;
color_class ruby_global_var = color_class perl_variable;
color_class ruby_instance_var = color_class perl_variable;
color_class ruby_keyword = color_class perl_keyword;
color_class ruby_number = color_class perl_constant;
color_class ruby_perl_var = color_class perl_constant;
color_class ruby_punctuation = color_class perl_constant;
color_class ruby_regexp = color_class perl_function;
color_class ruby_shell_cmd = color_class perl_constant;
color_class ruby_shell_subst = color_class perl_constant;
color_class ruby_str_subst = color_class perl_string;
color_class ruby_string = color_class perl_string;


#define RUBY_IDENT    "(@?@?|<Dollar>?)[a-zA-Z_][a-zA-Z0-9_]*"
#define RUBY_NAME RUBY_IDENT "[!?=]?"

// This is now global so it can be shared between
// ruby_func_name_finder() and ruby_toggle_view_controller()
//
char defname[FNAMELEN];


//
// Numbers:
//
// 123 1_234 123.45 1.2e-3 0xffff (hex) 0b01011 (binary) 0377 (octal)
// ?a       ASCII character
// ?\\      ASCII value of backslash
// ?\C-a    Control-a
// ?\M-a    Meta-a
// ?\M-\C-a Meta-Control-a
// :symbol  Integer corresponding to identifiers, variables, and operators.
//

#define R_INT "[1-9](_?[0-9])*"
#define R_INT0 "[0-9](_?[0-9])*"

#define R_DEC "-?(" R_INT "|0)(%." R_INT0 ")?" "([eE]-?" R_INT0 ")?"
#define R_OCT "0(_?[0-7])*"
#define R_HEX "0x[0-9a-fA-F](_?[0-9a-fA-F])*"
#define R_CHA "%?(\[MC]-)*([^ \\]|\\\\|\\s)"
#define R_SYM ":[a-zA-Z_][a-zA-Z0-9_]*"

#define R_NUM "(" R_DEC "|" R_OCT "|" R_HEX "|" R_CHA "|" R_SYM ")"


//
// Perl-style variables for convenience and obfuscation...
//
// $!         The exception information message set by 'raise'.
// $@         Array of backtrace of the last exception thrown.
// $&         The string matched by the last successful pattern match in this scope.
// $`         The string to the left  of the last successful match.
// $'         The string to the right of the last successful match.
// $+         The last bracket matched by the last successful match.
// $1         The Nth group of the last successful match. May be > 1.
// $~         The information about the last match in the current scope.
// $=         The flag for case insensitive, nil by default.
// $/         The input record separator, newline by default.
// $\         The output record separator for the print and IO#write. Default is nil.
// $,         The output field separator for the print and Array#join.
// $;         The default separator for String#split.
// $.         The current input line number of the last file that was read.
// $<         The virtual concatenation file of the files given on command line.
// $>         The default output for print, printf. $stdout by default.
// $_         The last input line of string by gets or readline.
// $0         Contains the name of the script being executed. May be assignable.
// $*         Command line arguments given for the script sans args.
// $$         The process number of the Ruby running this script.
// $?         The status of the last executed child process.
// $:         Load path for scripts and binary modules by load or require.
// $"         The array contains the module names loaded by require.
// $-0        The alias to $/.
// $-a        True if option -a is set. Read-only variable.
// $-d        The alias to $DEBUG.
// $-F        The alias to $;.
// $-i        In in-place-edit mode, this variable holds the extention, otherwise nil.
// $-I        The alias to $:.
// $-l        True if option -l is set. Read-only variable.
// $-p        True if option -p is set. Read-only variable.
// $-v        The alias to $VERBOSE.
//
// These will count as pre-defined globals:
//
// $DEBUG     The status of the -d switch.
// $FILENAME  Current input file from $<. Same as $<.filename.
// $LOAD_PATH The alias to $:.
// $stderr    The current standard error output.
// $stdin     The current standard input.
// $stdout    The current standard output.
// $VERBOSE   The verbose flag, which is set by the -v switch.
//
#define R_PERL_VAR "%$[!@&`'+~=/\\,;.<>_*$?:\"]|%$[0-9]+" \
                   "|%$-[0adFiIlpv]"
//                     "|%$(DEBUG|FILENAME|LOAD_PATH|stderr|stdin|stdout|VERBOSE)"


//
// Sample Ruby strings:
//
// 'string'
//
// 'no #{subst}'
// %q!no #{subst}!
//
// "#{subst}, #{$subst}, #{@subst}, and backslashes\n"
// %Q(#{subst} and backslashes)
//
// %!#{subst} and backslashes!
// %[paired [delimiters] can nest]
//
// `echo command interpretation with #{subst} and backslashes`
// %x/echo command interpretation with #{subst} and backslashes/
//

ruby_parse_string(int t)
{
    int level = 0;
    char delim = character(t);
    char can_nest = 0;
    char can_replace = 1;
    char shell_string = '`' == delim;

    if (strchr("\"'`", delim)) {
        can_replace = delim != '\'';
        point = t + 1;
    }
    else if (delim != '%') {
//        say("ruby_parse_string() - unknown string start '%c'", delim);
        return;
    }
    else if (strchr("qQx", character(t+1))) {
        can_replace = character(t+1) != 'q';
        shell_string = character(t+1) == 'x';
        delim = character(t+2);
        point = t + 3;
    }
    else {
        delim = character(t+1);
        point = t + 2;
    }

    char search_str[30];
    strcpy(search_str, "[");

    if (delim == '(') {
        can_nest = delim;
        delim = ')';
        strcat(search_str, "()");
    }
    else if (delim == '<') {
        can_nest = delim;
        delim = '>';
        strcat(search_str, "<>");
    }
    else if (delim == '{') {
        can_nest = delim;
        delim = '}';
        strcat(search_str, "{}");
    }
    else if (delim == '[') {
        can_nest = delim;
        delim = ']';
        // close bracket must come first in char group
        strcat(search_str, "][");
    }
    else if (!strchr("~!@#$%^&*_+-=|\\:;,./?)]}>'\"`", delim)) {
//        say("ruby_parse_string() - unknown delim '%c'", delim);
        return;
    }
    else {
        int len = strlen(search_str);
        search_str[len] = delim;
        search_str[len+1] = '\0';
    }

    if (can_replace && delim != '#') {
        strcat(search_str, "#");
    }

    strcat(search_str, "\\]");

    // set the color classes to use
    //
    int cc_str, cc_subst;
    if (shell_string) {
        cc_str = color_class ruby_shell_cmd;
        cc_subst = color_class ruby_shell_subst;
    }
    else {
        cc_str = color_class ruby_string;
        cc_subst = color_class ruby_str_subst;
    }

//    say("ruby_parse_string() search_str = '%s'", search_str);

    while (re_search(1, search_str)) {

        char ch = character(point - 1);

        if (ch == '\\') {
            ++point;
        }
        else if (ch == can_nest) {
            ++level;
        }
        else if (can_replace && ch == '#'
            && character(point) == '{')
        {
            set_character_color(t, point, cc_str);
            t = point - 1;
            search(1, "}");
            set_character_color(t, point, cc_subst);
//            say("ruby_parse_string() replace code - %d to %d", t, point);
            t = point;
        }
        else if (ch == delim) {
            --level;
            if (level < 0) {
                break;
            }
        }
        else if (can_replace && ch == '#') {
            // do nothing - just to avoid the message below
        }
        else {
            // This implies the pattern was malformed
            say("ruby_parse_string() unknown '%c' - %d, %d",
                ch, t, point);
        }
    }

    set_character_color(t, point, cc_str);
}

// `/' any_char* `/'[`e'|`i'|`m'|`n'|`o'|`s'|`u'|`x']
// `%'`r' char any_char* char
//
// /normal regex/i
// %r|alternate form|
//
//    /i         case insensitive
//    /m         multiline mode - '.' will match newline
//    /x         extended mode - whitespace is ignored
//    /o         perform #{...} substitutions just once
//    /[neus]    encoding: none, EUC, UTF-8, SJIS, respectively
//
int ruby_parse_regexp(int t)
{
    int level = 0;
    char delim = character(t);
    char can_nest = 0;

    if ('/' == delim) {
        point = t + 1;
    }
    else if (delim != '%' || character(t+1) != 'r') {
//         say("ruby_parse_regexp() - unknown Regexp start '%c'",
//             delim);
        return 0;
    }
    else {
        delim = character(t+2);
        point = t + 3;
    }

    char search_str[30];
    strcpy(search_str, "[]-[\\\n");

    if (delim == '(') {
        can_nest = delim;
        delim = ')';
        strcat(search_str, "()");
    }
    else if (delim == '<') {
        can_nest = delim;
        delim = '>';
        strcat(search_str, "<>");
    }
    else if (delim == '{') {
        can_nest = delim;
        delim = '}';
        strcat(search_str, "{}");
    }
    else if (delim == '[') {
        can_nest = delim;
        delim = ']';
        // square brackets already in search string
    }
    else if (!strchr("~!@#$%^&*_+-=|\\:;,./?)]}>'\"`", delim)) {
//        say("ruby_parse_regexp() - unknown delim '%c'", delim);
        return 0;
    }
    else if (!strchr(search_str, delim)) {
        int len = strlen(search_str);
        search_str[len] = delim;
        search_str[len+1] = '\0';
    }

    strcat(search_str, "]");

//    say("ruby_parse_regexp() search_str = '%s'", search_str);

    while (re_search(1, search_str)) {

        char ch = character(point - 1);

        if (ch == '\\') {
            ++point;
        }
        else if (ch == '\n') {
            return 0;
        }
        else if (ch == '[') {
            int ps = point;
            if (re_search(1, "[^\\][]]")) {
            }
            else {
                point = ps;
                to_end_line();
                break;
            }
        }
        else if (ch == can_nest) {
            ++level;
        }
        else if (ch == delim) {
            --level;
            if (level < 0) {
                if (matches_at(point, 1, "[eimnosux]+")) {
                  point = matchend;
                }
                break;
            }
        }
        else if (strchr("]-", ch)) {
            // do nothing - just to avoid the message below
        }
        else {
            // This implies the pattern was malformed
            say("ruby_parse_regexp() unknown '%c' - %d, %d (%d:%d)",
                ch, t, point,
                lines_between(0, point, 0)+1, get_column(point)+1);
        }
    }

    set_character_color(t, point, color_class ruby_regexp);
    return 1;
}


//
// Pre-defined global constants
//
// ttp://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/variable.html
//
// TRUE              The typical true value.
// FALSE             The false itself.
// NIL               The nil itself.
// STDIN             The standard input. The default value for $stdin.
// STDOUT            The standard output. The default value for $stdout.
// STDERR            The standard error output. The default value for $stderr.
// ENV               The hash contains current environment variables.
// ARGF              The alias to the $<.
// ARGV              The alias to the $*.
// DATA              The file object of the script, pointing just after __END__.
// RUBY_VERSION      The ruby version string (VERSION was depricated).
// RUBY_RELEASE_DATE The relase date string.
// RUBY_PLATFORM     The platform identifier.
//
// Also including these as pre-defined globals:
//
// $DEBUG     The status of the -d switch.
// $FILENAME  Current input file from $<. Same as $<.filename.
// $LOAD_PATH The alias to $:.
// $stderr    The current standard error output.
// $stdin     The current standard input.
// $stdout    The current standard output.
// $VERBOSE   The verbose flag, which is set by the -v switch.
//

//
// Built-in functions
//
// http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/function.html
//
// Some methods defined in the Kernel module can be called from
// everywhere, and are to be called like functions. You'd better think
// twice before redefining these methods.
//
// Array(arg)               Convert argument to array using to_a.
// Float(arg)               Convert argument to float value.
// Integer(arg)             Convert argument to integer value.
// String(arg)              Convert argument to string using Kernel#to_s.
// at_exit                  Register block for clean-up.
// autoload(module, file)   Specifies file to autoload to access module.
// binding                  Returns variable/method binding.
// caller([level])          Returns context information (the backtrace)
// catch(tag){...}          Executes block, catching given throws.
// chop                     Returns $_ with last character removed.
// chop!                    Self-modifying chop.
// chomp([rs])              Returns $_ with line ending removed.
// chomp!                   Self-modifying chomp.
// eval(expr)               Evaluate expr as a Ruby program.
// exec(command...)         Executes command as subprocess, never returns.
// exit([status])           Exits immediately with status.
// exit!                    Exits, ignoring exception handling.
// fork                     Does a fork(2) system call.
// gets([rs])               Read string from input into $_.
// readline([rs])           Read string from input into $_.
// global_variables         Returns list of global variable names.
// gsub(pattern[, replace]) RegExp substitution on $_.
// gsub!                    Self-modifying gsub.
// iterator?                Returns true, if called inside a block.
// load(file[, priv])       Loads and evaluates the Ruby program in file.
// local_variables          Returns list of local variable names.
// loop                     Loops forever (until terminated explicitly).
// open(file[, mode])       Opens the file, and returns a File object.
// p(obj)                   Prints human-readable form of obj to stdout.
// print(arg1...)           Prints arguments (or $_).
// printf(...)              Formatted printing.
// proc                     Makes a block into a procedure object.
// lambda                   Makes a block into a procedure object.
// putc(c)                  Writes the char c to the default output ($>).
// raise                    Raises an exception.
// fail                     Raises an exception.
// rand(max)                Random integer in [1..max-1]
// readlines([rs])          Reads input into an array of lines.
// require(feature)         Demands library file specified by feature.
// select()                 Calls select(2) system call.
// sleep([sec])             Sleep for sec seconds.
// split([sep[, limit]])    Splits a string into an array.
// format(format...)        Returns string formatted like sprintf.
// sprintf(format...)       Returns formatted string.
// srand([seed])            Sets the random number seed for the rand.
// sub(pattern[, replace])  Returns $_ with a single RegExp sub performed.
// sub!                     Self-modifying form of sub.
// syscall(num, arg...)     Makes a system call as specified.
// system(command...)       Perform command in a sub-process.
// test(cmd, file [, file]) Does a file test.
// throw(tag[, value])      Casts an non-local exit to enclosing catch.
// trace_var()              Set hook to execute on var change.
// trap(signal, command)    Specifies the signal handler for the signal.
// untrace_var()            Deletes trace_var hook.
//


//
// Pre-defined classes and modules
//
// http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/classes.html
//
// Object
//     Array
//     Data
//     Dir
//     Exception
//         Interrupt
//         NotImplementError
//         SignalException
//         StandardError
//             ArgumentError
//             FloatDomainError
//             IOError
//                 EOFError
//             IndexError
//             LoadError
//             LocalJumpError
//             NameError
//             RuntimeError
//             SecurityError
//             SyntaxError
//             SystemCallError
//                 Errno::E*
//             SystemStackError
//             ThreadError
//             TypeError
//             ZeroDivisionError
//         SystemExit
//         fatal
//     Hash
//     IO
//         File
//     MatchingData
//     Module
//         Class
//     Numeric
//         Integer
//             Bignum
//             Fixnum
//         Float
//     Proc
//     Range
//     Regexp
//     String
//     Struct
//     Time
//     NilClass
// Comparable
// Enumerable
// Errno
// FileTest
// GC
// Kernel
// Marshal
// Math
// ObjectSpace
// Precision
// Process
//

// Return code for word from here to point (something with alpha or
// digits).
//
// 0 = unknown (identifier?)
// 1 = Keyword
// 2 = Regexp
// 3 = Pre-defined global constants, e.g., ARGV.
// 4 - Pre-defined classes and modules
// 5 - Built-in functions
// 6 - Common methods
// 7 - Global Variable, e.g., $myGlobal
// 8 - Class instance variable, e.g., @length
// 9 - Class variable, e.g., @@count
//
is_ruby_keyword(int from)
{
    char buf[500], *s;

    save_var case_fold = 0;

    if (point - from > sizeof(buf) / sizeof(char) - 10)
        save_var point = from + sizeof(buf) / sizeof(char) - 10;

    // put identifier into string with '|' chars on either side
    //
    buf[0] = '|';
    grab(from, point, buf + 1);
    strcat(buf, "|");

    // If it follows a period, don't count it as a keyword.
    //
    char follows_period = 0; // matches_at(from, -1, "%.[ \t]*");

    // Keywords (mostly reserved)
    //
    if (!follows_period && strstr(
        "|__FILE__|__LINE__|alias|and|BEGIN|begin|break|case|catch"
        "|class|def|defined?|do|else|elsif|END|end|ensure|false|for"
        "|if|in|include|initialize|loop|module|new|next|nil|not|or"
        "|private|protected|public|raise|redo|require|rescue|retry"
        "|return|self|super|then|throw|true|undef|unless|until|when"
        "|while|yield"
        "|", buf))
    {
        return 1;
    }

    // Special case: highlight this class name as if it were a Regexp
    // itself.
    //
    if (!strcmp("|Regexp|", buf)) {
        return 2;
    }

    // Pre-defined global constants
    //
    if (strstr(
        "|TRUE|FALSE|NIL|STDIN|STDOUT|STDERR|ENV|ARGF|ARGV|DATA"
        "|RUBY_VERSION|RUBY_RELEASE_DATE|RUBY_PLATFORM"
        "$DEBUG|$FILENAME|$LOAD_PATH|$stderr|$stdin|$stdout|$VERBOSE"

        "|", buf))
    {
        return 3;
    }

    // Pre-defined classes and modules
    //
    if (strstr(
        "|ArgumentError|Array|Bignum|Class|Comparable|Data|Dir"
        "|Enumerable|EOFError|Errno|Exception|fatal|File|FileTest"
        "|Fixnum|Float|FloatDomainError|GC|Hash|IndexError|Integer"
        "|Interrupt|IO|IOError|Kernel|LoadError|LocalJumpError|Marshal"
        "|MatchingData|Math|Module|NameError|NilClass"
        "|NotImplementError|Numeric|Object|ObjectSpace|Precision|Proc"
        "|Process|Range|Regexp|RuntimeError|SecurityError"
        "|SignalException|StandardError|String|Struct|SyntaxError"
        "|SystemCallError|SystemExit|SystemStackError|ThreadError|Time"
        "|TypeError|ZeroDivisionError"
        "|", buf))
    {
        return 4;
    }

    // Built-in functions
    // (Some items, e.g. Array, overlap previous sections)
    //
    if (strstr(
        "|Array|at_exit|autoload|binding|caller|chomp|chomp!|chop"
        "|chop!|eval|exec|exit|exit!|fail|Float|fork|format|gets"
        "|global_variables|gsub|gsub!|Integer|iterator?|lambda|load"
        "|local_variables|open|open|p|print|printf|proc|putc|putc|rand"
        "|readline|readlines|select|sleep|split|sprintf|srand|String"
        "|sub|sub!|syscall|system|test|trace_var|trap|untrace_var"
        "|", buf))
    {
        return 5;
    }

    // Common methods
    //
    if (strstr(
        "|each|each_with_index|load|to_f|to_i|to_s"
        "|", buf))
    {
        return 6;
    }

    // Global variables
    //
    if ('$' == buf[1]) {
        return 7;
    }

    // Global variables
    //
    if ('@' == buf[1]) {
        return ('@' == buf[2]) ? 8 : 9;
    }

    return 0;
}


ruby_parse_command(int t)
{
    point = t;
    point += parse_string(1, "[ \t]*");
    set_character_color(t, point, -1);
    t = point;
    if (parse_string(1, "#")) {
        to_end_line();
        set_character_color(t, point, color_class ruby_comment);
    }
    else if (parse_string(1, "=begin")) {
        re_search(1, "^[ \t]*=end");
        to_end_line();
        set_character_color(t, point, color_class ruby_comment);
    }
    else if (parse_string(1, "=end")) {
        to_end_line();
        t = point;
        re_search(-1, "^[ \t]*=begin");
        to_begin_line();
        set_character_color(point, t, color_class ruby_comment);
        point = t;
    }
    // Take out %=string= and %$string$ for now...
    //
    else if (matches_at(point, 1,
        "[\"'`]|(%%[]-qQx{<[()>}~!@#%^&*_+%|\\:;,./?])"))
    {
        ruby_parse_string(point);
    }
    else if (matches_at(point, 1, R_NUM)) {
        point = matchend;
        set_character_color(t, point, color_class ruby_number);
    }
    else if (matches_at(point, 1, R_PERL_VAR)) {
        point = matchend;
        set_character_color(t, point, color_class ruby_perl_var);
    }
    else if (matches_at(point, 1, "/|(%%r)")
        && !matches_at(point, RE_REVERSE, "(<^c:*keyword>" RUBY_IDENT "|" R_NUM "|[])])[ \t]*" ))
    {
        ruby_parse_regexp(point);
    }
    else if (parse_string(1, RUBY_IDENT "[!?]?")) {
        point = matchend;
        switch (is_ruby_keyword(t)) {
        case 1:
            set_character_color(t, point, color_class ruby_keyword);
            break;
        case 2:
            set_character_color(t, point, color_class ruby_regexp);
            break;
        case 3:
            set_character_color(t, point, color_class ruby_global);
            break;
        case 4:
            set_character_color(t, point, color_class ruby_class);
            break;
        case 7:
            set_character_color(t, point, color_class ruby_global_var);
            break;
        case 8:
            set_character_color(t, point, color_class ruby_instance_var);
            break;
        case 9:
            set_character_color(t, point, color_class ruby_class_var);
            break;
        // Ignore 5 & 6 for now, until I decide what I want to do.
        //
        default:
            set_character_color(t, point, -1);
            break;
        }
    }
    else if (strchr("{}[]", character(point))) {
        ++point;
        set_character_color(t, point, color_class ruby_brace);
    }
    else if (matches_at(point, 1, "::|[]-`~!@#$%^&*()_=+[\\|;':\"<>?,./]"))
    {
        point = matchend;
        set_character_color(t, point, color_class ruby_punctuation);
    }
    else {
       say("ruby_parse_command() unknown '%c' - %d (%d:%d)",
           character(point), point,
           lines_between(0, point, 0)+1, get_column(point)+1);
        ++point;
    }
}


color_ruby_range(from, to)
{
    if (from >= to)
        return to;
    save_var point, matchstart, matchend;
    point = to;             // Color entire lines.
    nl_forward();
    to = point;
    set_character_color(from, to, -1);
    point = from;
    ruby_before_continuation();

    int t = point;

    while (point < to) {
        if (!re_search(1, "[^ \t\n]")) {
            t = size();
            break;
        }
        t = matchstart;
        ruby_parse_command(t);
    }
    if (to < t)
        set_character_color(to, t, -1);
    return point;
}


// Indent the following line more?
//
// Typically true for lines following one of:
//
//     def | class | module | if | elsif | else | unless | while |
//     until | case | when | begin | for | rescue
//
int ruby_indent_more()
{
    if (parse_string(1, "(^|.*;|.*=)[ \t]*<c:*keyword></word>"
        "(def|class|module|if|elsif|else|unless|while|until|case|when|begin|for|rescue|ensure)</word>")) {
        if (!parse_string(1, ".*<c:*keyword></word>end</word>"))    // Multiple statements: ignore block.
            return 1;
        else
            return 0;
    }

    // Check for braces/brackets
    //
    char* brace_pat = "<FB><Min>.*(<c:*brace>[][}{]|<c:*punctuation>[)(]|<c:*keyword></word>(do)</word>)";
    if (parse_string(1, brace_pat)) {
        int count = 0;
        do {
            // opening brace or 'o' in 'do'
            //
            if (strchr("[{(o", character(matchend - 1))) {
                ++count;
            }
            else {
                --count;
            }
        } while (matches_at(matchend, 1, brace_pat));
        if (ruby_closeback && count < 0)
            ++count;
        return count;
    }

    // Odd case of an 'end' embedded in the line.
    //
    if (parse_string(1, ".*;[ \t]*<c:*keyword></word>end</word>")) {
        return -1;
    }

    return 0;
}


// Indent *this* line less?
//
// True for lines starting with one of:
//
//     end | when | else | elsif | rescue
//
// If ruby_closeback is set then also true for lines starting with a
// closing brace/bracket.
//
int ruby_indent_less()
{
    char* brace_pat = "<FB><Min>[ \t]*(<c:*brace>[]}]|<c:*punctuation>[)])";
    if (ruby_closeback && parse_string(1, brace_pat)) {
        return 1;
    }

    return parse_string(1,
//        "(.*;)?"
        "[ \t]*<c:*keyword></word>("
        "end|when|else|elsif|rescue|ensure"
        ")</word>") != 0 ? 1 : 0;
}


// Is this a continuation line?
//
ruby_is_continuation()
{
    save_var point;
    to_begin_line();

    // leave out '|' since 'array.each { |pair|' is so common.
    //
    return parse_string(RE_REVERSE, "(\\|(<c:*punctuation>[-%&*+@/<=>^~])[ \t]*)\n");
}


// If this is a continuation line, move back to the actual start.
//
ruby_before_continuation()
{
    while (ruby_is_continuation())
        nl_reverse();
    to_begin_line();
}


ruby_step()
{
    return (ruby_indent > 0) ? ruby_indent : tab_size;
}


// Indent line at orig based on this one.
//
ruby_indent_continuation(orig)
{
    int ind;

    ruby_before_continuation();
    ind = get_indentation(point);
    point = orig;
    indent_to_column(ind + 2 * ruby_step());
}


do_ruby_indent() on ruby_tab['\t']
{
    if (maybe_indent_rigidly(0))
        return;
    if (this_cmd != CMD_INDENT_REG)
        this_cmd = C_INDENT;
    if (current_column() > get_indentation(point) || prev_cmd == C_INDENT) {
        indent_like_tab();
        return;
    }
    ruby_indenter();
}


ruby_indenter()
{
    int orig = point, ind;

    recolor_buffer_range(0, size());

    if (matches_at(give_begin_line(), 1, "=begin|=end")
        || point <= narrow_start) {
        indent_to_column(0);
        return;
    }
    if (ruby_is_continuation()) {
        ruby_indent_continuation(orig);
        return;
    }
    do {
        to_begin_line();
        if (!re_search(-1, "[^ \t\n]")) /* Find previous non-blank line */
            break;
        to_indentation();
    } while (parse_string(1, "#")); // that's not a comment.
    ruby_before_continuation();
    ind = get_indentation(point);
    ind += ruby_step() * ruby_indent_more();
    point = orig;
    if (ruby_indent_less())
        ind -= ruby_step();
    to_indentation();       /* go to current line's indent */
    indent_to_column(ind);
}


// move back to nearest tab stop
// unindent if in the initial whitespace of the line
//
command ruby_unindent() on ruby_tab[NUMSHIFT(GREYTAB)]
{
    int tab = get_soft_tab_size(), old, old_point;

    if (maybe_indent_rigidly(1))
        return;
    old = current_column();
    old_point = point;
    to_indentation();
    if (point >= old_point) {
        old_point = point;
        move_to_column(((current_column() - 1) / tab) * tab);
        delete(old_point, point);
        return;
    }
    else {
        point = old_point;
    }
    move_to_column(((current_column() - 1) / tab) * tab);
    if (old && old == current_column())
        point--;
}


// fix indentation if necessary when } or ) is typed
//
command ruby_close() on ruby_tab['}'], ruby_tab[')'], ruby_tab[']']
{
    // only if typed inside indentation, & it might need fixup
    //
    normal_character();
    if (current_column() - 1 <= get_indentation(point))
    {
        fix_ruby_indentation();
    }
    save_var inside_show_matching_delimiter = 1;
    if (Matchdelim)
        find_delimiter();
}


// ++[TB] 9/22/2006 - v 1.1.0 additions by Jim Morris
// indent if the d of end is typed so end indents immediately like } does
//
command ruby_maybe_indent_end() on ruby_tab['d'], ruby_tab['e'], ruby_tab['f'], ruby_tab['n']
{
    normal_character();
    recolor_range(give_begin_line(), point);
    if (matches_at(point, -1,
        "<c:*keyword>(end|when|else|elsif|rescue|ensure)"))
        fix_ruby_indentation();
}
// [TB]++ 9/22/2006 - v 1.1.0 additions by Jim Morris


// recompute this line's indentation without moving point
//
fix_ruby_indentation()
{
    if (in_shell_buffer)
        return;
    save_spot point;
    to_indentation();
    ruby_indenter();
}


command ruby_mode()
{
    mode_default_settings();
    mode_keys = ruby_tab;       /* Use these keys. */
    major_mode = _ruby_mode_name;
    compile_buffer_cmd = compile_ruby_cmd;  // can compile this
    strcpy(comment_start, "#[ \t]*");
    strcpy(comment_pattern, "#.*$");
    strcpy(comment_begin, "# ");
    strcpy(comment_end, "");
    recolor_range = color_ruby_range;   // set up coloring rules
    recolor_from_here = recolor_from_top;
    if (want_code_coloring)     // maybe turn on coloring
        when_setting_want_code_coloring();
    if (auto_show_ruby_delimiters)
        auto_show_matching_characters = ruby_auto_show_delim_chars;
    indent_with_tabs = ruby_indent_with_tabs;
    indenter = ruby_indenter;
    auto_indent = 1;
    fill_mode = misc_language_fill_mode;

    try_calling("ruby-mode-hook");
    drop_all_colored_regions();
    make_mode();
}


when_loading()
{
    ruby_tab[ALT('q')] = (short) find_index("fill_comment");
}


// show_debug_text(int from, int to, char* message, int level)
// {
//     char buf[100];
//     to = MIN(to, from + ptrlen(buf) - 1);
//     grab(from, to, buf);
//     say("%s '%s' at %d (%d:%d) level %d", message, buf, from,
//         lines_between(0, from, 0)+1, get_column(from)+1, level);
// }


// Set display_func_name to the name of the function we're editing, and
// return 1.  If not in a function, set display_func_name to "" and
// return 1.  If user pressed a key and we gave up for now, return 0.
//
ruby_func_name_finder()
{
    int from, to;

    char classname[FNAMELEN];
    // char defname[FNAMELEN];
    *classname = '\0';
    *defname = '\0';

    char buf[FNAMELEN];

    char isclass = 1;

    int level = 0;

    save_var point, case_fold = 0;

    // if we are at the start of a 'class' or 'def' line, it'll count
    // as being inside the class or method, so go to the end of the
    // line to enable the search function to find the keyword.
    //
    to_end_line();

    int startingLine = lines_between(0, point, 0);

// For things like "def <=>(other)"
//#define RUBY_METH RUBY_NAME
#define RUBY_METH "[^ \t\n(;]+"

    while (do_color_searching(SEARCH_REGEX | SEARCH_REVERSE,
        "<c:*keyword>(</word>(def|class|module)</word>[ \t\n]+"
        "(" RUBY_METH "))"
        "|<c:*keyword></word>(end|do)</word>"
        "|(^|;|=)[ \t]*<c:*keyword></word>("
        "if|unless|case|while|until|begin|for"
        ")</word>"))
    {
        grab(matchend, MIN(matchstart, matchend + ptrlen(buf) - 1), buf);

        if (!strncmp(buf, "end", 3)) {
            // Ignore one 'end' on the starting line.
            // This allows the 'end' line for a class or method to
            // count as part of the class or method.
            //
            if (lines_between(0, matchstart, 0) == startingLine) {
                startingLine = -1;
            }
            else {
                ++level;
            }
        }
        else if (!strncmp(buf, "class", 5) || !strncmp(buf, "module", 6)) {
            --level;
            if (level < 0) {
                isclass = buf[0] == 'c';
                from = find_group(3, 1);
                to = find_group(3, 0);
                to = MIN(to, from + ptrlen(classname) - 1);
                grab(from, to, classname);
                break;
            }
        }
        else if (!strncmp(buf, "def", 3)) {
            if (level <= 0 && !*defname) {
                from = find_group(3, 1);
                to = find_group(3, 0);
                if (matches_at(to, 1, "(%.|::)(" RUBY_NAME ")" )) {
                    from = find_group(2, 1);
                    to = find_group(2, 0);
                }
                to = MIN(to, from + ptrlen(defname) - 1);
                grab(from, to, defname);
            }
            else {
                --level;
            }
        }
        else {
            // prevent plain old scripting outside a class/def from
            // picking up a name on the same level..
            //
            if (level > 0)
                --level;
            if (buf[0] != ';')
                do_color_searching(SEARCH_REGEX | SEARCH_REVERSE,
                    "(\n|<c:*punctuation>;)");
        }
    }

    if (*classname && *defname) {
        if (strlen(classname) + strlen(defname) + 10 >= ptrlen(display_func_name)) {
            sprintf(display_func_name, "def ...::%.*s", ptrlen(display_func_name) - 10, defname);
        }
        else {
            sprintf(display_func_name, "def %s::%s", classname, defname);
        }
    }
    else if (*classname) {
        sprintf(display_func_name, "%s %.*s",
            isclass ? "class" : "module",
            ptrlen(display_func_name) - 10, classname);
    }
    else if (*defname) {
        sprintf(display_func_name, "def %.*s", ptrlen(display_func_name) - 10, defname);
    }
    else {
        *display_func_name = 0;
    }
    return 1;
}


tag_mode_ruby()
{
    char func[TAGLEN];
    int start;

    save_var case_fold = 1;
    while (re_search(1, "^[ \t]*(def|class|module)[ \t]+(" RUBY_IDENT "(\\." RUBY_NAME ")?)")) {
        grab(start = find_group(2, 1), find_group(2, 0), func);
        add_tag(func, start);
    }
}


suffix_rb()
{
    ruby_mode();
}


/* Ruby is a line-oriented language. Ruby expressions and statements are
 * terminated at the end of a line unless the statement is obviously
 * incomplete---for example if the last token on a line is an operator
 * or comma. A semicolon can be used to separate multiple expressions on
 * a line. You can also put a backslash at the end of a line to continue
 * it onto the next. Comments start with `#' and run to the end of the
 * physical line. Comments are ignored during compilation.
 *
 * if, unless, case, while, until, begin, for, do (might not be
 * initial), def, class, module
 */

// if bool-expr [then]
//     body
// elsif bool-expr [then]
//     body
// else
//     body
// end
//
// unless bool-expr [then]
//     body
// else
//     body
// end
//
// expr if     bool-expr
//
// expr unless bool-expr
//
// case target-expr
//   when comparison [, comparison]... [then]
//     body
// [else
//   body]
// end
//
// (case comparisons may be regexen)
//
// while bool-expr [do]
//  body
// end
//
// until bool-expr [do]
//  body
// end
//
// begin
//  body
// end while bool-expr
//
// begin
//  body
// end until bool-expr
//
// for name[, name]... in expr [do]
//   body
// end
//
// expr.each do | name[, name]... |
//   body
// end
//
// expr while bool-expr
//
// expr until bool-expr
//
//
// Also significant for indenting:
//
// expr.each { | name[, name]... |
//   body
// }
//
// square bracket for array, curly brace for hash


// ++[TB] 9/22/2006 - v 1.1.0 additions by Jim Morris
/*
 * Get Ruby help using the Ruby ri command
 * ri must be installed on your path
 * whatever is highlighted will be passed as the argument to ri, if
 * nothing is highighted you will prompted for the string to lookup.
 */

command get_ruby_help() on ruby_tab[NUMALT(FKEY(1))]
{
    char helpstr[512];
    char tag[128];

    save_spot point, mark;

    say("");
    fix_region();

    // find the function name to get help on, either use selected
    // region or prompt for it
    if (is_highlight_on() && mark - point < sizeof(tag)){
        grab(point, mark, tag);
        highlight_off();
    }else{
        get_string(tag, "Ruby Help: ");
    }

    sprintf(helpstr, "ri -T %s", tag);
    pipe_text(NULL, "-rubyhelp", helpstr, NULL, PIPE_SYNCH|PIPE_CLEAR_BUF|PIPE_SKIP_SHELL , 0);
    view_buffer("-rubyhelp", 0);
    delete_buffer("-rubyhelp");
}

// these need to be global as per eel manual
char ruby_env1[FNAMELEN];
char ruby_env2[FNAMELEN];

// add some usefull things to the environment that the scripts can use
void add_environment()
{
    sprintf(ruby_env1, "EPS_FILENAME=%s", filename); putenv(ruby_env1);
    if(strlen(defname) > 0){
        sprintf(ruby_env2, "EPS_CURRENT_METHOD=%s", defname); putenv(ruby_env2);
    }else{
        putenv("EPS_CURRENT_METHOD"); // delete it
    }
}

/*
 * Pipe the current buffer through the ruby interpreter and display
 * results in a popup, unless there is an arg where bit 0 is set in which
 * case send results to a visible buffer and switch to it.
 * If there is a an arg and bit 1 is set it will prompt for arguments to send to the script
 */

command ruby_run() on ruby_tab[NUMALT(FKEY(11))]
{
    char rubycmd[512];
    char *outbuf;
    char args[500];
    int showbuf= 0;

    // see if we want to switch to the output buffer
    if(has_arg && (iter & 1) != 0){
        outbuf= "ruby_process";
        showbuf= 1;
    }else
        outbuf= temp_buf();

    strcpy(rubycmd, "ruby - ");

    // see if we need to prompt for arguments
    if(has_arg && (iter & 2) != 0){
        get_str_auto_def(args, "script arguments");
        strcat(rubycmd, args);
    }

    iter= 0;

    // add some useful environment variables
    add_environment();
    pipe_text(bufname, outbuf, rubycmd, NULL, PIPE_SYNCH|PIPE_CLEAR_BUF|PIPE_SKIP_SHELL , 0);
    if(!showbuf){
        view_buffer(outbuf, 0);
        delete_buffer(outbuf);
    }else{
        to_buffer(outbuf);
    }

}

/*
 * Run a specific test case with a ruby test unit, the test case run is the method
 * that the cursor is currently in, it will save the file if needed
 * the results will be displayed in a popup, unless there is an arg in which case send
 * results to a visible buffer and switch to it
 */
command ruby_run_test_case() on ruby_tab[NUMALT(NUMSHIFT((FKEY(11))))]
{
    if(strlen(defname) > 0 && strncmp(defname, "test_", 5) == 0){
        if(is_unsaved_buffer()){
// 			switch (bufed_ask_save()) {
// 				case 0:
// 				case 1: error("Not saved or run");
// 			}
            save_file();
        }

        // add some useful environment variables
        add_environment();
        char rubycmd[512];
        sprintf(rubycmd, "ruby %s --name %s", filename, defname);
        pipe_text(NULL, "ruby-test-run", rubycmd, NULL, PIPE_SYNCH|PIPE_CLEAR_BUF|PIPE_SKIP_SHELL , 0);
        if(has_arg)
            to_buffer("ruby-test-run");
        else
            view_buffer("ruby-test-run", 0);
    }else{
        error("Not in a ruby test case method");
    }
}

/*
 * Run the given string through the ruby interpreter and return the buffer number
 * of where the results are
 * the number of arguments to pass are specifed followed by an array of strings of the arguments
 */
int ruby_execute(char *script, int nargs, char *vals[])
{
    char *inp_buffer= temp_buf();
    int size;
    char cmd[512];
    strcpy(cmd, "ruby - ");
    int i;
    for(i=0;i<nargs;i++){
        strcat(cmd, vals[i]);
        strcat(cmd, " ");
    }

    // add some useful environment variables
    add_environment();
    buf_stuff(name_to_bufnum(inp_buffer), script, strlen(script));
    pipe_text(inp_buffer, "-ruby-execute", cmd, NULL, PIPE_SYNCH|PIPE_CLEAR_BUF|PIPE_SKIP_SHELL , 0);
    return name_to_bufnum("-ruby-execute");
}

// same as above but returns the result as a string
char *ruby_execute_to_string(char *script, int nargs, char *vals[])
{
    int bn= ruby_execute(script, nargs, vals);
    // get the output into a string
    int size= buf_size(bn);
    char *dest= malloc(size+1);
    buf_grab_bytes(bn, 0, size, dest);
    buf_delete(bn);
    return dest;
}


/*
 * Send the selected text to the given command, and return it as a string
 * Similar to filter_region command but returns result as a string instead
 */
char *ruby_region_pipe_to_command(char *cmd)
{
    save_spot point, mark;
    char *inp_buffer= NULL;
    int oldbufnum= bufnum;

    say("");
    fix_region();

    // if a region is selected send it to the command
    if (is_highlight_on() && mark - point > 0){
        inp_buffer= temp_buf();
        xfer(inp_buffer, point, mark);
        highlight_off();
    }

    // run the program with the selection as input
    pipe_text(inp_buffer, "-ruby-pipe-output", cmd, NULL, PIPE_SYNCH|PIPE_CLEAR_BUF|PIPE_SKIP_SHELL , 0);

    // get the output into a string
    bufname= "-ruby-pipe-output";
    char *res= NULL;
    grab_expanding(0, size(), &res, 256);
    bufnum= oldbufnum;
    return res;
}

// the show_table_script defined in-line
char show_table_script_rb[]= "\
    #! /usr/local/bin/ruby\n\
	if ARGV.size < 2\n\
		STDOUT << \"Usage: show_tables model project_path\"\n\
		exit 1\n\
	end\n\
\n\
	word= ARGV[0]\n\
	project= ARGV[1]\n\
\n\
	require \"#{project}/config/boot\"\n\
	require \"#{project}/config/environment\"\n\
\n\
	klass = Object.const_get(word) rescue nil\n\
	if klass and klass.class == Class and klass.ancestors.include?(ActiveRecord::Base)\n\
		columns = klass.columns_hash\n\
\n\
		data = []\n\
		data += [%w[column primary sql_type default]]\n\
		data += [%w[------ ------- -------- -------]]\n\
		data += columns.collect { |col, attrs| [col, attrs.primary.to_s, attrs.sql_type.to_s, attrs.default.to_s] }\n\
\n\
		STDOUT << data.inject('') do |output, array|\n\
			output + array.inject('') { |row_str, value| row_str + value.ljust(20) } + \"\\n\"\n\
		end\n\
	elsif klass and klass.class == Class and not klass.ancestors.include?(ActiveRecord::Base)\n\
		STDOUT << \"#{word} is not an Active Record derived class\"\n\
	else\n\
		STDOUT << \"#{word} was not recognised as a class\"\n\
	end\n";

// Example of how to use ruby_execute_to_string()
//
// command ruby_test()
// {
// 	char *args[2];
// 	args[0]= "User";
// 	args[1]= "/home/morris/work/ruby/rails/testapp";
// 	char *res= ruby_execute_to_string(show_table_script_rb, 2, args);
// 	stuff(res);
// 	free(res);
// }


/*
 * Show columns of the selected Model in a pop up.
 *
 * (Thanks to http://blog.seagul.co.uk/articles/2006/07/14/textmate-command-to-display-active-record-column-attributes)
 * The current buffer must be a file that is either in app/views/aview/... or app/controllers or app/models
 *
 */
command ruby_show_columns()
{
    char model[128];
    char project_path[FNAMELEN];

    save_spot point, mark;

    say("");
    fix_region();

    // find the Model name to get columns for
    // region or prompt for it
    if (is_highlight_on() && mark - point < sizeof(model)){
        grab(point, mark, model);
        highlight_off();
    }else{
        get_string(model, "Model name: ");
    }

    // figure out project path from file we are looking at
    strcpy(project_path, filename);
    char *t= get_tail(project_path);
    char *ext= get_extension(filename);
    if(strcmp(ext, ".rhtml") == 0 || strcmp(ext, ".rjs") == 0){
        // presume we are in a view
        strcpy(t, "../../..");
    }else if(strcmp(ext, ".rb") == 0){
        // presume we are in a controller or model
        strcpy(t, "../..");
    }

    // run the show_columns ruby script which extracts columns for the given model in project path
    char *args[2];
    args[0]= model;
    args[1]= project_path;
    int resbuf= ruby_execute(show_table_script_rb, 2, args);
    view_buf(resbuf, 0);
    buf_delete(resbuf);
}

/*
 * Toggle between the rails view and controller for the current function
 * If it is an .rhtml file then find ../../controllers/[name]-controller.rb where name is the current
 * leaf directory node.
 * If it is a .rb file then find ../views/[name].rhtml where name is the current method
 *
 */
command ruby_toggle_view_controller() on reg_tab[NUMCTRL(NUMALT('v'))]
{
    char name[FNAMELEN];
    char path[FNAMELEN];
    char path2[FNAMELEN];
    char action[132];
    char *ext= get_extension(filename);
    if(strcmp(ext, ".rhtml") == 0 || strcmp(ext, ".rjs") == 0){
        strcpy(action, "def ");
        char *tail= get_tail(filename);
        strncat(action, tail, strlen(tail)-strlen(ext));
        strcpy(name, filename);
        char *taile= rindex(name, path_sep);
        if(taile == NULL) return;
        *taile= 0;
        char *tails= rindex(name, path_sep);
        if(tails == NULL) return;
        strcpy(path, filename);
        char *p1= strstr(path, "/views/"); // TODO need to be path_sep
        if(p1 == NULL) return;
        *p1 = 0;
        strcat(path, "/controllers");
        strcat(path, tails);
        strcat(path, "_controller.rb");
        find_it(path, 0);
        point = 0;
        search(1, action);
    }else if(strcmp(ext, ".rb") == 0){
        if(strlen(defname) > 0){
            // get view name to use
            strcpy(name, filename);
            char *tail= get_tail(name);
            char *p1= strstr(tail, "_controller");
            if(p1 == NULL) return;
            *p1= 0;
            strcpy(path, filename);
            char *p2= strstr(path, "/controllers/"); // TODO need to be path_sep
            if(p2 == NULL) return;
            *p2 = 0;
            strcat(path, "/views/");
            strcat(path, tail);
            strcat(path, "/");
            strcat(path, defname);
            strcpy(path2, path);
            strcat(path, ".rhtml");
            strcat(path2, ".rjs");
            if(check_file(path)){
                find_it(path, 0);
            }else if(check_file(path2)){
                find_it(path2, 0);
            }else{
                say("No matching view found");
            }
        }
    }

}

#define UNDO_FLAG_PRE_SNIPPET (UNDO_FLAG|'S')
#define UNDO_FLAG_POST_SNIPPET (UNDO_FLAG|'T')

// expands the # if in a string or regexp to #{} placing cursor between
// the brackets unless it is escaped with a \.
//
command ruby_pound_sign() on ruby_tab['#']
{
    normal_character();
    if (ruby_expand_pound != 1)
        return;

    int c = get_character_color(point);

    if (c == color_class ruby_string
        || c == color_class ruby_shell_cmd
        || c ==  color_class ruby_regexp)
    {
        // check it is not escaped
        if (character(point-2) != '\\')
        {
            undo_mainloop();
            undo_flag = UNDO_FLAG_PRE_SNIPPET;
            stuff("{}");
            --point;
            undo_flag = UNDO_FLAG_POST_SNIPPET;
        }
    }
}

command ruby_undo() on ruby_tab[FKEY(9)]
{
    if (UNDO_FLAG_POST_SNIPPET == undo_flag)
    {
//        say("undoing snippet");
        undo_to_flag(UNDO_FLAG_PRE_SNIPPET);
    }
    else
    {
//        say("normal undo");
        undo_it(1, 0);
    }
}

command ruby_normal_character() on ruby_tab['\n']		/* more bindings, see below */
{
    if (UNDO_FLAG_POST_SNIPPET == undo_flag)
        undo_flag = UNDO_INSERT;
    normal_character();
}

when_loading()		/* put it on the other keys, too */
{
    set_range(ruby_tab, ' ', (short) ruby_normal_character, '~' - ' ' + 1);
    set_range(ruby_tab, 128, (short) ruby_normal_character, MAX_CHAR - 128 + 1);

    ruby_tab[')'] = (short) ruby_close;
    ruby_tab[']'] = (short) ruby_close;
    ruby_tab['}'] = (short) ruby_close;

    ruby_tab['d'] = (short) ruby_maybe_indent_end;
    ruby_tab['e'] = (short) ruby_maybe_indent_end;
    ruby_tab['f'] = (short) ruby_maybe_indent_end;
    ruby_tab['n'] = (short) ruby_maybe_indent_end;

    ruby_tab['#'] = (short) ruby_pound_sign;
}
// [TB]++ 9/22/2006 - v 1.1.0 additions by Jim Morris
