This file is indexed.

/usr/share/gps/plug-ins/text_utils.py is in gnat-gps-common 5.3dfsg-1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  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
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
"""Defines editor-specific functions

YOU SHOULD ALMOST ALWAYS LOAD THIS FILE

This script defines a number of python functions and GPS actions that can
be used inside GPS editors. These can be used to move the cursor around or
edit the text.
They are often programmed so that they emulate the Emacs editor, but they
are independent of the Emacs mode and do not override any key shortcut. As
a result you can define your own shortcuts for the actions defined in this
package.
See also emacs.xml
"""


############################################################################
## No user customization below this line
############################################################################

import GPS
import string, traceback
import navigation_utils
import gtk
from gps_utils import *

should_extend_selection = False
# Whether the selection should be extended when moving the cursor

GPS.Preference ("Plugins/emacs/transient_mark").create (
   "Transient Mark", "boolean",
   """If unset, the selected region is never unselected when the clipboard is modified by a Cut/Copy/Paste operation. This is broadly similar to the Emacs mode with the same name""", False)

GPS.Preference("Plugins/emacs/bgcolor").create(
    "Background color", "color",
    """Background color for popup windows (zap-to-char,...)""",
    "yellow")

def replace (frm, to, text):
   """Replace a part of the buffer by the given text"""
   frm.buffer ().delete (frm, to)
   frm.buffer ().insert (frm, text)

def goto_subprogram_start (cursor):
   """Return an EditorLocation corresponding to the subprogram in which
      cursor is"""
   blocks = {"CAT_PROCEDURE":1, "CAT_FUNCTION":1, "CAT_ENTRY":1,
             "CAT_PROTECTED":1, "CAT_TASK":1, "CAT_PACKAGE":1}

   if cursor.block_type() == "CAT_UNKNOWN":
      return None

   min = cursor.buffer().beginning_of_buffer()
   while not blocks.has_key (cursor.block_type()) and cursor > min:
     cursor = cursor.block_start() - 1

   if cursor > min: return cursor
   else:            return None

def get_local_vars (subprogram):
   """Return a list of GPS.Entity that are variables local to the
      subprogram. It might not work accurately with nested subprograms"""
   result = []
   if subprogram:
      locFile = subprogram.body().file()
      locFrom = subprogram.body().line()
      locTo   = subprogram.end_of_scope().line()

      for e in locFile.entities (local=True):
          decl = e.declaration()
          if not e.is_type() \
             and decl.file() == locFile \
             and decl.line() >= locFrom \
             and decl.line() <= locTo:
                result.append (e)

   return result

def delete_until_char(char, buffer=None):
    """Delete all characters forward from the current cursor position,
       until CHAR is seen. CHAR itself is also deleted.
       If the current character is CHAR, it is skipped and the next
       occurrences of CHAR is searched.
    """
    if not buffer:
        buffer = GPS.EditorBuffer.get()

    start = buffer.current_view().cursor()
    end = start + 1
    while end.get_char() != char:
        end = end + 1
    buffer.delete (start, end)

@interactive("Editor", name="zap to char")
class Zap_To_Char(CommandWindow):
    """Deletes all characters from the cursor position up to and including
       the next occurrence of a character. The character is queried
       interactively
    """
    def __init__(self):
        CommandWindow.__init__(
            self,
            prompt="Zap to char:",
            on_changed=self.on_changed)
        self.set_background(GPS.Preference("Plugins/emacs/bgcolor").get())

    @with_save_excursion
    def on_changed(self, input, cursor_pos):
        delete_until_char(char=input)
        self.destroy()

@interactive ("Editor",  name="toggle wrapping")
def toggle_editor_wrapping():
    """Toggle word wrapping in the current editor"""

    buffer  = GPS.EditorBuffer.get ()
    v = buffer.current_view()
    from pygps import get_widgets_by_type
    text_view = get_widgets_by_type(gtk.TextView, v.pywidget())[0]
    if text_view.get_wrap_mode() == gtk.WRAP_NONE:
        text_view.set_wrap_mode(gtk.WRAP_WORD)
    else:
        text_view.set_wrap_mode(gtk.WRAP_NONE)


