1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
|
"""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;
"""
############################################################################
# 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 = "control-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 ##########
import sys;
import GPS;
import string;
import re;
from misc_text_utils import *
import text_utils;
GPS.parse_xml ("""
<action name='""" + action_name + """' output="none" category="Ada">
<description>Ada syntax-based reserved word and construct expansion</description>
<filter module="Source_Editor" language="ada" />
<shell lang="python">ada_expansion.expand_syntax()</shell>
</action>
<key action='""" + action_name + """'>""" + default_action_key + """</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":
if within_Ada_statements (current_file): # expand word here since it must not be an attr def clause
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 != 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 != 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 <identifier>;"?"""
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 == 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()
if string.find (label_line,':') != -1: # found a colon, which might be for a label
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)
if remainder and remainder[0] == '=': # found assignment operation ":="
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()
|