// // ruby_mode // // v 1.0.0 // 2005-Apr-05 // // Timothy Byrd // timbyrd@pobox.com // // (Please contact me with comments/corrections.) // // This is a mode for Ruby files // // History: // 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. ruby-comment A comment in Ruby. ruby-keyword A keyword in Ruby. ruby-number A number or :symbol in Ruby. ruby-punctuation Punctuation in Ruby. ruby-regexp A regular expression in Ruby. ruby-str-subst A string substitution in Ruby, e.g., "i = #{i}". ruby-string A string in Ruby. ruby-ugly-var An ugly Perl-style variable in Ruby, e.g., $&. ruby-variable A variable in Ruby. #endif #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 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_comment = color_class perl_comment; color_class ruby_keyword = color_class perl_keyword; color_class ruby_number = color_class perl_constant; color_class ruby_punctuation = color_class perl_constant; color_class ruby_regexp = color_class perl_function; color_class ruby_str_subst = color_class perl_string; color_class ruby_string = color_class perl_string; color_class ruby_ugly_var = color_class perl_constant; color_class ruby_variable = color_class perl_variable; #define RUBY_IDENT "(@?@?|$?)[a-zA-Z_][a-zA-Z0-9_]*" #define RUBY_NAME RUBY_IDENT "[!?=]?" // // 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 "(%." 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. // $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. // $-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. // #define R_UGLY_VAR "%$[!@&`'+~=/\\,;.<>_*$?:\"]|%$[0-9]+" \ "|%$(DEBUG|FILENAME|LOAD_PATH|stderr|stdin|stdout|VERBOSE)" \ "|%$-[0adFiIlpv]" // // 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; 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'; 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, "\\]"); // 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, color_class ruby_string); t = point - 1; search(1, "}"); set_character_color(t, point, color_class ruby_str_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, color_class ruby_string); } // `/' any_char* `/'[`i'|`o'|`p'] // `%'`r' char any_char* char // // /normal regex/i // %r|alternate form| // 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; } // Return code for word from here to point (something with alpha or // digits). // // 0 = unknown (identifier?) // 1 = keyword // 2 = Regexp // is_ruby_keyword(int from) { char buf[500], *s; if (point - from > sizeof(buf) / sizeof(char) - 10) save_var point = from + sizeof(buf) / sizeof(char) - 10; buf[0] = '|'; // get identifier, between | chars grab(from, point, buf + 1); strcat(buf, "|"); // If it follows a period, don't count it as a keyword. // char follows_period = matches_at(from, -1, "%.[ \t]*"); if (!follows_period && strstr( "|__FILE__|__LINE__|alias|and|BEGIN|begin|break|case|class|def" "|defined?|do|else|elsif|END|end|ensure|false|for|if|in|include" "|module|next|nil|not|or|raise|redo|require|rescue|retry|return" "|self|super|then|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; } 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_UGLY_VAR)) { point = matchend; set_character_color(t, point, color_class ruby_ugly_var); } else if (matches_at(point, 1, "/|(%%r)") && !matches_at(point, -1, "(" 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; 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]*" "(def|class|module|if|elsif|else|unless|while|until|case|when|begin|for|rescue)")) { if (!parse_string(1, ".*end")) // Multiple statements: ignore block. return 1; else return 0; } // Check for braces/brackets // char* brace_pat = ".*([][}{]|[)(]|(do))"; 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]*end")) { 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 ines starting with a // closing brace/bracket. // int ruby_indent_less() { char* brace_pat = "[ \t]*([]}]|[)])"; if (ruby_closeback && parse_string(1, brace_pat)) { return 1; } return parse_string(1, // "(.*;)?" "[ \t]*(" "end|when|else|elsif|rescue" ")") != 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, "(\\|([-%&*+@/<=>^~])[ \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(); } // 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; 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, "((def|class|module)[ \t\n]+" "(" RUBY_METH "))" "|(end|do)" "|(^|;)[ \t]*(" "if|unless|case|while|until|begin|for" ")")) { 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|;)"); } } 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