@interactive ("Editor", in_ada_file, name="subprogram box")
@with_save_excursion
def add_subprogram_box():
   """Search backward for the first subprogram or package declaration. Before the start of this declaration, insert a comment box containing the name of the subprogram. This provides helpful separations between subprograms, and is similar to the style used in the GNAT compiler or GPS themselves"""

   buffer  = GPS.EditorBuffer.get ()
   loc = goto_subprogram_start (buffer.current_view().cursor())
   if loc:
      name = loc.block_name()
      loc = loc.block_start().beginning_of_line();
      dashes = '-' * (len (name) + 6)
      box = dashes + "\n" + "-- " + name + " --\n" + dashes + "\n\n"
      buffer.insert (loc, box)
      buffer.indent (loc, loc.forward_line (3))

@interactive ("Editor", "Source editor", name="select line")
def select_line():
   """Select the current line in the current editor, including trailing newline
      This moves the cursor to the end of the line"""
   buffer = GPS.EditorBuffer.get ()
   loc    = buffer.current_view ().cursor ()
   buffer.select (loc.beginning_of_line(), loc.end_of_line() + 1)

def get_selection_or_buffer (buffer=None):
   """If a selection exists, returns its beginning and end. Otherwise
      return the beginning and end of buffer.
      The buffer is returned as the first field of the tuple"""
   if not buffer:
      buffer = GPS.EditorBuffer.get ()
   start = buffer.selection_start ()
   end   = buffer.selection_end ()
   if start == end:
      return (buffer, buffer.beginning_of_buffer (), buffer.end_of_buffer ())
   else:
      return (buffer, start, end)

def get_selection_or_word (buffer=None):
   """If a selection exists, returns its beginning and end. Otherwise
      return the beginning and end of the current word..
      The buffer is returned as the first field of the tuple"""
   if not buffer:
      buffer = GPS.EditorBuffer.get ()
   start = buffer.selection_start ()
   end   = buffer.selection_end ()
   if start == end:
      loc = buffer.current_view().cursor ()
      return (buffer, goto_word_start (loc), goto_word_end (loc))
   else:
      return (buffer, start, end)

def get_selection_or_line (buffer, location):
   """If a selection exists, returns its beginning and end. Otherwise
      return the beginning and end of line.
      The buffer is returned as the first field of the tuple"""

   if isinstance (location, FileLocation):
      location = GPS.EditorLocation(buffer, location.line(), location.column())

   buffer = location.buffer ()
   start  = buffer.selection_start ()
   end    = buffer.selection_end ()
   if start == end:
      return (buffer, location.beginning_of_line (), location.end_of_line ())
   else:
      return (buffer, start, end)

@interactive ("Editor", "Source editor", name="Move block right",
              menu="/Edit/Selection/Move right", key="control-alt-greater")
def move_block (chars=1):
   """Move the current selection chars characters to the right. If chars
      is negative, moves to the left. If there is no selection, indent
      the current line."""

   buffer = GPS.EditorBuffer.get ()
   tab_width = 8

   # Determine extents of the selection
   start_line = buffer.selection_start().line()
   end_line   = buffer.selection_end().line()

   beg_loc = buffer.selection_start().beginning_of_line()
   end_loc = buffer.selection_end().end_of_line()

   had_selection = not (buffer.selection_start() == buffer.selection_end())

   if not had_selection:
       cursor_loc = buffer.current_view ().cursor()
       cursor_line = cursor_loc.line()
       cursor_col  = cursor_loc.column()

   end_loc = end_loc.forward_char(-1)

   text = buffer.get_chars(beg_loc, end_loc)

   newtext = []
   for line in text.split('\n'):
      if chars > 0:
         # Insert x chars at the beginning of the line
         newtext += [" " * chars + line]
      else:
         # ... remove x blanks from the beginning of the text ...

         for c in range (-chars):
             if line == "":
                break
             if line[0] == '\t':
                line = " " * (tab_width - 1) + line[1:]
             elif line[0] == ' ':
                line = line[1:]
             else:
                break
         newtext += [line]

   buffer.start_undo_group ()
   buffer.delete (beg_loc, end_loc)
   buffer.insert (EditorLocation (buffer, start_line, 1), "\n".join(newtext))
   buffer.finish_undo_group ()

   if had_selection:
       # Reselect the range of lines
       start_loc = EditorLocation (buffer, start_line, 1)
       end_loc   = EditorLocation (buffer, end_line, 1).end_of_line()
       buffer.select (start_loc, end_loc)
   else:
       # Replace the cursor
       buffer.current_view().goto (
          EditorLocation (buffer,
             cursor_line,
             max (0, cursor_col + chars)))

