"""This module provides minimally-intrusive Ada 95 reserved word and construct expansion in GPS. Specifically, if an abbreviation of a reserved word is either on a line by itself or follows only a label, that abbreviation is expanded into the full spelling. Note that not all reserved words are candidates for expansion: they must be long enough for expansion to be of use in the first place. Additionally, some (not all) constructs that require either a trailing identifier or trailing reserved word are expanded to include that identifier or reserved word. These include the following: named block statements named basic loops named while-loops named for-loops if-then statements case statements select statements record/end-record pairs Finally, begin-end pairs for program units are expanded to include the corresponding unit name. Expansions follow the user's letter casing preferences for reserved words and identifiers. A reserved word that is spelled fully does not require expansion of the word itself, but, for the sake of minimal intrusion, also does not invoke construct expansion. Thus a person who types everything will not be intruded upon except for the case in which a user-defined identifier matches an abbreviation for a reserved word. To mitigate this effect the minimum abbreviation length may be set to a larger value by altering the variable "min_abbreviation" declared below. Examples: A named loop is required to have the loop name follow the "end loop". If the user enters this text: Foo : loo and enters the expansion key (the control-space key by default) it will be expand into the following: Foo : loop | end loop Foo; where the vertical bar '|' indicates the cursor location after expansion. The cursor indentation is controlled by the user's syntax indentation preference. This expansion is also done for begin-end pairs, For example, if the user enters: procedure Foo is beg it will be expanded into procedure Foo is begin | end Foo; Nested declarations are ignored such that the correct name is used by the expansion. For another example, this time without an identifier but with the required completion: if is expanded into if | then end if; """ import sys import GPS import re from misc_text_utils import replace_line, insert_line, get_line, attempt_up, \ blanks, up, down import text_utils ############################################################################ # Customization variables # These variables can be changed in the initialization commands associated # with this script (see /Tools/Scripts) min_abbreviation = 3 # We use the minimum abbreviation length to determine if the word # should be expanded into a reserved word. Hence, any word of length less # than this value will not be expanded. # This doesn't make reserved word expansion less a "problem" if the user # perceives it as such, but it does mitigate it somewhat. action_name = "Conditionally expand Ada syntax" # Name of the GPS action this module creates. Changing this variable # has no effect, since the action is created as soon as this module # is loaded default_action_key = "primary-h" # To change the default expansion key, you should go to the menu # /Edit/Key shortcuts, and select the action action_name in the Ada # category. Changing the default action_key here has no effect, since # the default key is set as soon as GPS is loaded # ################## No user customization below this point ########## GPS.parse_xml(""" Ada syntax-based reserved word and """ """construct expansion ada_expansion.expand_syntax() """ + default_action_key + """ """) def expand_syntax(): """Expand selected Ada 95 reserved words and syntax.""" requires_space_key = do_expansion() if requires_space_key: GPS.Editor.insert_text(' ') # ################## No public API below this point ################## def debug(s): "comment-out this return statement to enable debugging statements..." return name = sys._getframe(1).f_code.co_name GPS.Console("Messages").write(name + ": " + s + "\n") def do_expansion(): """conditionally expands the word just before the space key is hit, and returns a boolean indicating whether a space is required to be entered afterward""" try: current_file = GPS.current_context().file().name() except: # Indicate that a blank character is required since this routine is not # going to be doing anything return True orig_word = "" word = "" found_colon = False new_line = "" line_num = GPS.Editor.cursor_get_line(current_file) column_num = GPS.Editor.cursor_get_column(current_file) line = GPS.Editor.get_chars(current_file, line_num, 0) line = string.rstrip(line) # we only expand if the cursor is at the correct position if column_num != len(line) + 1: # +1 because GPS columns don't start at 0 return True if string.strip(line) == '': return True label = potential_label(current_file) debug("potential_label() returned '" + label + "'") # check for situations like this: "foo : declare" first_colon_pos = string.find(line, ':') if first_colon_pos == -1: # didn't find a colon orig_word = string.lower(string.strip(line)) found_colon = False else: # found a colon, maybe an assignment pattern = re.compile("^([ \t]*)(.*):(.*)($|--)", re.IGNORECASE) match = re.search(pattern, line) remainder = match.group(3) if string.find(remainder, "=") == 0: # found assignment, not just a colon return True orig_word = string.lower(string.strip(remainder)) found_colon = True if len(orig_word) >= min_abbreviation: debug("orig_word is '" + orig_word + "'") word = expanded_abbreviation(orig_word, expansion_words) debug("expanded word is '" + word + "'") if word == '': # no expansion found # we leave it as they typed it since it is not a word of interest return True # so that a space key is emitted else: # allowed to expand but abbreviation was not long enough word = orig_word if label != '': # replace occurrence of label with identifier_case(label) within line # note we cannot assign label first since we are searching for it in # the call to replace line = string.replace(line, label, identifier_case(label)) label = identifier_case(label) new_line = line[:len(line) - len(orig_word)] + word_case(word) debug("new_line is '" + new_line + "'") # note we cannot prepend the blank to the label before we do the following # search if found_colon: width = string.find(new_line, label) else: width = len(new_line) - len(word) # and now we can prepend the blank to the label for subsequent use if label != '': label = ' ' + label if word == "begin": replace_line(current_file, new_line) # NOTE: do not 'improve' the following sequence of statements by # merging the call to "insert_line(blanks(width + syntax_indent()))" # to here, after the call to replace_line(...) above, The reason is # that associated_decl() would be affected by the insert_line() # effect, in that it would start below the 'begin' we just inserted # via the replace_line() call and thus be mislead to give the wrong # result. if label != '': insert_line(blanks(width + syntax_indent())) insert_line(blanks(width) + word_case('end') + label + ';') else: # no label, try the decl unit name unit_name = associated_decl(current_file) if unit_name != '': insert_line(blanks(width + syntax_indent())) insert_line(blanks(width) + word_case('end') + ' ' + unit_name + ';') else: # no label and no decl unit name insert_line(blanks(width + syntax_indent())) insert_line(blanks(width) + word_case('end') + ';') up() text_utils.goto_end_of_line() return False elif word == "declare": replace_line(current_file, new_line) insert_line(blanks(width + syntax_indent())) insert_line(blanks(width) + word_case('begin')) insert_line(blanks(width) + word_case('end') + label + ';') up(2) text_utils.goto_end_of_line() return False elif word == "while": new_line = new_line + word_case(' loop') replace_line(current_file, new_line) insert_line(blanks(width) + word_case('end loop') + label + ';') GPS.Editor.cursor_set_position( current_file, line_num, len(new_line) - 4) return False elif word == "loop": replace_line(current_file, new_line) insert_line(blanks(width) + word_case('end loop') + label + ';') up() text_utils.goto_end_of_line() insert_line(blanks(width + syntax_indent())) return False elif word == "for": # expand word here since it must not be an attr def clause if within_Ada_statements(current_file): new_line = new_line + ' ' + word_case(' loop') replace_line(current_file, new_line) insert_line(blanks(width) + word_case('end loop') + label + ';') # place the cursor at the loop variable declaration GPS.Editor.cursor_set_position( current_file, line_num, len(new_line) - 4) return False elif word == "if": new_line = new_line + word_case(' then') replace_line(current_file, new_line) insert_line(blanks(width) + word_case('end if;')) GPS.Editor.cursor_set_position( current_file, line_num, len(new_line) - 4) return False elif word == 'case': new_line = new_line + word_case(' is') replace_line(current_file, new_line) insert_line(blanks(width) + word_case('end case;')) GPS.Editor.cursor_set_position( current_file, line_num, len(new_line) - 2) return False elif word in ('record', 'select'): replace_line(current_file, new_line) insert_line(blanks(width + syntax_indent())) insert_line(blanks(width) + word_case('end ') + word_case(word) + ';') up() text_utils.goto_end_of_line() return False else: if word != orig_word: # we've expanded the word but it isn't one of the interesting ones # above so we just make the expansion take effect replace_line(current_file, new_line) return True return True # ie emit a blank # words to expand whenever the trigger key is hit immediately after the word expansion_words = ( 'abort', 'abstract', 'accept', 'access', 'aliased', 'array', 'begin', 'case', 'constant', 'declare', 'delay', 'delta', 'digits', 'else', 'elsif', 'entry', 'exception', 'exit', 'for', 'function', 'generic', 'if', 'limited', 'loop', 'others', 'package', 'pragma', 'private', 'procedure', 'protected', 'raise', 'range', 'record', 'renames', 'requeue', 'return', 'reverse', 'select', 'separate', 'subtype', 'tagged', 'task', 'terminate', 'type', 'until', 'when', 'while', 'with') def word_case(word): pref = string.lower(GPS.Preference("Ada-Reserved-Casing").get()) if pref == "upper": return string.upper(word) elif pref == "mixed": return word.title() elif pref == "lower": return string.lower(word) elif pref == "unchanged": return word else: # we punt on Smart_Mixed return word def identifier_case(id): pref = string.lower(GPS.Preference("Ada-Ident-Casing").get()) if pref == "upper": return string.upper(id) elif pref == "mixed": return id.title() elif pref == "lower": return string.lower(id) elif pref == "unchanged": return id else: # we punt on Smart_Mixed return id def associated_decl(current_file): original_line_num = GPS.Editor.cursor_get_line(current_file) original_column_num = GPS.Editor.cursor_get_column(current_file) block_count = 0 expecting_declaration = False result = "" # we immediately attempt to go up a line to start searching because # we want to skip the line we are manipulating. # Note that if we cannot go up initially we return the null string # as the result, but that makes sense because this function will # never be called in such a case when writing legal Ada code. For # example, legal Ada never has a "begin" on the very first line. going_up = attempt_up() while going_up: prev_line = get_line() search_begin_line = word_case(prev_line) if string.find(search_begin_line, 'begin') != -1: if block_count == 0: break else: block_count = block_count + 1 elif significant_end(prev_line): block_count = block_count - 1 expecting_declaration = True elif found_separated("procedure|function", prev_line): if not instantiation(prev_line, current_file): if expecting_declaration: # found decl for previously encountered begin/end expecting_declaration = False else: # use this one pattern = re.compile( '^([ \t]*)(procedure|function)([ \t]*)' '([a-zA-Z0-9_."=/<>+\-&*]+)(.*)', re.IGNORECASE | re.DOTALL) match = re.search(pattern, prev_line) result = match.group(4) break elif found_separated("task", prev_line): # we ignore task declarations if found_separated("body", prev_line): if expecting_declaration: # found decl for previously encountered begin/end expecting_declaration = False else: # use this one pattern = re.compile( '^([ \t]*)task([ \t]*)body([ \t]*)' '([a-zA-Z0-9_.]+)(.*)', re.IGNORECASE | re.DOTALL) match = re.search(pattern, prev_line) result = match.group(4) break elif found_separated("entry", prev_line): if expecting_declaration: # found decl for previously encountered begin/end expecting_declaration = False else: # use this one pattern = re.compile( '^([ \t]*)entry([ \t]*)([a-zA-Z0-9_.]+)(.*)', re.IGNORECASE | re.DOTALL) match = re.search(pattern, prev_line) result = match.group(3) break elif found_separated("package", prev_line): if found_separated("body", prev_line): if expecting_declaration: # found decl for previously encountered begin/end expecting_declaration = False else: # use this one pattern = re.compile( '^([ \t]*)package([ \t]*)body([ \t]*)' '([a-zA-Z0-9_.]+)(.*)', re.IGNORECASE | re.DOTALL) match = re.search(pattern, prev_line) result = match.group(4) break going_up = attempt_up() GPS.Editor.cursor_set_position( current_file, original_line_num, original_column_num) return identifier_case(result) def found_separated(word, this_line): pattern = re.compile("([ \t]*)(" + word + ")([ \t]*)", re.IGNORECASE) match = re.search(pattern, this_line) return match is not None def instantiation(prev_line, current_file): original_line_num = GPS.Editor.cursor_get_line(current_file) original_column_num = GPS.Editor.cursor_get_column(current_file) # check for an instantiation *on the same line* as the subprogram decl pattern = re.compile("([ \t]*)is([ \t]*)new(.*)", re.DOTALL | re.IGNORECASE) match = re.search(pattern, prev_line) if match is not None: return True # check for instantiation on next line down down() next_line = get_line() if found_separated("new", next_line): GPS.Editor.cursor_set_position( current_file, original_line_num, original_column_num) return True else: GPS.Editor.cursor_set_position( current_file, original_line_num, original_column_num) return False def expanded_abbreviation(word, words): if word == "": return "" for W in words: if string.find(W, string.lower(word)) == 0: return W return "" def significant_end(this_line): """does this_line contain either "end;" or "end ;"?""" target_line = string.lower(this_line) if string.find(target_line, 'end;') != -1: return True pattern = re.compile( "^([ \t]*)end([ \t]*)(.*);(.*)($|--)", re.IGNORECASE | re.DOTALL) match = re.search(pattern, target_line) if match is None: return False if match.group(3) not in ('loop', 'record', 'if', 'case', 'select'): return True return False def within_Ada_statements(current_file): line_num = GPS.Editor.cursor_get_line(current_file) column_num = GPS.Editor.cursor_get_column(current_file) up_count = 0 result = False block_count = 0 going_up = attempt_up() while going_up: up_count = up_count + 1 prev_line = get_line() prev_line = string.lower(prev_line) if string.find(prev_line, 'begin') != -1: # found it if block_count == 0: result = True break else: block_count = block_count + 1 elif significant_end(prev_line): block_count = block_count - 1 going_up = attempt_up() # now return cursor to original position GPS.Editor.cursor_set_position(current_file, line_num, column_num) debug("returning " + str(result)) return result def potential_label(current_file): if not within_Ada_statements(current_file): return "" label = "" label_line = get_line() label_line = string.rstrip(label_line) # strip trailing whitespace if string.find(label_line, ':') == -1: # no colon on this line # look on the previous line for a stand-alone label, ie "foo :" or # "foo:" # Rather than go hunting, the label, if any, must be only 1 line up. # This will be ok since a label is never the first line of a program # unit. line_num = GPS.Editor.cursor_get_line(current_file) column_num = GPS.Editor.cursor_get_column(current_file) going_up = attempt_up() if going_up: label_line = get_line() # found a colon, which might be for a label if string.find(label_line, ':') != -1: pattern = re.compile( "^([ \t]*)(.*):(.*)", re.IGNORECASE | re.DOTALL) match = re.search(pattern, label_line) remainder = string.strip(match.group(3)) if remainder == '': # right syntax so far temp_label = match.group(2) if temp_label != '': # found a label label = string.strip(temp_label) # now return cursor to original position GPS.Editor.cursor_set_position(current_file, line_num, column_num) else: # found ':' pattern = re.compile("^([ \t]*)(.*):(.*)", re.IGNORECASE) match = re.search(pattern, label_line) remainder = string.lstrip(match.group(3)) label = match.group(2) # found assignment operation ":=" if remainder and remainder[0] == '=': debug("returning label '" + label + "'") return label # Treat as a label, even if it won't be, such as in variable # declarations. # Since we only use it where allowed, this isn't a problem. label = string.strip(label) debug("returning label '" + label + "'") return label def syntax_indent(): # we make this a function so that we will catch any user changes in # the preference without having to reload this module return GPS.Preference("Ada-Indent-Level").get()