Avoid deleting the range when the selection is incomplete.
[mp-5.x.git] / mp_core.mpsl
blob755e2209c10d1fede1dec21099c9168c6e77d42c
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     Copyright (C) 1991-2010 Angel Ortega <angel@triptico.com>
8     This program is free software; you can redistribute it and/or
9     modify it under the terms of the GNU General Public License
10     as published by the Free Software Foundation; either version 2
11     of the License, or (at your option) any later version.
13     This program is distributed in the hope that it will be useful,
14     but WITHOUT ANY WARRANTY; without even the implied warranty of
15     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16     GNU General Public License for more details.
18     You should have received a copy of the GNU General Public License
19     along with this program; if not, write to the Free Software
20     Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
22     http://www.triptico.com
26 /** global data **/
28 /* L(x) is the same as gettext(x) */
29 L = gettext;
31 /* LL(x) is the same as x */
32 sub LL(x) { x; }
34 /* configuration */
36 mp.config = {};
37 mp.config.undo_levels = 100;
38 mp.config.word_wrap = 0;
39 mp.config.auto_indent = 0;
40 mp.config.tab_size = 8;
41 mp.config.tabs_as_spaces = 0;
42 mp.config.dynamic_tabs = 0;
43 mp.config.unlink = 1;
44 mp.config.case_sensitive_search = 1;
45 mp.config.global_replace = 0;
46 mp.config.preread_lines = 60;
47 mp.config.mark_eol = 0;
48 mp.config.maximize = 0;
49 mp.config.keep_eol = 1;
51 /* default end of line, system dependent */
52 if (mp.drv.id eq 'win32')
53         mp.config.eol = "\r\n";
54 else
55         mp.config.eol = "\n";
57 /* status line */
58 mp.config.status_format = "%m%n %x,%y [%l] %R%O %s %e %t";
59 mp.status_line_info = {
60         '%V'    =>      mp.VERSION,
61         '%m'    =>      sub { mp.active.txt.mod && '*' || ''; },
62         '%x'    =>      sub { mp.active.txt.x + 1; },
63         '%y'    =>      sub { mp.active.txt.y + 1; },
64         '%l'    =>      sub { size(mp.active.txt.lines); },
65         '%R'    =>      sub { mp.macro.process_event && 'R' || ''; },
66         '%O'    =>      sub { mp.config.insert && 'O' || ''; },
67         '%s'    =>      sub { mp.active.syntax.name; },
68         '%t'    =>      sub { mp.tags[mp.get_word(mp.active())].label; },
69         '%n'    =>      sub { mp.active.name; },
70         '%w'    =>      sub { mp.word_count(mp.active()); },
71         '%e'    =>      sub { mp.active.encoding || ''; },
72         '%%'    =>      '%'
75 /* a regex for selecting words */
76 mp.word_regex = "/[[:alnum:]_]+/i";
78 /* if it does not work (i.e. not GNU regex), fall back */
79 if (regex(mp.word_regex, "test") == NULL)
80         mp.word_regex = '/[A-Z_][A-Z0-9_]*/i';
82 /* document list */
83 mp.docs = [];
84 mp.active_i = 0;
86 /* allowed color names (order matters, match the Unix curses one) */
87 mp.color_names = [ "default", "black", "red", "green",
88         "yellow", "blue", "magenta", "cyan", "white" ];
90 /* color definitions */
91 mp.colors = {
92         'normal' => {           'text'  => [ 'default', 'default' ],
93                                 'gui'   => [ 0x000000, 0xffffff ] },
94         'cursor' => {           'text'  => [ 'default', 'default' ],
95                                 'gui'   => [ 0x000000, 0xffffff ],
96                                 'flags' => [ 'reverse' ] },
97         'selection' => {        'text'  => [ 'red', 'default' ],
98                                 'gui'   => [ 0xff0000, 0xffffff ],
99                                 'flags' => [ 'reverse'] },
100         'comments' => {         'text'  => [ 'green', 'default' ],
101                                 'gui'   => [ 0x00cc77, 0xffffff ] },
102         'documentation' => {    'text'  => [ 'cyan', 'default' ],
103                                 'gui'   => [ 0x8888ff, 0xffffff ] },
104         'quotes' => {           'text'  => [ 'blue', 'default' ],
105                                 'gui'   => [ 0x0000ff, 0xffffff ],
106                                 'flags' => [ 'bright' ] },
107         'matching' => {         'text'  => [ 'black', 'cyan' ],
108                                 'gui'   => [ 0x000000, 0xffff00 ] },
109         'word1' => {            'text'  => [ 'green', 'default' ],
110                                 'gui'   => [ 0x00aa00, 0xffffff ],
111                                 'flags' => [ 'bright' ] },
112         'word2' => {            'text'  => [ 'red', 'default' ],
113                                 'gui'   => [ 0xff6666, 0xffffff ],
114                                 'flags' => [ 'bright' ] },
115         'tag' => {              'text'  => [ 'cyan', 'default' ],
116                                 'gui'   => [ 0x8888ff, 0xffffff ],
117                                 'flags' => [ 'underline' ] },
118         'spell' => {            'text'  => [ 'red', 'default' ],
119                                 'gui'   => [ 0xff8888, 0xffffff ],
120                                 'flags' => [ 'bright', 'underline' ] },
121         'search' => {           'text'  => [ 'black', 'green' ],
122                                 'gui'   => [ 0x000000, 0x00cc77 ] }
125 /* hash of specially coloured words */
126 mp.word_color = {};
128 mp.keycodes = {};
130 mp.actions = {};
132 mp.actdesc = {};
134 mp.alert_log = [];
136 /** the menu **/
138 mp.menu = [
139         [
140                 LL("&File"),
141                 [ 'new', 'open', 'save', 'save_as', 'close', 'revert',
142                         'open_under_cursor',
143                         '-', 'hex_view',
144                         '-', 'set_password',
145                         '-', 'open_config_file', 'open_templates_file',
146                         '-', 'sync',
147                         '-', 'save_session', 'load_session',
148                         '-', 'exit'
149                 ]
150         ],
151         [
152                 LL("&Edit"),
153                 [ 'undo', 'redo', '-',
154                         'cut_mark', 'copy_mark', 'paste_mark', 'delete_mark',
155                         'delete_line', '-',
156                         'mark', 'mark_vertical', 'unmark', '-',
157                         'insert_template', 'insert_next_item', '-',
158                         'word_wrap_paragraph', 'join_paragraph', '-',
159                         'exec_command', '-',
160                         'eval', 'eval_doc'
161                 ]
162         ],
163         [
164                 LL("&Search"),
165                 [ 'seek', 'seek_next', 'seek_prev', 'replace', '-',
166                         'complete', '-',
167                         'seek_misspelled', 'ignore_last_misspell', '-',
168                         'seek_repeated_word', '-',
169                         'find_tag', 'complete_symbol', '-', 'grep'
170                 ]
171         ],
172         [
173                 LL("&Go to"),
174                 [ 'next', 'prev',
175                         'move_bof', 'move_eof', 'move_bol', 'move_eol',
176                         'goto', 'move_word_right', 'move_word_left',
177                         'section_list',
178                         '-', 'document_list'
179                 ]
180         ],
181         [
182                 LL("&Options"),
183                 [ 'record_macro', 'play_macro', '-',
184                         'encoding', 'tab_options', 'line_options', 'repeated_words_options',
185                         'toggle_spellcheck', '-',
186                         'word_count', '-',
187                         'zoom_in', 'zoom_out', '-',
188                         'about'
189                 ]
190         ]
193 mp.actions_by_menu_label = {};
195 /** code **/
198  * mp.redraw - Triggers a redraw on the next cycle.
200  * Triggers a full document redraw in the next cycle.
201  */
202 sub mp.redraw()
204         /* just increment the redraw trigger */
205         mp.redraw_counter++;
209 sub mp.active()
210 /* returns the active document */
212         local d;
214         /* empty document list? create a new, empty one */
215         if (size(mp.docs) == 0)
216                 mp.new();
218         /* get active document */
219         d = mp.docs[mp.active_i];
221         /* if it's read only but has modifications, revert them */
222         if (d.read_only && size(d.undo)) {
223                 while (size(d.undo))
224                         mp.undo(d);
226                 mp.message = {
227                         'timeout'       => time() + 2,
228                         'string'        => '*' ~ L("Read-only document") ~ '*'
229                 };
230         }
232         return d;
236 sub mp.process_action(a)
237 /* processes an action */
239         local f, d;
241         d = mp.active();
243         if ((f = mp.actions[a]) != NULL)
244                 f(d);
245         else {
246                 mp.message = {
247                         'timeout'       => time() + 2,
248                         'string'        => sprintf(L("Unknown action '%s'"), a)
249                 };
250         }
252         return NULL;
256 sub mp.process_event(k)
257 /* processes a key event */
259         local d, a;
261         /* empty document list? do nothing */
262         if (size(mp.docs) == 0)
263                 return;
265         d = mp.active();
267         if (mp.keycodes_t == NULL)
268                 mp.keycodes_t = mp.keycodes;
270         /* get the action asociated to the keycode */
271         if ((a = mp.keycodes_t[k]) != NULL) {
273                 /* if it's a hash, store for further testing */
274                 if (is_hash(a))
275                         mp.keycodes_t = a;
276                 else {
277                         /* if it's executable, run it */
278                         if (is_exec(a))
279                                 a(d);
280                         else
281                         /* if it's an array, process it sequentially */
282                         if (is_array(a))
283                                 foreach(l, a)
284                                         mp.process_action(l);
285                         else
286                                 mp.process_action(a);
288                         mp.keycodes_t = NULL;
289                 }
290         }
291         else {
292                 mp.insert_keystroke(d, k);
293                 mp.keycodes_t = NULL;
294         }
296         mp.shift_pressed = NULL;
298         /* if there is a keypress notifier function, call it */
299         if (is_exec(d.keypress))
300                 d.keypress(d, k);
302         return NULL;
306 sub mp.build_status_line()
307 /* returns the string to be drawn in the status line */
309         if (mp.message) {
310                 /* is the message still active? */
311                 if (mp.message.timeout > time())
312                         return mp.message.string;
314                 mp.message = NULL;
315         }
317         return sregex("/%./g", mp.config.status_format, mp.status_line_info);
321 sub mp.backslash_codes(s, d)
322 /* encodes (d == 0) or decodes (d == 1) backslash codes
323    (like \n, \r, etc.) */
325         d &&    sregex("/[\r\n\t]/g", s, { "\r" => '\r', "\n" => '\n', "\t" => '\t'}) ||
326                 sregex("/\\\\[rnt]/g", s, { '\r' => "\r", '\n' => "\n", '\t' => "\t"});
330 sub mp.long_op(func, a1, a2, a3, a4)
331 /* executes a potentially long function */
333         local r;
335         mp.busy(1);
336         r = func(a1, a2, a3, a4);
337         mp.busy(0);
339         return r;
343 sub mp.get_history(key)
344 /* returns a history for the specified key */
346         if (key == NULL)
347                 return NULL;
348         if (mp.history == NULL)
349                 mp.history = {};
350         if (mp.history[key] == NULL)
351                 mp.history[key] = [];
353         return mp.history[key];
357 sub mp.menu_label(action)
358 /* returns a label for the menu for an action */
360         local l;
362         /* if action is '-', it's a menu separator */
363         if (action eq '-')
364                 return NULL;
366         /* no recognized action? return */
367         if (!exists(mp.actions, action))
368                 return action ~ "?";
370         /* get the translated description */
371         l = L(mp.actdesc[action]) || action;
373         /* is there a keycode that generates this action? */
374         foreach (i, sort(keys(mp.keycodes))) {
375                 if (mp.keycodes[i] eq action) {
376                         /* avoid mouse and window pseudo-keycodes */
377                         if (!regex("/window/", i) && !regex("/mouse/", i)) {
378                                 l = l ~ ' [' ~ i ~ ']';
379                                 break;
380                         }
381                 }
382         }
384         mp.actions_by_menu_label[l] = action;
386         return l;
390 sub mp.trim_with_ellipsis(str, max)
391 /* trims the string to the last max characters, adding ellipsis if done */
393         local v = regex('/.{' ~ max ~ '}$/', str);
394         return v && '...' ~ v || str;
398 sub mp.get_doc_names(max)
399 /* returns an array with the trimmed names of the documents */
401         map(sub(e) {
402                 (e.txt.mod && '* ' || '') ~ mp.trim_with_ellipsis(e.name, (max || 24));
403         }, mp.docs);
407 sub mp.usage()
408 /* set mp.exit_message with an usage message (--help) */
410         mp.exit_message = 
411         sprintf(L(
412                 "Minimum Profit %s - Programmer Text Editor\n"\
413                 "Copyright (C) Angel Ortega <angel@triptico.com>\n"\
414                 "This software is covered by the GPL license. NO WARRANTY.\n"\
415                 "\n"\
416                 "Usage: mp-5 [options] [files...]\n"\
417                 "\n"\
418                 "Options:\n"\
419                 "\n"\
420                 " -t {tag}           Edits the file where tag is defined\n"\
421                 " -e {mpsl_code}     Executes MPSL code\n"\
422                 " -f {mpsl_script}   Executes MPSL script file\n"\
423                 " -d {directory}     Set current directory\n"\
424                 " -x {file}          Open file in the hexadecimal viewer\n"\
425                 " -txt               Use text mode instead of GUI\n"\
426                 " +NNN               Moves to line number NNN of last file\n"\
427                 "\n"\
428                 "Homepage: http://triptico.com/software/mp.html\n"\
429                 "Mailing list: mp-subscribe@lists.triptico.com\n"
430         ), mp.VERSION);
434 sub mp.process_cmdline()
435 /* process the command line arguments (ARGV) */
437         local o, line;
439         mp.load_tags();
441         /* skip ARGV[0] */
442         shift(ARGV);
444         while (o = shift(ARGV)) {
445                 if (o eq '-h' || o eq '--help') {
446                         mp.usage();
447                         mp.exit();
448                         return;
449                 }
450                 else
451                 if (o eq '-e') {
452                         /* execute code */
453                         local c = shift(ARGV);
455                         if (! regex('/;\s*$/', c))
456                                 c = c ~ ';';
458                         eval(c);
459                 }
460                 else
461                 if (o eq '-f') {
462                         /* execute script */
463                         local s = shift(ARGV);
465                         if (stat(s) == NULL)
466                                 ERROR = sprintf(L("Cannot open '%s'"), s);
467                         else {
468                                 mp.open(s);
469                                 eval(join("\n", mp.active.txt.lines));
470                                 mp.close();
471                         }
472                 }
473                 else
474                 if (o eq '-d')
475                         chdir(shift(ARGV));
476                 else
477                 if (o eq '-t')
478                         mp.open_tag(shift(ARGV));
479                 else
480                 if (o eq '-x') {
481                         local s = shift(ARGV);
483                         if (mp.hex_view(s) == NULL)
484                                 ERROR = sprintf(L("Cannot open '%s'"), s);
485                 }
486                 else
487                 if (o eq '-txt')
488                         mp.config.text_mode = 1;
489                 else
490                 if (regex('/^\+/', o)) {
491                         /* move to line */
492                         line = o - 1;
493                 }
494                 else
495                         mp.open(o);
496         }
498         if (ERROR) {
499                 mp.exit_message = ERROR ~ "\n";
500                 ERROR = NULL;
501                 mp.exit();
502                 return;
503         }
505         /* if no files are loaded, try a session */
506         if (size(mp.docs) == 0 && mp.config.auto_sessions) {
507                 mp.load_session();
508         }
509         else {
510                 /* set the first as the active one */
511                 mp.active_i = 0;
512         }
514         mp.active();
516         /* if there is a line defined, move there */
517         if (line != NULL)
518                 mp.set_y(mp.active(), line);
522 sub mp.load_profile()
523 /* loads ~/.mp.mpsl */
525         /* if /etc/mp.mpsl exists, execute it */
526         if (stat('/etc/mp.mpsl') != NULL) {
527                 eval( sub {
528                         local INC = [ '/etc' ];
529                         load('mp.mpsl');
530                 });
531         }
533         /* if ~/.mp.mpsl exists, execute it */
534         if (ERROR == NULL && stat(HOMEDIR ~ '.mp.mpsl') != NULL) {
535                 eval( sub {
536                         local INC = [ HOMEDIR ];
537                         load(".mp.mpsl");
538                 });
539         }
541         /* errors? show in a message */
542         if (ERROR != NULL) {
543                 mp.message = {
544                         'timeout'       => time() + 20,
545                         'string'        => ERROR
546                 };
548                 ERROR = NULL;
549         }
553 sub mp.setup_language()
554 /* sets up the language */
556         /* set gettext() domain */
557         gettext_domain('minimum-profit', APPDIR ~ 'locale');
559         /* test if gettext() can do a basic translation */
560         if (gettext('&File') eq '&File' && ENV.LANG) {
561                 /* no; try alternatives using the LANG variable */
562                 local v = [ sregex('!/!g', ENV.LANG) ]; /* es_ES.UTF-8 */
563                 push(v, shift(split('.', v[-1])));      /* es_ES */
564                 push(v, shift(split('_', v[-1])));      /* es */
566                 foreach (l, v) {
567                         eval('load("lang/' ~ l ~ '.mpsl");');
569                         if (ERROR == NULL)
570                                 break;
571                 }
573                 ERROR = NULL;
574         }
578 sub mp.normalize_version(vs)
579 /* converts a version string to something usable with cmp() */
581         map(sub(e) { sprintf("%03d", e); },
582                 split('.',
583                         sregex('/-.+$/', vs)));
587 sub mp.assert_version(found, minimal, package)
588 /* asserts that 'found' version of 'package' is at least 'minimal',
589    or generate a warning otherwise */
591         if (cmp(mp.normalize_version(found),
592                 mp.normalize_version(minimal)) < 0) {
593                 mp.alert(sprintf(L("WARNING: %s version found is %s, but %s is needed"),
594                                 package, found, minimal));
595         }
599 sub mp.test_versions()
600 /* tests component versions */
602         local mpdm = MPDM();
604         mp.assert_version(mpdm.version, '2.0.0', 'MPDM');
605         mp.assert_version(MPSL.VERSION, '2.0.0', 'MPSL');
609 /** MAIN **/
611 load("mp_drv.mpsl");
612 load("mp_move.mpsl");
613 load("mp_edit.mpsl");
614 load("mp_file.mpsl");
615 load("mp_clipboard.mpsl");
616 load("mp_search.mpsl");
617 load("mp_tags.mpsl");
618 load("mp_syntax.mpsl");
619 load("mp_macro.mpsl");
620 load("mp_templates.mpsl");
621 load("mp_spell.mpsl");
622 load("mp_misc.mpsl");
623 load("mp_crypt.mpsl");
624 load("mp_keyseq.mpsl");
625 load("mp_session.mpsl");
626 load("mp_build.mpsl");
627 load("mp_writing.mpsl");
628 load("mp_toys.mpsl");
630 mp.load_profile();
631 mp.setup_language();
632 mp.drv.startup();
633 mp.process_cmdline();
634 mp.test_versions();
635 mp.drv.main_loop();
636 mp.drv.shutdown();