make_interactive (lambda:move_block(-1),
                  category="Editor", filter="Source editor",
                  menu="/Edit/Selection/Move left", key="control-alt-less",
                  name="Move block left")

@interactive("Editor", "Source editor", menu="/Edit/Selection/Untabify")
@with_save_excursion
def untabify ():
   """Replace tab characters in the current selection (or the whole buffer)
      with the correct amount of spaces. The tab stops are every n columns
      where n is specified by a preference in the Preferences dialog"""

   tab_width = 8
   buffer, start, end = get_selection_or_buffer ()
   while start < end:
      start = start.search ("\t", dialog_on_failure=False)
      if not start:
         break
      size = tab_width - ((start [0].column() - 1) % tab_width)
      replace (start [0], start [1] - 1, " " * size)
      start = start [1]

def lines_with_digit (buffer, loc, max=None):
   """Return an EditorLocation pointing to the last line adjacent to
      loc that contains a digit in the same column as loc. See description
      of serialize () for an example. The search can be limited to a
      specific max location"""

   if max:
      max = max.end_of_line ()
   else:
      max = buffer.end_of_buffer ()

   col  = loc.column () - 1
   loc2 = loc.end_of_line () + 1  # to beginning of next line
   while loc2 < max:
      eol = loc2.end_of_line ()
      check = loc2 + col
      if check > eol or not buffer.get_chars (check, check).isdigit():
         return loc2 - 1
      loc2 = eol + 1 # to beginning of next line

   return max

@interactive ("Editor", "Source editor", "/Edit/Rectangle/Serialize")
@with_save_excursion
def serialize (increment=1):
   """Increment a set of numbers found on adjacent lines.
      The exact behavior depends on whether there is a current selection
      or not.
      If there is no selection, then the set of lines considered is from
      the current line on and includes all adjacent lines that have at
      least one digit in the original columns. In the following example,
      | marks the place where the cursor is at the beginning:

           AAA |10 AAA
           CCC 34567 CCC
           DDD DDD

      then only the first two lines will be modified, and will become

           AAA 10 AAA
           CCC 11 CCC
           DDD DDD

      If there is a selection, all the lines in the selection are
      modified. For each line, the columns that had digits in the first
      line are modified, no matter what they actually contain. In the
      example above, if you select all three lines, the replacement becomes

           AAA 10 AAA
           CCC 11567 CCC
           DDD 12D

      ie only the fifth and sixth columns are modified since only those
      columns contained digits in the first line. This feature assumes that
      you are selecting a relevant set of lines. But it allows you to
      transform blank lines more easily. For instance, if you have

           AAA 1
           BBB
           CCC

      this is transformed into

           AAA 1
           BBB 2
           CCC 3
   """

   buffer = GPS.EditorBuffer.get ()
   start = buffer.selection_start ()
   end   = buffer.selection_end ()
   if start == end:
      has_sel = False
      start = buffer.current_view ().cursor ()
      end   = lines_with_digit (buffer, start)
   else:
      has_sel = True
   loc   = start

   # From start .. end, all lines are equal now
   end = end.end_of_line ()

   # Find the range of text to replace on each line
   repl = loc
   while buffer.get_chars (repl, repl).isdigit():
      repl = repl + 1

   frm_col = loc.column () - 1    # columns start at 0 on a line
   end_col = (repl - 1).column () - 1

   try:
      value = int (buffer.get_chars (loc, repl - 1)) + increment
   except:
      GPS.Console().write ("Cursor must be before a number")
      return

   format = "%0" + str (end_col - frm_col + 1) + "d"

   # And now do the replacement
   repl = loc.end_of_line () + 1  # to beginning of next line
   while repl < end:
       if has_sel:
          # We had a selection: make sure the column range exists on the
          # line, and fill it with the value
          eol = repl.end_of_line ()
          if repl + frm_col > eol:
             buffer.insert (eol,
                           " " * ((eol - repl) - frm_col + 2)
                           + format % value)
          else:
             replace (repl + frm_col, min (repl + end_col, eol),
                      format % value)
       else:
          # We had no selection: replace the digit, no matter how many cols
          to = repl + frm_col
          while buffer.get_chars (to, to).isdigit():
             to = to + 1
          replace (repl + frm_col, to - 1, format % value)

       repl = repl.end_of_line () + 1
       value = value + increment

