Added Mercurial support.
[mp-5.x.git] / mp_core.mpsl
blob512cb84fbc081d2deff684f7b25a3d4c820c282d
1 /*
3     Minimum Profit 5.x
4     A Programmer's Text Editor
6     Copyright (C) 1991-2012 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     undo_levels:            100,
38     word_wrap:              0,
39     auto_indent:            0,
40     tab_size:               8,
41     tabs_as_spaces:         0,
42     dynamic_tabs:           0,
43     unlink:                 1,
44     case_sensitive_search:  1,
45     global_replace:         0,
46     preread_lines:          60,
47     mark_eol:               0,
48     maximize:               0,
49     keep_eol:               1,
50     smart_bol:              1
53 /* default end of line, system dependent */
54 if (mp.drv.id eq 'win32')
55         mp.config.eol = "\r\n";
56 else
57         mp.config.eol = "\n";
59 /* status line */
60 mp.config.status_format = "%m%n %x,%y [%l] %R%O %s %e %t";
61 mp.status_line_info = {
62         '%V'    =>      mp.VERSION,
63         '%m'    =>      sub { mp.active.disk_op && '!' || (mp.active.txt.mod && '*' || ''); },
64         '%x'    =>      sub { mp.active.txt.x + 1; },
65         '%y'    =>      sub { mp.active.txt.y + 1; },
66         '%l'    =>      sub { size(mp.active.txt.lines); },
67         '%R'    =>      sub { mp.macro.process_event && 'R' || ''; },
68         '%O'    =>      sub { mp.config.insert && 'O' || ''; },
69         '%s'    =>      sub { mp.active.syntax.name; },
70         '%t'    =>      sub { mp.tags[mp.get_word(mp.active())].label; },
71         '%n'    =>      sub { mp.active.name; },
72         '%w'    =>      sub { mp.word_count(mp.active()); },
73         '%e'    =>      sub { mp.active.encoding || ''; },
74         '%%'    =>      '%'
77 /* a regex for selecting words */
78 mp.word_regex = "/[[:alnum:]_]+/i";
80 /* if it does not work (i.e. not GNU regex), fall back */
81 if (regex("test", mp.word_regex) == NULL)
82         mp.word_regex = '/[A-Z_][A-Z0-9_]*/i';
84 /* document list */
85 mp.docs = [];
86 mp.active_i = 0;
88 /* allowed color names (order matters, match the Unix curses one) */
89 mp.color_names = [
90     "default",
91     "black", "red", "green", "yellow",
92     "blue", "magenta", "cyan", "white"
95 /* color definitions */
96 mp.colors = {
97     normal: {
98         text:   [ 'default', 'default' ],
99         gui:    [ 0x000000, 0xffffff ]
100     },
101     cursor: {
102         text:   [ 'default', 'default' ],
103         gui:    [ 0x000000, 0xffffff ],
104         flags:  [ 'reverse' ]
105     },
106     selection: {
107         text:   [ 'red', 'default' ],
108         gui:    [ 0xff0000, 0xffffff ],
109         flags:  [ 'reverse']
110     },
111     comments: {
112         text:   [ 'green', 'default' ],
113         gui:    [ 0x00cc77, 0xffffff ]
114     },
115     documentation: {
116         text:   [ 'cyan', 'default' ],
117         gui:    [ 0x8888ff, 0xffffff ]
118     },
119     quotes: {
120         text:   [ 'blue', 'default' ],
121         gui:    [ 0x0000ff, 0xffffff ],
122         flags:  [ 'bright' ]
123     },
124     matching: {
125         text:   [ 'black', 'cyan' ],
126         gui:    [ 0x000000, 0xffff00 ]
127     },
128     word1: {
129         text:   [ 'green', 'default' ],
130         gui:    [ 0x00aa00, 0xffffff ],
131         flags:  [ 'bright' ]
132     },
133     word2: {
134         text:   [ 'red', 'default' ],
135         gui:    [ 0xff6666, 0xffffff ],
136         flags:  [ 'bright' ]
137     },
138     tag: {
139         text:   [ 'cyan', 'default' ],
140         gui:    [ 0x8888ff, 0xffffff ],
141         flags:  [ 'underline' ]
142     },
143     spell: {
144         text:   [ 'red', 'default' ],
145         gui:    [ 0xff8888, 0xffffff ],
146         flags:  [ 'bright', 'underline' ]
147     },
148     search: {
149         text:   [ 'black', 'green' ],
150         gui:    [ 0x000000, 0x00cc77 ]
151     }
154 /* hash of specially coloured words */
155 mp.word_color = {};
157 mp.keycodes = {};
159 mp.actions = {};
161 mp.actdesc = {};
163 mp.alert_log = [];
165 /** the menu **/
167 mp.menu = [
168     [
169         LL("&File"), [
170             'new', 'open', 'save', 'save_as', 'close', 'revert',
171             'open_under_cursor',
172             '-', 'hex_view',
173             '-', 'set_password',
174             '-', 'open_config_file', 'open_templates_file',
175             '-', 'sync',
176             '-', 'save_session', 'load_session',
177             '-', 'exit'
178         ]
179     ],
180     [
181         LL("&Edit"), [
182             'undo', 'redo', '-',
183             'cut_mark', 'copy_mark', 'paste_mark', 'delete_mark',
184             'delete_line', 'cut_lines_with_string', '-',
185             'mark',     'mark_vertical', 'unmark', 'mark_all', '-',
186             'insert_template', 'insert_next_item', '-',
187             'word_wrap_paragraph', 'join_paragraph', '-',
188             'exec_command', 'filter_selection', '-',
189             'exec_action', 'eval', 'eval_doc'
190         ]
191     ],
192     [
193         LL("&Search"), [
194             'seek', 'seek_next', 'seek_prev', 'replace', '-',
195             'complete', '-',
196             'seek_misspelled', 'ignore_last_misspell', '-',
197             'seek_repeated_word', '-',
198             'find_tag', 'complete_symbol', '-', 'grep'
199         ]
200     ],
201     [
202         LL("&Go to"), [
203             'next', 'prev',
204             'move_bof', 'move_eof', 'move_bol', 'move_eol',
205             'goto', 'move_word_right', 'move_word_left',
206             'section_list',
207             '-', 'document_list'
208         ]
209     ],
210     [
211         LL("&Options"), [
212             'record_macro', 'play_macro', '-',
213             'encoding', 'tab_options', 'line_options',
214             'repeated_words_options', 'toggle_spellcheck', '-',
215             'word_count', '-',
216             'zoom_in', 'zoom_out', '-',
217             'about'
218         ]
219     ]
222 mp.actions_by_menu_label = {};
224 /** code **/
227  * mp.redraw - Triggers a redraw on the next cycle.
229  * Triggers a full document redraw in the next cycle.
230  */
231 sub mp.redraw()
233         /* just increment the redraw trigger */
234         mp.redraw_counter++;
238 sub mp.active()
239 /* returns the active document */
241         local d;
243         /* empty document list? create a new, empty one */
244         if (size(mp.docs) == 0)
245                 mp.new();
247         /* get active document */
248         d = mp.docs[mp.active_i];
250         /* if it's read only but has modifications, revert them */
251         if (d.read_only && size(d.undo)) {
252                 while (size(d.undo))
253                         mp.undo(d);
255                 mp.message = {
256                         'timeout'       => time() + 2,
257                         'string'        => '*' ~ L("Read-only document") ~ '*'
258                 };
259         }
261         return d;
265 sub mp.process_action(a)
266 /* processes an action */
268     local f, d;
270     d = mp.active();
272     if ((f = mp.actions[a]) != NULL)
273         f(d);
274     else {
275         mp.message = {
276             timeout:    time() + 2,
277             string:     sprintf(L("Unknown action '%s'"), a)
278         };
279     }
281     return NULL;
285 sub mp.test_mtime(doc)
286 /* tests for a file's mtime (uses GUI) */
288     if (doc.mtime && time() > mp.mtime_test) {
289         local s;
291         if ((s = stat(doc.name)) != NULL && s[9] > doc.mtime) {
292             if (mp.confirm(L("File was changed externally. Reload?")) == 1)
293                 mp.actions.revert(doc);
295             doc.mtime = s[9];
296         }
298         mp.mtime_test = time() + 2;
299     }
301     return doc;
305 sub mp.process_event(k)
306 /* processes a key event */
308     if (size(mp.docs)) {
309         local d, a;
311         d = mp.active();
313         mp.test_mtime(d);
314     
315         if (mp.keycodes_t == NULL)
316             mp.keycodes_t = mp.keycodes;
317     
318         /* get the action asociated to the keycode */
319         if ((a = mp.keycodes_t[k]) != NULL) {
320     
321             /* if it's a hash, store for further testing */
322             if (is_hash(a))
323                 mp.keycodes_t = a;
324             else {
325                 /* if it's executable, run it */
326                 if (is_exec(a))
327                     a(d);
328                 else
329                 /* if it's an array, process it sequentially */
330                 if (is_array(a))
331                     foreach(l, a)
332                         mp.process_action(l);
333                 else
334                     mp.process_action(a);
335     
336                 mp.keycodes_t = NULL;
337             }
338         }
339         else {
340             mp.insert_keystroke(d, k);
341             mp.keycodes_t = NULL;
342         }
343     
344         mp.shift_pressed = NULL;
345     
346         /* if there is a keypress notifier function, call it */
347         if (is_exec(d.keypress))
348             d.keypress(d, k);
349     }
351     return NULL;
355 sub mp.build_status_line()
356 /* returns the string to be drawn in the status line */
358         if (mp.message) {
359                 /* is the message still active? */
360                 if (mp.message.timeout > time())
361                         return mp.message.string;
363                 mp.message = NULL;
364         }
366         return sregex(mp.config.status_format, "/%./g", mp.status_line_info);
370 sub mp.backslash_codes(s, d)
371 /* encodes (d == 0) or decodes (d == 1) backslash codes
372    (like \n, \r, etc.) */
374         d && sregex(s, "/[\r\n\t]/g", { "\r" => '\r', "\n" => '\n', "\t" => '\t'}) ||
375                  sregex(s, "/\\\\[rnt]/g", { '\r' => "\r", '\n' => "\n", '\t' => "\t"});
379 sub mp.long_op(func, a1, a2, a3, a4)
380 /* executes a potentially long function */
382         local r;
384         mp.busy(1);
385         r = func(a1, a2, a3, a4);
386         mp.busy(0);
388         return r;
392 sub mp.get_history(key)
393 /* returns a history for the specified key */
395         if (key == NULL)
396                 return NULL;
397         if (mp.history == NULL)
398                 mp.history = {};
399         if (mp.history[key] == NULL)
400                 mp.history[key] = [];
402         return mp.history[key];
406 sub mp.menu_label(action)
407 /* returns a label for the menu for an action */
409         local l;
411         /* if action is '-', it's a menu separator */
412         if (action eq '-')
413                 return NULL;
415         /* no recognized action? return */
416         if (!exists(mp.actions, action))
417                 return action ~ "?";
419         /* get the translated description */
420         l = L(mp.actdesc[action]) || action;
422         /* is there a keycode that generates this action? */
423         foreach (i, sort(keys(mp.keycodes))) {
424                 if (mp.keycodes[i] eq action) {
425                         /* avoid mouse and window pseudo-keycodes */
426                         if (!regex(i, "/window/") && !regex(i, "/mouse/")) {
427                                 l = l ~ ' [' ~ i ~ ']';
428                                 break;
429                         }
430                 }
431         }
433         mp.actions_by_menu_label[l] = action;
435         return l;
439 sub mp.trim_with_ellipsis(str, max)
440 /* trims the string to the last max characters, adding ellipsis if done */
442         local v = regex(str, '/.{' ~ max ~ '}$/');
443         return v && '...' ~ v || str;
447 sub mp.get_doc_names(max)
448 /* returns an array with the trimmed names of the documents */
450         map(
451                 mp.docs,
452                 sub(e) {
453                         (e.txt.mod && '* ' || '') ~ mp.trim_with_ellipsis(e.name, (max || 24));
454                 }
455         );
459 sub mp.usage()
460 /* set mp.exit_message with an usage message (--help) */
462         mp.exit_message = 
463         sprintf(L(
464                 "Minimum Profit %s - Programmer Text Editor\n"\
465                 "Copyright (C) Angel Ortega <angel@triptico.com>\n"\
466                 "This software is covered by the GPL license. NO WARRANTY.\n"\
467                 "\n"\
468                 "Usage: mp-5 [options] [files...]\n"\
469                 "\n"\
470                 "Options:\n"\
471                 "\n"\
472                 " -t {tag}           Edits the file where tag is defined\n"\
473                 " -e {mpsl_code}     Executes MPSL code\n"\
474                 " -f {mpsl_script}   Executes MPSL script file\n"\
475                 " -d {directory}     Set current directory\n"\
476                 " -x {file}          Open file in the hexadecimal viewer\n"\
477                 " -txt               Use text mode instead of GUI\n"\
478                 " +NNN               Moves to line number NNN of last file\n"\
479                 "\n"\
480                 "Homepage: http://triptico.com/software/mp.html\n"\
481                 "Mailing list: mp-subscribe@lists.triptico.com\n"
482         ), mp.VERSION);
486 sub mp.process_cmdline()
487 /* process the command line arguments (ARGV) */
489         local o, line;
491         mp.load_tags();
493         /* skip ARGV[0] */
494         shift(ARGV);
496         while (o = shift(ARGV)) {
497                 if (o eq '-h' || o eq '--help') {
498                         mp.usage();
499                         mp.exit();
500                         return;
501                 }
502                 else
503                 if (o eq '-e') {
504                         /* execute code */
505                         local c = shift(ARGV);
507                         if (! regex(c, '/;\s*$/'))
508                                 c = c ~ ';';
510                         eval(c);
511                 }
512                 else
513                 if (o eq '-f') {
514                         /* execute script */
515                         local s = shift(ARGV);
517                         if (stat(s) == NULL)
518                                 ERROR = sprintf(L("Cannot open '%s'"), s);
519                         else {
520                                 mp.open(s);
521                                 eval(join(mp.active.txt.lines, "\n"));
522                                 mp.close();
523                         }
524                 }
525                 else
526                 if (o eq '-d')
527                         chdir(shift(ARGV));
528                 else
529                 if (o eq '-t')
530                         mp.open_tag(shift(ARGV));
531                 else
532                 if (o eq '-x') {
533                         local s = shift(ARGV);
535                         if (mp.hex_view(s) == NULL)
536                                 ERROR = sprintf(L("Cannot open '%s'"), s);
537                 }
538                 else
539                 if (o eq '-txt')
540                         mp.config.text_mode = 1;
541                 else
542                 if (regex(o, '/^\+/')) {
543                         /* move to line */
544                         line = o - 1;
545                 }
546                 else
547                         mp.open(o);
548         }
550         if (ERROR) {
551                 mp.exit_message = ERROR ~ "\n";
552                 ERROR = NULL;
553                 mp.exit();
554                 return;
555         }
557         /* if no files are loaded, try a session */
558         if (size(mp.docs) == 0 && mp.config.auto_sessions) {
559                 mp.load_session();
560         }
561         else {
562                 /* set the first as the active one */
563                 mp.active_i = 0;
564         }
566         mp.active();
568         /* if there is a line defined, move there */
569         if (line != NULL)
570                 mp.set_y(mp.active(), line);
574 sub mp.load_profile()
575 /* loads ~/.mp.mpsl */
577         /* if /etc/mp.mpsl exists, execute it */
578         if (stat('/etc/mp.mpsl') != NULL) {
579                 eval( sub {
580                         local INC = [ '/etc' ];
581                         load('mp.mpsl');
582                 });
583         }
585         /* if ~/.mp.mpsl exists, execute it */
586         if (ERROR == NULL && stat(HOMEDIR ~ '.mp.mpsl') != NULL) {
587                 eval( sub {
588                         local INC = [ HOMEDIR ];
589                         load(".mp.mpsl");
590                 });
591         }
593         /* errors? show in a message */
594         if (ERROR != NULL) {
595                 mp.message = {
596                         'timeout'       => time() + 20,
597                         'string'        => ERROR
598                 };
600                 ERROR = NULL;
601         }
605 sub mp.setup_language()
606 /* sets up the language */
608         /* set gettext() domain */
609         gettext_domain('minimum-profit', APPDIR ~ 'locale');
611         /* test if gettext() can do a basic translation */
612         if (gettext('&File') eq '&File' && ENV.LANG) {
613                 /* no; try alternatives using the LANG variable */
614                 local v = [ sregex(ENV.LANG, '!/!g') ]; /* es_ES.UTF-8 */
615                 push(v, shift(split(v[-1], '.')));      /* es_ES */
616                 push(v, shift(split(v[-1], '_')));      /* es */
618                 foreach (l, v) {
619                         eval('load("lang/' ~ l ~ '.mpsl");');
621                         if (ERROR == NULL)
622                                 break;
623                 }
625                 ERROR = NULL;
626         }
630 sub mp.normalize_version(vs)
631 /* converts a version string to something usable with cmp() */
633     vs->sregex('/-.+$/')->split('.')->map(sub(e) { sprintf("%03d", e); });
637 sub mp.assert_version(found, minimal, package)
638 /* asserts that 'found' version of 'package' is at least 'minimal',
639    or generate a warning otherwise */
641         if (cmp(mp.normalize_version(found),
642                 mp.normalize_version(minimal)) < 0) {
643                 mp.alert(sprintf(L("WARNING: %s version found is %s, but %s is needed"),
644                                 package, found, minimal));
645         }
649 sub mp.test_versions()
650 /* tests component versions */
652         local mpdm = MPDM();
654         mp.assert_version(mpdm.version, '2.0.2', 'MPDM');
655         mp.assert_version(MPSL.VERSION, '2.0.2', 'MPSL');
659 /** modules **/
661 local mp_modules = [
662     'drv',
663     'move',
664     'edit',
665     'file',
666     'clipboard',
667     'search',
668     'tags',
669     'syntax',
670     'macro',
671     'templates',
672     'spell',
673     'misc',
674     'crypt',
675     'keyseq',
676     'session',
677     'build',
678     'writing',
679     'toys',
680     'vcs'
683 foreach (m, mp_modules) {
684     eval('load("mp_' ~ m ~ '.mpsl");');
686     if (ERROR != NULL)
687         mp.exit_message = mp.exit_message ~ ERROR ~ "\n";
690 ERROR = NULL;
692 /** main **/
694 mp.load_profile();
695 mp.setup_language();
696 mp.drv.startup();
697 mp.process_cmdline();
698 mp.test_versions();
699 mp.drv.main_loop();
700 mp.drv.shutdown();