1/*
2
3    Minimum Profit - A Text Editor
4    File manipulation.
5
6    ttcdt <dev@triptico.com> et al.
7
8    This software is released into the public domain.
9    NO WARRANTY. See file LICENSE for details.
10
11*/
12
13/** RTF config tweaks **/
14
15mp.config.rtf_font_face      = 'Times New Roman';
16mp.config.rtf_font_size      = 12;
17mp.config.rtf_mono_font_face = 'Courier New';
18mp.config.rtf_mono_font_size = 10;
19mp.config.rtf_style_default  = '\sa227\sl480\slmult1';
20mp.config.rtf_style_first    = '\qj\fi0';
21mp.config.rtf_style_para     = '\qj\fi567';
22mp.config.rtf_style_center   = '\qc\fi0';
23mp.config.rtf_style_verbatim = '\ql\fi0\sl0';
24mp.config.troff_font_size    = 12;
25mp.config.troff_page_height  = 10;
26mp.config.html_title         = '';
27
28/** editor actions **/
29
30mp.actions['new']   = sub {
31    local d = mp.find_file_by_name(L("<unnamed>"));
32
33    if (d != -1)
34        d = mp.set_active(d);
35    else
36        d = mp.add(new(mp_doc, { is_new: 1 }));
37
38    return d;
39};
40
41mp.actions['next'] = sub { mp.next(); };
42mp.actions['prev'] = sub { mp.prev(); };
43
44mp_doc.actions['save_as'] = sub (d, newname) {
45
46    if (newname == NULL)
47        newname = mp.savefile(L("Save file as") + ':');
48
49    if (newname != NULL) {
50        /* store new name */
51        d.name = newname;
52
53        if (d->long_op(d.save) == -1)
54            mp.alert(sprintf(L("Error saving '%s': %s"),
55                        mp.trim(d.name, 32), ERRNO));
56        else
57            d->detect_syntax();
58    }
59
60    return d;
61};
62
63mp_doc.actions['save'] = sub (d) {
64
65    /* name is <unnamed> or something similar; ask for one */
66    if (regex(d.name, "/^<.+>$/"))
67        d->actions.save_as();
68    else
69    if (d->long_op(d.save) == -1)
70        mp.alert(sprintf(L("Error saving '%s': %s"),
71                        mp.trim(d.name, 32), ERRNO));
72
73    return d;
74};
75
76mp_doc.actions['export'] = sub (d, filename) {
77    local ret = 0;
78
79    mp.load_on_demand(mp_mptxt, "mp_mptxt.mpsl");
80
81    local formats = mp_mptxt.exporters->map(index)->join(", ");
82
83    if (filename == NULL)
84        filename = mp.savefile(L("Export to file (available formats: ")
85            + formats + '):');
86
87    if (filename != NULL) {
88        local format = filename->split(".")->pop()->lc();
89
90        d->busy(1);
91
92        ret = mp_mptxt.export(d, format);
93
94        d->busy(0);
95
96        if (ret == NULL)
97            mp.alert(sprintf(L("Unsupported format '%s'.\n(Not one of: [%s])"),
98                format, formats));
99        else {
100            local p_enc = ENCODING;
101            encoding(mp_mptxt.encodings[format]);
102
103            if ((f = open(filename, "wb"))) {
104                f->write(ret)->close();
105            }
106            else {
107                mp.alert(sprintf(L("Error exporting '%s': %s"),
108                    mp.trim(filename, 32), ERRNO));
109            }
110
111            encoding(p_enc);
112        }
113    }
114};
115
116mp.actions['close'] = sub (d) {
117    if (d->query_close() != 0)
118        mp.close();
119};
120
121mp.actions['exit']  = sub {
122    if (mp.config.auto_sessions)
123        mp.save_session();
124
125    if (mp.actions.close_all())
126        mp_c.exit();
127};
128
129mp.actions['suspend'] = sub {
130    mp_drv.suspend();
131};
132
133mp.actions['open'] = sub (d, filename) {
134    if (filename == NULL)
135        filename = mp.openfile(L("File to open") + ':');
136
137    if (filename != NULL && filename != "") {
138        if (mp.long_op(mp.open, filename) == NULL) {
139            if (ERRNO == NULL) {
140                /* not open but no ERRNO: it's encrypted */
141                local f = mp.form(
142                    [
143                        {
144                            type: 'password',
145                            label: L("Password") + ':'
146                        }
147                    ]
148                );
149
150                if (f != NULL) {
151                    if (mp.long_op(mp.open, filename, f[0]) == NULL)
152                        mp.alert(sprintf(L("Bad password for file '%s'"),
153                            mp.trim(filename, 32)));
154                }
155            }
156            else
157                mp.alert(sprintf(L("Error opening '%s': %s"),
158                            mp.trim(filename, 32), ERRNO));
159        }
160    }
161
162    return filename;
163};
164
165
166mp.actions['open_recent'] = sub (d) {
167    /* create the list of filenames... */
168    local filenames = mp.state.files->map(index)->
169        /* delete all those not having a timestamp... */
170        grep(sub (v, i) { exists(mp.state.files[v], 't'); })->
171
172        /* sort by timestamp... */
173        sort(sub (a, b) { cmp(mp.state.files[b].t, mp.state.files[a].t); })->
174
175        /* and add a timestamp for humans */
176        map(sub (v) { v + sprintf("\t%t{%Y-%m-%d %H:%M:%S}", mp.state.files[v].t); });
177
178    local form = mp.form(
179        [
180            {
181                label:  L("Recent files") + ':',
182                type:   'list',
183                list:   filenames
184            }
185        ]
186    );
187
188    if (form != NULL) {
189        mp.actions.open(d, filenames[form[0]]->split("\t")->shift());
190    }
191};
192
193
194mp.actions['open_folder'] = sub {
195    local f;
196
197    if ((f = mp_drv.openfolder(L("Folder to open"))) != NULL)
198        mp.actions.open(NULL, f);
199};
200
201mp_doc.actions['revert'] = sub (d) {
202    /* save current name */
203    local p = d.name;
204
205    if (d.txt.mod) {
206        local r;
207        r = mp.confirm(sprintf(L("'%s' has changed. Are you sure?"),
208                            mp.trim(p, 32)));
209
210        /* cancel? don't close */
211        if (r == 0 || r == 2)
212            return d;
213    }
214
215    d->store_undo();
216
217    mp.close();
218    if (mp.long_op(mp.open, p) == NULL && ERRNO != NULL)
219        mp.alert(sprintf("Error opening '%s': %s", p, ERRNO));
220
221    local nd = mp.active();
222
223    nd.undo_q = d.undo_q;
224
225    nd->set_y(d.txt.y)->set_x(d.txt.x);
226    nd.txt.vy = d.txt.vy;
227
228    return nd;
229};
230
231mp.actions['open_config_file']  = sub {
232    mp.open(mp.config_file);
233};
234
235mp.actions['sync'] = sub {
236    /* save all modified documents */
237    foreach (d, grep(mp.docs, sub (e) { e.txt.mod; }))
238        d->actions.save();
239
240    return d;
241};
242
243mp_doc.actions['exec_command'] = sub (d, cmd) {
244    if (cmd == NULL) {
245        local t = mp.form(
246            [
247                {
248                    label:      L("System command") + ':',
249                    type:       'text',
250                    history:    'system'
251                }
252            ]
253        );
254
255        if (t != NULL)
256            cmd = t[0];
257    }
258
259    if (cmd != NULL) {
260        /* does it start with a pipe? */
261        if (regex(cmd, '/^\|/')) {
262            local p;
263
264            /* yes; current document should be fed to it */
265            cmd = sregex(cmd, '/^\|/');
266
267            if ((p = popen(cmd, "w")) != NULL) {
268                mp.busy(1);
269
270                foreach (l, d->get_active_area()->clone()->mp_c.vw_unwrap())
271                    write(p, l + mp.config.eol);
272
273                pclose(p);
274                mp.busy(0);
275            }
276            else
277                mp.alert(sprintf(L("Error writing to command '%s'"), cmd));
278        }
279        else {
280            /* no; execute command and insert into cursor */
281            local p;
282
283            if ((p = popen(cmd, "r")) != NULL) {
284                local l;
285
286                d->store_undo();
287                mp.busy(1);
288
289                /* prevent auto indenting the command output */
290                local p_config = clone(mp.config);
291
292                mp.config.auto_indent = 0;
293
294                foreach (l, p)
295                    d->insert(l);
296
297                mp.config = p_config;
298
299                /* invalidate window size to trigger a visual re-wrap */
300                mp.txt.last_tx = 0;
301
302                pclose(p);
303                mp.busy(0);
304            }
305            else
306                mp.alert(sprintf(L("Error reading from command '%s'"), cmd));
307        }
308    }
309
310    return d;
311};
312
313mp_doc.actions['filter_selection'] = sub (d, cmd) {
314    if (cmd == NULL) {
315        local t = mp.form(
316            [
317                {
318                    'label'     => L("System command") + ':',
319                    'type'      => 'text',
320                    'history'   => 'system2'
321                }
322            ]
323        );
324
325        if (t != NULL)
326            cmd = t[0];
327    }
328
329    if (cmd != NULL) {
330        d->store_undo();
331
332        /* if there is no selection, take full document */
333        if (d.txt.mark == NULL) {
334            d->move_bof()->mark()->move_eof()->mark();
335        }
336
337        /* take it out */
338        d->cut();
339
340        /* now feed it to the command */
341        local p = popen2(cmd);
342
343        if (p != NULL) {
344            write(p[1], join(mp.clipboard, "\n"));
345            pclose(p[1]);
346
347            local l;
348            while ((l = read(p[0])) != NULL)
349                d->insert(l);
350
351            pclose(p[0]);
352        }
353    }
354
355    return d;
356};
357
358mp.actions['close_all'] = sub {
359    local s;
360
361    while (s = size(mp.docs)) {
362        local doc = mp.docs[mp.active_i];
363
364        /* close current document */
365        mp.actions.close(doc);
366
367        /* if the size of the list hasn't changed,
368            action was cancelled, so don't exit */
369        if (s == size(mp.docs))
370            return 0;
371    }
372
373    return 1;
374};
375
376mp_doc.actions['open_under_cursor'] = sub (d) {
377    local w;
378
379    if (d.txt.lines[d.txt.y]->regex('/^@@ /')) {
380        /* it's a diff mark */
381
382        /* pick line */
383        local l = d.txt.lines[d.txt.y]->regex('/[0-9]+/');
384        local f;
385        local y = d.txt.y - 1;
386
387        foreach (_, y) {
388            /* pick filename (stripping mark from previous line) */
389            if (d.txt.lines[y]->regex('/^\+\+\+ /')) {
390                f = d.txt.lines[y]->sregex('/^\+\+\+ .\//');
391                break;
392            }
393        }
394
395        if (f != NULL)
396            d = mp.open(f + ':' + l + ':');
397    }
398    else
399    if ((w = d->get_word(regcomp("/[^ \t\"\'<>]+/"))) != NULL)
400        d = mp.open(w);
401
402    return d;
403};
404
405
406sub mp.load_on_demand(sym, src)
407/* loads a source code file on demand and whines if it can't */
408{
409    local ok = bool(1);
410
411    if (sym == NULL) {
412        eval(sub { load(src); } );
413
414        if (ERROR) {
415            mp.alert(ERROR);
416            ERROR = NULL;
417            ok = !ok;
418        }
419    }
420
421    return ok;
422}
423
424
425mp.actions['hex_view']  = sub (doc, filename) {
426    local d = NULL;
427
428    if (filename == NULL)
429        filename = mp.openfile(L("File to open") + ':');
430
431    if (filename != NULL && filename != "") {
432        if (mp.load_on_demand(mp_hex_view, "mp_hex.mpsl")) {
433            d = new(mp_hex_view, { name: filename })->init();
434
435            if (d == NULL)
436                mp.alert(sprintf("Error opening '%s': %s",
437                            mp.trim(filename, 32), ERRNO));
438            else
439                mp.add(d);
440        }
441    }
442
443    return d;
444};
445
446mp.actions['open_dropped_files'] = sub {
447    while (size(mp.dropped_files))
448        mp.open(shift(mp.dropped_files));
449};
450
451
452mp_doc.actions["idle"] = sub (d) {
453    /* do nothing */
454    return d;
455};
456
457
458/** default key bindings **/
459
460mp.keycodes['ctrl-n']           = 'next';
461mp.keycodes['ctrl-o']           = 'open';
462mp.keycodes['ctrl-q']           = 'exit';
463mp.keycodes['ctrl-l']           = 'suspend';
464mp.keycodes['ctrl-w']           = 'close';
465mp.keycodes['dropped-files']    = 'open_dropped_files';
466mp.keycodes['close-window']     = 'exit';
467mp_doc.keycodes['ctrl-s']       = 'save';
468mp_doc.keycodes['ctrl-enter']   = 'open_under_cursor';
469mp_doc.keycodes['alt-enter']    = 'ctrl-enter';
470mp_doc.keycodes['alt-o']        = 'ctrl-enter';
471mp_doc.keycodes['ctrl-k']       = "toggle_visual_wrap";
472mp_doc.keycodes["idle"]         = "idle";
473
474/** action descriptions **/
475
476mp.actdesc['new']                = LL("New");
477mp.actdesc['save']               = LL("Save...");
478mp.actdesc['save_as']            = LL("Save as...");
479mp.actdesc['export']             = LL("Export...");
480mp.actdesc['next']               = LL("Next");
481mp.actdesc['prev']               = LL("Previous");
482mp.actdesc['open']               = LL("Open...");
483mp.actdesc['exit']               = LL("Exit");
484mp.actdesc['suspend']            = LL("Suspend");
485mp.actdesc['close']              = LL("Close");
486mp.actdesc['revert']             = LL("Revert");
487mp.actdesc['close_all']          = LL("Close all");
488mp.actdesc['open_folder']        = LL("Open folder...");
489
490mp.actdesc['open_config_file']   = LL("Edit configuration file");
491mp.actdesc['sync']               = LL("Save modified texts");
492mp.actdesc['exec_command']       = LL("Run system command...");
493mp.actdesc['filter_selection']   = LL("Filter selection through system command...");
494mp.actdesc['open_under_cursor']  = LL("Open file under cursor");
495mp.actdesc['hex_view']           = LL("Hexadecimal viewer...");
496mp.actdesc['open_dropped_files'] = LL("Open dropped files");
497mp.actdesc['open_recent']        = LL("Open recent files...");
498mp.actdesc['toggle_visual_wrap'] = LL("Toggle visual wrap");
499mp.actdesc['idle']               = LL("Called when idle");
500
501/** code **/
502
503sub mp.chomp(str)
504/* chomps the end of file chars from a string */
505{
506    sregex(str, "/\r*\n*$/");
507}
508
509
510sub mp_doc.recov_file_name(doc)
511/* returns the recovery file name for this document */
512{
513    local fn = doc.name;
514
515    /* if it's not a recovery file itself, calculate it */
516    if (!regex(fn, "/\.recov$"))
517        fn = HOMEDIR + ".mp-" + md5(fn) + ".recov";
518
519    return fn;
520}
521
522
523sub mp_doc.pre_event(doc, k)
524/* an event is to be processed */
525{
526    if (doc.mtime && time() > mp.mtime_test) {
527        local s;
528
529        if ((s = stat(doc.name)) != NULL && s[9] > doc.mtime) {
530            if (mp.confirm(sprintf(L("'%s' was changed externally. Reload?"),
531                mp.trim(doc.name, 32))) == 1) {
532                doc->actions.revert();
533                k = NULL;
534            }
535
536            doc.mtime = s[9];
537        }
538
539        mp.mtime_test = time() + 2;
540    }
541
542    return k;
543}
544
545
546sub mp_doc.post_event(doc, k)
547/* an event has just been processed */
548{
549    /* if it's read only but has modifications, revert them */
550    if (doc.read_only && size(doc.undo_q)) {
551        while (size(doc.undo_q))
552            doc->undo();
553
554        /* forget the last insert time */
555        doc.last_insert = 0;
556
557        mp.message = {
558            timeout:    time() + 2,
559            string:     '*' + L("Read-only document") + '*'
560        };
561    }
562
563    /* only try auto save if it's not disabled */
564    if (mp.config.auto_save_period)
565        mp.auto_save();
566
567    return doc;
568}
569
570
571sub mp.auto_save()
572/* auto save recovery files */
573{
574    local t = time();
575
576    foreach (doc, mp.docs) {
577        if (doc.txt.mod) {
578            /* first time? just store current time */
579            if (doc.auto_save_time == NULL)
580                doc.auto_save_time = t;
581            else
582            /* time to check again? */
583            if (t > doc.auto_save_time + mp.config.auto_save_period) {
584                local f;
585
586                if ((f = open(doc->recov_file_name(), "w")) != NULL) {
587                    f->write(
588                        sprintf(L("RECOVERED DATA for %s at %t{%Y-%m-%d %H:%M:%S}"),
589                            doc.name, t));
590
591                    foreach (l, doc.txt.lines)
592                        f->write("\n")->write(l);
593
594                    f->close();
595                }
596
597                /* store checked time */
598                doc.auto_save_time = t;
599            }
600        }
601        else {
602            /* reset timer */
603            doc.auto_save_time = NULL;
604        }
605    }
606}
607
608
609sub mp_doc.query_close(doc)
610/* queries a close; if it returns 0, the closing is rejected */
611{
612    local r = 1;
613
614    if (doc.txt.mod) {
615        r = mp.confirm(sprintf(L("'%s' has changed. Save changes?"),
616                    mp.trim(doc.name, 32)));
617
618        /* confirm? save */
619        if (r == 1)
620            doc->actions.save();
621    }
622
623    return r;
624}
625
626
627sub mp.save_th(f, doc)
628/* mp.save() helper */
629{
630    local eol = mp.config.keep_eol && doc.eol || mp.config.eol;
631    local lines;
632
633    doc.disk_op = 1;
634
635    if (doc->visual_wrap())
636        lines = clone(doc.txt.lines)->mp_c.vw_unwrap();
637    else
638        lines = doc.txt.lines;
639
640    /* save as a plain text file */
641    foreach (l, nl, lines) {
642        /* write a line separator if it's not the first line */
643        if (nl)
644            write(f, eol);
645
646        write(f, l);
647    }
648
649    doc.disk_op = 0;
650
651    return nl;
652}
653
654
655sub mp_doc.save(doc)
656/* saves a file */
657{
658    local f;
659    local s = NULL;
660    local ret = 0;
661
662    /* if unlink before write is desired, do it */
663    if (mp.config.unlink && (s = stat(doc.name)) != NULL)
664        unlink(doc.name);
665
666    /* set the encoding for this file opening */
667    TEMP_ENCODING = doc.encoding;
668
669    if ((f = open(doc.name, "wb")) == NULL) {
670        /* can't write? delete name */
671        doc.name = L("<unnamed>");
672        ret = -1;
673    }
674    else {
675        ret = 0;
676
677        /* if the document has a password, save it encrypted */
678        if (doc.password)
679            mp.crypt_save(f, doc.txt.lines, doc.password, doc.crypt_ver);
680        else
681            mp.save_th(f, doc);
682
683        close(f);
684        doc.txt.mod = 0;
685
686        /* set back the permissions and ownership, if available */
687        if (s != NULL) {
688            chmod(doc.name, s[2]);
689            chown(doc.name, s[4], s[5]);
690        }
691
692        s = stat(doc.name);
693        doc.mtime  = s[9];
694        doc.is_new = 0;
695
696        /* delete the recovery file */
697        unlink(doc->recov_file_name());
698    }
699
700    return ret;
701}
702
703
704sub mp_doc.long_op(doc, func, a2, a3, a4)
705{
706    local r;
707
708    doc->busy(1);
709    r = doc->func(a2, a3, a4);
710    doc->busy(0);
711
712    return r;
713}
714
715
716sub mp.add(doc)
717/* adds a doc to the list of documents */
718{
719    /* store in the list and set as active */
720    ins(mp.docs, doc, mp.active_i);
721
722    return mp.set_active(mp.active_i);
723}
724
725
726sub mp.next()
727/* rotates through the document list */
728{
729    return mp.set_active((mp.active_i + 1) % count(mp.docs));
730}
731
732
733sub mp.prev()
734/* rotates through the document list, backwards */
735{
736    return mp.set_active((mp.active_i - 1 + count(mp.docs)) % count(mp.docs));
737}
738
739
740sub mp_doc.state(doc)
741/* returns a structure to be store into the state */
742{
743    local state = NULL;
744
745    if (!regex(doc.name, "/^</") && !doc.no_state && !doc.is_new)
746        state = { vy: doc.txt.vy, o: doc->get_offset(), t: time() };
747
748    return state;
749}
750
751
752sub mp.close()
753/* closes the active document */
754{
755    /* no state for files? create it */
756    if (!exists(mp.state, 'files'))
757        mp.state.files = {};
758
759    /* store in the state of files */
760    local l = mp.active();
761
762    local state = l->state();
763
764    if (state != NULL)
765        mp.state.files[l.name] = state;
766
767    /* delete the recovery file */
768    if (l->exists("recov_file_name"))
769        unlink(l->recov_file_name());
770
771    /* delete from the list */
772    del(mp.docs, mp.active_i);
773
774    /* rotate if it was the last one */
775    if (mp.active_i == size(mp.docs))
776        mp.active_i = 0;
777
778    mp.set_active(mp.active_i);
779
780    /* cannot call mp.active() */
781}
782
783
784sub mp.find_file_by_name(filename)
785/* finds an open file by its name */
786{
787    mp.docs->map(sub(d) { d.name; })->seek(filename);
788}
789
790
791sub mp.open(filename, password)
792/* opens a new document */
793{
794    local d, s, f;
795    local x, y;
796
797    /* test if filename includes :y: or :y:x: positioning */
798    if (regex(filename, "/.+:[0-9]+:([0-9]+:)?$/")) {
799        local l = split(filename, ':');
800
801        x        = integer(l[2]);
802        y        = integer(l[1]);
803        filename = l[0];
804    }
805
806    if ((s = mp.find_file_by_name(filename)) != -1) {
807        /* already open */
808        d = mp.set_active(s);
809    }
810    else
811    if ((s = stat(filename)) == NULL) {
812        /* non-existent file; create as new */
813        mp.message = {
814            timeout:    time() + 2,
815            string:     sprintf(L("New file '%s'"), filename)
816        };
817
818        ERRNO = NULL;
819
820        d = mp.add(new(mp_doc, { name: filename, is_new: 1 }));
821    }
822    else
823    if (s[13] != NULL) {
824        local r;
825
826        /* canonicalize, if possible */
827        filename = s[13];
828
829        /* look again for this filename in the open files */
830        if ((r = mp.find_file_by_name(filename)) != -1)
831            d = mp.set_active(r);
832    }
833
834    if (d == NULL) {
835        /* still not open: load */
836        f = open(filename, "rb");
837
838        if (f != NULL) {
839            local ver;
840
841            if ((ver = mp.crypt_detect(f))) {
842                ERRNO = NULL;
843
844                if (password != NULL && password != '') {
845                    /* and load the file */
846                    local lines = mp.crypt_load(f, password, ver);
847
848                    if (lines != NULL) {
849                        d = mp.add(new(mp_doc, { name: filename }));
850                        d.txt.lines = lines;
851                        d.password  = password;
852                        d.crypt_ver = ver;
853                    }
854                }
855            }
856            else {
857                /* mp.crypt_detect() called read(), so there
858                   is (possibly) and eol already detected */
859                local e = eol(f);
860
861                /* close file (needed for rewinding AND
862                   possible encoding autodetection) */
863                close(f);
864
865                /* reopen and read in auto-chomp mode */
866                AUTO_CHOMP = 1;
867
868                f = open(filename, "rb");
869                d = mp.add(new(mp_doc, { name: filename }));
870
871                d.txt.lines = map(f);
872
873                if (count(d.txt.lines) == 0)
874                    d.txt.lines->push('');
875
876                /* store the detected eol or the default one */
877                d.eol = e || mp.config.eol;
878
879                AUTO_CHOMP = 0;
880            }
881
882            close(f);
883        }
884        else
885        if (bitand(s[2], 0040000)) {
886            /* it's a directory: show it */
887            ERRNO = NULL;
888
889            if (mp.load_on_demand(mp_doc_dir, "mp_dir.mpsl")) {
890                d = mp.add(new(mp_doc_dir, { name: filename }));
891                d->setup();
892            }
893        }
894
895        if (d) {
896            /* store mtime */
897            d.mtime = s[9];
898
899            /* store the encoding */
900            d.encoding = DETECTED_ENCODING || ENCODING || '';
901
902            d->detect_syntax();
903
904            /* ignore further system errors */
905            ERRNO = NULL;
906        }
907    }
908
909    if (d) {
910        /* look in the state if we have a previous position */
911        local p;
912        if ((p = mp.state.files[filename]) != NULL) {
913            /* if it has an offset, use it */
914            if (p->exists("o"))
915                d->set_offset(integer(p.o));
916            else {
917                d->set_y(integer(p.y));
918                d->set_x(integer(p.x));
919            }
920
921            d.txt.vy = integer(p.vy);
922        }
923
924        if (y)
925            d->search_set_y(y - 1);
926        if (x)
927            d->set_x(x - 1);
928
929        /* filter the document through the spellchecker (if it's active) */
930        mp.spell_document(d);
931    }
932
933    return d;
934}
935
936
937/** visual wrap **/
938
939sub mp.vw_col()
940/* returns the column for visual wrap */
941{
942    local tx = mp.window.tx;
943
944    if (tx > mp.config.double_page)
945        tx /= 2;
946
947    return tx - 2;
948}
949
950
951sub mp_doc.vw_unwrap(doc)
952/* unwraps a document, keeping the position */
953{
954    doc->busy(1);
955    local o = doc->get_offset();
956    mp_c.vw_unwrap(doc.txt.lines);
957    doc->set_offset(o);
958    doc->busy(0);
959}
960
961
962sub mp_doc.vw_wrap(doc)
963/* wraps a document, keeping the position */
964{
965    doc->busy(1);
966    local o = doc->get_offset();
967    mp_c.vw_wrap(doc.txt.lines, mp.vw_col());
968    doc->set_offset(o);
969    doc.txt.vx = 0;
970    doc->busy(0);
971}
972
973
974mp_doc.actions["toggle_visual_wrap"] = sub (d) {
975    mp.config.visual_wrap = !mp.config.visual_wrap;
976
977    if (d->visual_wrap())
978        d->vw_wrap();
979    else
980        d->vw_unwrap();
981};
982
983
984sub mp_doc.get_offset(doc)
985/* returns the offset of the cursor from the document */
986{
987    return mp_c.get_offset(doc.txt.lines, doc.txt.y) + doc.txt.x;
988}
989
990
991sub mp_doc.set_offset(doc, o)
992/* moves the cursor to an absolute offset */
993{
994    local r = mp_c.set_offset(doc.txt.lines, o);
995
996    doc->set_y(r[1]);
997    doc->set_x(r[0]);
998    txt.vx = 0;
999}
1000