@interactive ("Editor", "Source editor", name="kill forward")
def delete_forward():
   """Delete the character just after the cursor in the current editor"""
   buffer = GPS.EditorBuffer.get()
   cursor = buffer.current_view().cursor()
   buffer.delete (cursor, cursor)

@interactive ("Editor", "Source editor", name="Delete Line")
def delete_line ():
    """Delete the current line and place the cursor on the beginning of the
       next line.
    """
    buffer = GPS.EditorBuffer.get()   # get the current buffer
    view = buffer.current_view()      # get the current view of this buffer
    location = view.cursor()          # get the location of the cursor

    # Get the bounds to delete
    start = location.beginning_of_line()
    end   = location.end_of_line()

    # Do the deletion
    buffer.delete (start, end)

def kill_line (location = None, count=1):
   """ Kills the end of the line on which LOCATION is.
       If LOCATION is unspecified, the current cursor location in the current
       editor is used.
       If the line is empty or contains only white spaces, the whole line is
       deleted.
       This is a better emulation of Emacs's behavior than the one provided by
       default by gtk+, which doesn't handle whitespaces correctly.
       When called several times from the same line, entries are appended in
       the clipboard.
       Count is the number of lines to delete. If greater than 1, then the
       whole lines are deleted, including newline characters."""

   if not location:
      location = GPS.EditorBuffer.get ().current_view ().cursor ()
   buffer = location.buffer ()
   start  = location

   append          = GPS.last_command() == "kill-line"
   GPS.set_last_command ("kill-line")

   # In case the current location points to a line terminator we just cut it
   if count == 1 and start.get_char() == "\n":
      buffer.cut (start, start, append)
   else:
      bol = start
      for line in range (1, count + 1):
         end       = bol.end_of_line ()
         str       = buffer.get_chars (start, end)
         strip_str = str.rstrip ()
         if count == 1 \
          and len (str) > 0 \
          and str [len (str) - 1] == '\n' and strip_str != "":
            end = end.forward_char (-1)
         bol = end+1
      buffer.cut (start, end, append)

################################################
## Moving the cursor
################################################

@interactive ("Editor", "Source editor", name="goto beginning of buffer")
def beginning_of_buffer():
   """Move the cursor to the beginning of the buffer"""
   buffer = GPS.EditorBuffer.get()
   buffer.current_view().goto (buffer.beginning_of_buffer(),
       should_extend_selection)

@interactive ("Editor", "Source editor", name="goto end of buffer")
def end_of_buffer():
   """Move the cursor to the end of the buffer"""
   buffer = GPS.EditorBuffer.get()
   buffer.current_view().goto (buffer.end_of_buffer(), should_extend_selection)

@interactive ("Editor", "Source editor", name="goto beginning of line")
def goto_beginning_of_line():
   """Goto the beginning of line"""
   view = GPS.EditorBuffer.get().current_view()
   view.goto (view.cursor().beginning_of_line(), should_extend_selection)

def end_of_line(file, line):
   """Goto to the end of the line in file"""
   buffer = GPS.EditorBuffer.get (GPS.File (file))
   loc  = GPS.EditorLocation (buffer, line, 1)
   buffer.current_view().goto (loc.end_of_line() - 1)

@interactive ("Editor", "Source editor", name="goto end of line")
def goto_end_of_line():
   """Goto the end of line"""
   view = GPS.EditorBuffer.get().current_view()
   view.goto (view.cursor().end_of_line(), should_extend_selection)

def is_space (char):
   return char == ' ' or char == '\t'

def goto_word_start (iter, underscore_is_word=True):
   """Move to the beginning of the current word (or leave the cursor
      where it is). This properly handles '_'
   """
   if underscore_is_word:
      while not iter.starts_word ():
         iter = iter.forward_word (-1)
      return iter
   else:
      while not iter.starts_word ():
         prev = iter
         iter = iter.forward_char (-1)
         c = iter.get_char ()
         if c == '_':
             return prev
      return iter

def goto_word_end (iter, underscore_is_word=True):
   if underscore_is_word:
      while True:
         iter = iter.forward_word ()
         try:
            if iter.get_char () != '_':
               return iter.forward_char (-1)
         except:
            return iter.buffer().end_of_buffer ()

   else:
      while not iter.ends_word ():
         prev = iter
         iter = iter.forward_char (1)
         try:
            if iter.get_char () == '_':
               return prev
         except:
            # Probably an invalid position.
            return iter.buffer().end_of_buffer ()
      return iter

