File: ada_expansion.py

package info (click to toggle)
gnat-gps 4.3-5
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 49,096 kB
  • ctags: 20,461
  • sloc: ada: 274,120; ansic: 154,849; python: 9,890; tcl: 9,812; sh: 8,192; xml: 7,970; cpp: 4,737; yacc: 3,520; makefile: 2,136; lex: 2,043; java: 1,638; perl: 302; awk: 265; sed: 161; asm: 14; fortran: 2; lisp: 1
file content (530 lines) | stat: -rw-r--r-- 19,734 bytes parent folder | download | duplicates (3)
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()