def delete_spaces(backward=True, forward=True, leave_one=False):
   """Delete all spaces around cursor, possibly leaving one"""
   buffer = GPS.EditorBuffer.get()
   start = buffer.current_view().cursor()
   end = start
   if forward:
      max = end.end_of_line()
      while is_space (end.get_char()) and end < max:
        end = end + 1
      end = end - 1
   if backward:
      max = start.beginning_of_line()
      start = start - 1
      while is_space (start.get_char()) and start >= max:
        start = start - 1
      start = start + 1
   if start <= end:
      buffer.delete(start, end)

   if leave_one:
       buffer.insert(start, " ")

@interactive ("Editor", "Source editor", name="delete horizontal space")
@with_save_excursion
def delete_horizontal_space(backward=1, forward=1):
   """Delete all spaces and tabs around the cursor in the current editor.
The two parameters can be used to control in what directions white spaces are
searched for"""
   delete_spaces(leave_one=False)

@interactive("Editor", "Source editor", name="just one space")
@with_save_excursion
def just_one_space():
    """Delete all spaces and tabs around the cursor, leaving one space.
       If there are no spaces around, a new space is inserted
    """
    delete_spaces(leave_one=True)

@interactive ("Editor", "Source editor", name="transpose chars")
def transpose_chars():
   """Transpose characters around cursor, moving forward one character."""
   buffer = GPS.EditorBuffer.get()
   cursor = buffer.current_view().cursor()
   if cursor > buffer.beginning_of_buffer():
      c = cursor.get_char ()
      buffer.start_undo_group ()
      buffer.delete (cursor, cursor)
      buffer.insert (cursor - 1, c)
      buffer.current_view().goto (cursor + 1)
      buffer.finish_undo_group ()

@interactive ("Editor", "Source editor", name="Transpose lines")
def transpose_lines (location = None):
   """Transpose the line at LOCATION (or current line) and the previous one,
      leaving the cursor after both"""
   if not location:
      location = GPS.EditorBuffer.get().current_view ().cursor ()
   buffer = location.buffer ()
   if location.line () < buffer.lines_count ():
      buffer.start_undo_group ()
      start = location.beginning_of_line ()
      end   = location.end_of_line ()
      text  = buffer.get_chars (start, end)
      buffer.delete (start, end)
      buffer.insert (start.forward_line (-1), text)
      buffer.current_view ().goto (start.end_of_line () + 1)
      buffer.finish_undo_group ()

@interactive ("Editor", "Source editor", name="open line")
@with_save_excursion
def open_line():
   """Insert a newline and leave cursor at its current place."""
   buffer = GPS.EditorBuffer.get()
   buffer.insert (buffer.current_view().cursor(), "\n")

@interactive ("Editor", "Source editor", name="Join line")
def join_line ():
   """Join the current line and the following one, separated by a single
      space, and leaves the cursor on the space"""
   buffer = GPS.EditorBuffer.get()
   eol = buffer.current_view().cursor().end_of_line()
   buffer.start_undo_group ()
   buffer.current_view().goto (eol)
   buffer.delete (eol, eol)  ## Newline character
   delete_spaces(backward=False, forward=True, leave_one=False)
   if not is_space (eol.forward_char (-1).get_char ()):
      buffer.insert (eol, " ")
   buffer.finish_undo_group ()

def apply_func_to_word (func, location=None):
   """Apply a function to the current word (starting at the current character).
      FUNC takes one argument, the text it replaces, and should return the
      replacement text"""
   if not location:
      location = GPS.EditorBuffer.get ().current_view().cursor()
   buffer = location.buffer()
   buffer.start_undo_group ()
   end = location.forward_word()
   text = func (buffer.get_chars (location, end))
   replace (location, end, text)
   buffer.finish_undo_group ()

@interactive ("Editor", "Source editor", name="Upper case word")
def upper_case_word (location=None):
   """Upper case the current word (starting at the current character)"""
   apply_func_to_word (str.upper, location)

@interactive ("Editor", "Source editor", name="Lower case word")
def lower_case_word (location=None):
   """Lower case the current word (starting at the current character)"""
   apply_func_to_word (str.lower, location)

@interactive ("Editor", "Source editor", name="Capitalize word")
def capitalize_case_word (location=None):
   """Capitalize the current word (starting at the current character)"""
   apply_func_to_word (str.capitalize, location)

@interactive ("Editor", "Source editor", name="Center line")
def center_line ():
   """Center the current line on the screen. If a comment line then the
      text inside the comment is centered, the comment markers remain
      unchanged.
   """
   buffer = GPS.EditorBuffer.get()
   location = buffer.current_view ().cursor ()
   initial = location.create_mark()

   buffer.start_undo_group ()
   start = location.beginning_of_line ()
   end   = location.end_of_line ()
   text  = buffer.get_chars (start, end)
   if text[0:2] == "--" or text[0:2] == "//" or text[0:2] == "##":
     start = start + 2

   if text[-3:] == "--\n" or text[-3:] == "//\n" or text[-3:] == "##\n":
     # Use right comment characters to center the text
     end = end - 3
     text = buffer.get_chars (start, end).strip()
     spaces = end.column() - start.column() + 1 - len(text)
     before = spaces / 2
     after = spaces / 2
     if before + after != spaces:
       after = after + 1
     buffer.delete (start, end)
     buffer.insert (start, ' ' * before + text + ' ' * after)
   else:
     # No right comment characters, use the highlight column to center the text
     col = GPS.Preference ("Src-Editor-Highlight-Column").get()
     text = buffer.get_chars (start, end).strip()
     spaces = int(col) - start.column() - len(text)
     before = spaces / 2
     buffer.delete (start, end - 1)
     buffer.insert (start, ' ' * before + text)

   # Move to next line
   buffer.current_view().goto (GPS.EditorLocation \
      (buffer,
       line=initial.location().forward_line(1).line(),
       column=location.column()))
   buffer.finish_undo_group ()

class BlockIterator:
   """An iterator for the various sections of an editor.
      Each step in the iteration returns a tuple (start, end) of EditorLocation
      instances for the section.
      The constructor parameter overlay_name can be one of:
          - "":          The whole buffer is returned
          - "selection": The current selection in the buffer is returned
          - "word":      The current word in the buffer is returned
          - overlay name: All sections for which this overlay applies are
                         returned. The name could be one of "comment",
                         "keywords", "string" or "character"
      Example of use:
          buffer = EditorBuffer.get()
          for start, end in BlockIterator (buffer, "comments"):
             ...
   """
   def __init__ (self, buffer, overlay_name):
      self.mark    = buffer.beginning_of_buffer ().create_mark()
      if overlay_name != "" \
       and overlay_name != "selection" \
       and overlay_name != "word":
         self.overlay = buffer.create_overlay (overlay_name)
         self.in_comment = \
           buffer.beginning_of_buffer().has_overlay (self.overlay)
      else:
         self.overlay = None
         self.overlay_name = overlay_name
   def __iter__ (self):
      return self
   def next (self):
      loc = self.mark.location ()
      if not self.overlay:
        if loc < loc.buffer().end_of_buffer():
           self.mark.move (loc.buffer().end_of_buffer())
           if self.overlay_name == "selection":
              return (loc.buffer().selection_start(),
                      loc.buffer().selection_end())
           elif self.overlay_name == "word":
              cursor = loc.buffer().current_view().cursor()
              start = cursor
              while not start.starts_word(): start = start - 1
              while not cursor.ends_word() : cursor = cursor + 1
              return (start, cursor)
           else:
              return (loc.buffer().beginning_of_buffer(),
                      loc.buffer().end_of_buffer())
        raise StopIteration
      else:
        # Find beginning of next section
        if not loc.has_overlay (self.overlay):
           loc = loc.forward_overlay (self.overlay)

        if loc >= loc.buffer().end_of_buffer():
           raise StopIteration

        loc2 = loc.forward_overlay (self.overlay)
        self.mark.move (loc2 + 1)
        return (loc, loc2 - 1)

class WordIterator:
   """An iterator for all words in a block. Each iteration returns a
      tuple (start, end) of EditorLocation instances.
      Example of use:
        buffer = EditorBuffer.get()
        for blockStart, blockEnd in BlockIterator (buffer, "comments"):
           for wordStart, wordEnd in WordIterator (blockStart, blockEnd):
              ...
   """
   def __init__ (self, start, end):
      self.mark = start.create_mark()
      self.end  = end
   def __iter__ (self):
      return self
   def starts_at (self, loc):
      self.mark.move (loc)
   def next (self):
      loc = self.mark.location ()
      while loc < self.end:
         loc2 = loc.forward_word ()
         if loc.get_char().isalpha():
            # Use a mark, in case the buffer is modified
            self.mark.move (loc2 + 1)
            return (loc, loc2 - 1)
         else:
            loc = loc + 1
      raise StopIteration

class LineIterator:
   """An iterator for all lines in a block. Each iteration returns a
      tuple (start, end) of EditorLocation instances."""
   def __init__ (self, start, end):
      self.mark = start.create_mark()
      self.end  = end.create_mark()
   def __iter__ (self):
      return self
   def next (self):
      loc = self.mark.location()
      if loc >= self.end.location():
        raise StopIteration
      loc2 = loc.end_of_line()
      if loc2 >= self.end.location():
        self.mark.move (self.end.location() + 1)
        return (loc, self.end.location())
      else:
        self.mark.move (loc2 + 1)
        return (loc, loc2)

### Emulating Emacs selection:
### In Emacs, one sets the mark first, then when the cursor is moved the
### selection is extended appropriately. This is rather tricky to emulate
### in gtk+.
### There are two implementations: when pygtk is available, we simply
### temporarily override the key bindings so that the selection is always
### extended. This avoids all flickering, has no run-time cost, and is
### certainly the nicest. Not quite perfect though, since other functions
### that move the cursor will not extend the selection, only the basic
### key bindings defined for a gkt.TextView do.
### However, if pygtk is not available, we emulate it by monitoring all
### location changes. The slow down is almost invisible, but since the
### selection is first cancelled by gtk+ when the cursor is moved, and we
### then reselect it, there is some flickering

try:
   import gtk, gobject
   has_pygtk = 1

   HOME      = 65360
   LEFT      = 65361
   UP        = 65362
   RIGHT     = 65363
   DOWN      = 65364
   PAGE_UP   = 65365
   PAGE_DOWN = 65366
   END       = 65367

   KP_HOME      = 65429
   KP_LEFT      = 65430
   KP_UP        = 65431
   KP_RIGHT     = 65432
   KP_DOWN      = 65433
   KP_PAGE_UP   = 65434
   KP_PAGE_DOWN = 65435
   KP_END       = 65436

   def override (key, modifier, movement, step, select):
       gtk.binding_entry_remove (gtk.TextView, key, modifier)
       gtk.binding_entry_add_signal (gtk.TextView, key, modifier,
                                     "move_cursor",
                                     gobject.TYPE_ENUM, movement,
                                     gobject.TYPE_INT,  step,
                                     gobject.TYPE_BOOLEAN, select)

   def override_key_bindings (select):
       """Override the default TextView keybinding to either always force
          the extension the selection, or not"""

       global should_extend_selection

       should_extend_selection = select

       override (RIGHT,    0, gtk.MOVEMENT_VISUAL_POSITIONS, 1, select)
       override (KP_RIGHT, 0, gtk.MOVEMENT_VISUAL_POSITIONS, 1, select)
       override (LEFT,     0, gtk.MOVEMENT_VISUAL_POSITIONS, -1, select)
       override (KP_LEFT,  0, gtk.MOVEMENT_VISUAL_POSITIONS, -1, select)

       override (RIGHT,    gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_WORDS, 1, select)
       override (KP_RIGHT, gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_WORDS, 1, select)
       override (LEFT,     gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_WORDS, -1, select)
       override (KP_LEFT,  gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_WORDS, -1, select)

       override (UP,       0, gtk.MOVEMENT_DISPLAY_LINES, -1, select)
       override (KP_UP,    0, gtk.MOVEMENT_DISPLAY_LINES, -1, select)
       override (DOWN,     0, gtk.MOVEMENT_DISPLAY_LINES, 1, select)
       override (KP_DOWN,  0, gtk.MOVEMENT_DISPLAY_LINES, 1, select)

       override (UP,      gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_PARAGRAPHS, -1, select)
       override (KP_UP,   gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_PARAGRAPHS, -1, select)
       override (DOWN,    gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_PARAGRAPHS, 1, select)
       override (KP_DOWN, gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_PARAGRAPHS, 1, select)

       override (HOME,    0, gtk.MOVEMENT_DISPLAY_LINE_ENDS, -1, select)
       override (KP_HOME, 0, gtk.MOVEMENT_DISPLAY_LINE_ENDS, -1, select)
       override (END,     0, gtk.MOVEMENT_DISPLAY_LINE_ENDS, 1, select)
       override (KP_END,  0, gtk.MOVEMENT_DISPLAY_LINE_ENDS, 1, select)

       override (HOME,    gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_BUFFER_ENDS, -1, select)
       override (KP_HOME, gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_BUFFER_ENDS, -1, select)
       override (END,     gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_BUFFER_ENDS, 1, select)
       override (KP_END,  gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_BUFFER_ENDS, 1, select)

       override (PAGE_UP,      0, gtk.MOVEMENT_PAGES, -1, select)
       override (KP_PAGE_UP,   0, gtk.MOVEMENT_PAGES, -1, select)
       override (PAGE_DOWN,    0, gtk.MOVEMENT_PAGES, 1, select)
       override (KP_PAGE_DOWN, 0, gtk.MOVEMENT_PAGES, 1, select)

       override (PAGE_UP,      gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_HORIZONTAL_PAGES, -1, select)
       override (KP_PAGE_UP,   gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_HORIZONTAL_PAGES, -1, select)
       override (PAGE_DOWN,    gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_HORIZONTAL_PAGES, 1, select)
       override (KP_PAGE_DOWN, gtk.gdk.CONTROL_MASK, gtk.MOVEMENT_HORIZONTAL_PAGES, 1, select)

except ImportError:
   has_pygtk = 0
   def on_location_changed (hook, file, line, column):
      try:
         buffer = GPS.EditorBuffer.get (file)
         mark   = buffer.get_mark ("emacs_selection_bound")
         buffer.select (mark.location(), buffer.current_view().cursor())
      except:
         pass  ## no such mark

@interactive ("Editor", "Source editor", name="set mark command")
def set_mark_command (location = None):
    """Set mark at LOCATION (or current cursor if LOCATION is unspecified)
      This is similar to Emacs's behavior: a mark is put at the current cursor position. You can then move the cursor elsewhere, and delete the text between this mark and the new cursor position. See also the action 'Cancel mark command'"""
    if not location:
       location = GPS.EditorBuffer.get ().current_view ().cursor ()
    if has_pygtk:
        location.create_mark ("selection_bound")
        override_key_bindings (select = True)
    else:
        location.create_mark ("emacs_selection_bound")
        GPS.Hook ("location_changed").add (on_location_changed)

@interactive ("Editor", "Source editor", name="Cancel mark command")
def cancel_mark_command (buffer = None):
    """Cancel the mark in BUFFER
      Remove the emacs-emulation mark in the current editor. See also the action 'Set mark command'"""
    if not buffer:
       buffer = GPS.EditorBuffer.get ()
    try:
       buffer.unselect ()
       if has_pygtk:
          override_key_bindings (select = False)
       else:
          buffer.get_mark ("emacs_selection_bound").delete ()
          GPS.Hook ("location_changed").remove (on_location_changed)
    except:
       pass  ## No such mark

def on_clipboard_changed (hook):
    """Called when the contents of the clipboard has changed"""
    if GPS.Preference ("Plugins/emacs/transient_mark").get():
       cancel_mark_command ()

GPS.Hook ("clipboard_changed").add (on_clipboard_changed)
GPS.parse_xml ("""
   <action name="kill line" output="none" category="Editor">
      <description>This is similar to Emacs' kill-line function. It deletes the end of the line after the cursor's current column. If the cursor is at the end of the line, it deletes the newline character and therefore joins the current line and the next.
The text that is deleted is copied to the clipboard. If you call this action multiple times from the same location, all deleted text is merged into a single clipboard, so that a single Paste will put it all back.
When this command is executed after a repeat_next command, the whole line is deleted to provide a more intuitive behavior.</description>
      <filter id="Source editor" />
      <shell lang="python">if $repeat == 1: text_utils.kill_line (None, $remaining+1)</shell>
   </action>

  <action name="New View Horizontal" output="none" category="MDI">
     <description>When on an editor, splits the current notebook into two side-by-side windows, so that the two windows show two views of the same file. If another window already exists to the side, a new view is created inside that existing notebook, rather than create a new one</description>
     <shell lang="python">GPS.MDI.current().split (vertically=False,new_view=True,reuse=True)</shell>
  </action>

  <action name="New View Vertical" output="none" category="MDI">
     <description>When on an editor, splits the current notebook into two windows vertically, so that the two windows show two views of the same file. If another window already exists above or below, a new view is created inside that existing notebook, rather than create a new one</description>
     <shell lang="python">GPS.MDI.current().split (vertically=True,new_view=True,reuse=True)</shell>
  </action>
""")