document.vala 16.1 KB
Newer Older
Sébastien Wilmet's avatar
Sébastien Wilmet committed
1
2
3
/*
 * This file is part of LaTeXila.
 *
4
 * Copyright © 2010-2011 Sébastien Wilmet
Sébastien Wilmet's avatar
Sébastien Wilmet committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 *
 * LaTeXila is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * LaTeXila is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LaTeXila.  If not, see <http://www.gnu.org/licenses/>.
 */

Sébastien Wilmet's avatar
Sébastien Wilmet committed
20
21
using Gtk;

22
public class Document : Gtef.Buffer
Sébastien Wilmet's avatar
Sébastien Wilmet committed
23
24
{
    public File location { get; set; }
25
    public bool readonly { get; set; default = false; }
26
    public weak DocumentTab tab;
27
    public uint _unsaved_doc_num = 0;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
28
    public int project_id { get; set; default = -1; }
29
    private bool backup_made = false;
30
    private string _etag;
31
    private string? encoding = null;
32
    private bool new_file = true;
33
    private DocumentStructure _structure = null;
34
    private FileInfo _metadata_info = new FileInfo ();
35

Sébastien Wilmet's avatar
Sébastien Wilmet committed
36
37
    public Document ()
    {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
38
39
40
41
        // syntax highlighting: LaTeX by default
        var lm = Gtk.SourceLanguageManager.get_default ();
        set_language (lm.get_language ("latex"));

Sébastien Wilmet's avatar
Sébastien Wilmet committed
42
43
44
45
46
        notify["location"].connect (() =>
        {
            update_syntax_highlighting ();
            update_project_id ();
        });
47

48
49
        changed.connect (() =>
        {
50
            new_file = false;
51
        });
52
53
54
55
56

        GLib.Settings editor_settings =
            new GLib.Settings ("org.gnome.latexila.preferences.editor");
        editor_settings.bind ("scheme", this, "gtef-style-scheme-id",
            SettingsBindFlags.GET);
Sébastien Wilmet's avatar
Sébastien Wilmet committed
57
58
    }

59
    public new void insert (ref TextIter iter, string text, int len)
60
    {
61
62
63
        Gtk.SourceCompletion completion = tab.view.completion;
        completion.block_interactive ();

64
        base.insert (ref iter, text, len);
65

66
67
        // HACK: wait one second before delocking completion, it's better than doing a
        // Utils.flush_queue ().
68
69
        Timeout.add_seconds (1, () =>
        {
70
            completion.unblock_interactive ();
71
72
73
74
            return false;
        });
    }

75
    public void load (File location)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
76
    {
77
78
79
80
81
82
83
84
85
86
87
88
        // First load metadata so when the notify::location signal is emitted,
        // get_metadata() works.
        try
        {
            _metadata_info = location.query_info ("metadata::*", FileQueryInfoFlags.NONE);
        }
        catch (Error e)
        {
            warning ("Get document metadata failed: %s", e.message);
            _metadata_info = new FileInfo ();
        }

Sébastien Wilmet's avatar
Sébastien Wilmet committed
89
90
91
92
        this.location = location;

        try
        {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
93
94
95
            uint8[] chars;
            location.load_contents (null, out chars, out _etag);
            string text = (string) (owned) chars;
96
97
98
99
100
101
102
103
104
105

            if (text.validate ())
                set_contents (text);

            // convert to UTF-8
            else
            {
                string utf8_text = to_utf8 (text);
                set_contents (utf8_text);
            }
Sébastien Wilmet's avatar
Sébastien Wilmet committed
106
107

            update_syntax_highlighting ();
108
109

            RecentManager.get_default ().add_item (location.get_uri ());
Sébastien Wilmet's avatar
Sébastien Wilmet committed
110
        }
111
        catch (Error e)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
112
        {
113
            warning ("%s", e.message);
114

Sébastien Wilmet's avatar
Sébastien Wilmet committed
115
            string primary_msg = _("Impossible to load the file '%s'.")
116
117
                .printf (location.get_parse_name ());
            tab.add_message (primary_msg, e.message, MessageType.ERROR);
Sébastien Wilmet's avatar
Sébastien Wilmet committed
118
119
120
        }
    }

Sébastien Wilmet's avatar
Sébastien Wilmet committed
121
122
    public void set_contents (string contents)
    {
123
124
125
126
127
        // if last character is a new line, don't display it
        string? contents2 = null;
        if (contents[contents.length - 1] == '\n')
            contents2 = contents[0:-1];

Sébastien Wilmet's avatar
Sébastien Wilmet committed
128
        begin_not_undoable_action ();
129
        set_text (contents2 ?? contents, -1);
130
        new_file = true;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
131
132
133
134
135
136
137
138
139
        set_modified (false);
        end_not_undoable_action ();

        // move the cursor at the first line
        TextIter iter;
        get_start_iter (out iter);
        place_cursor (iter);
    }

140
    public void save (bool check_file_changed_on_disk = true, bool force = false)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
141
    {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
142
143
144
        return_if_fail (location != null);

        // if not modified, don't save
145
        if (! force && ! new_file && ! get_modified ())
Sébastien Wilmet's avatar
Sébastien Wilmet committed
146
            return;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
147
148
149

        // we use get_text () to exclude undisplayed text
        TextIter start, end;
150
        get_bounds (out start, out end);
151
152
153
154
155
        string text = get_text (start, end, false);

        // the last character must be \n
        if (text[text.length - 1] != '\n')
            text = @"$text\n";
Sébastien Wilmet's avatar
Sébastien Wilmet committed
156
157
158

        try
        {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
159
160
            GLib.Settings settings =
                new GLib.Settings ("org.gnome.latexila.preferences.editor");
161
162
163
            bool make_backup = ! backup_made
                && settings.get_boolean ("create-backup-copy");

164
            string? etag = check_file_changed_on_disk ? _etag : null;
165

166
            // if encoding specified, convert to this encoding
167
            if (encoding != null)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
168
                text = convert (text, (ssize_t) text.length, encoding, "UTF-8");
169

170
171
            // else, convert to the system default encoding
            else
Sébastien Wilmet's avatar
Sébastien Wilmet committed
172
                text = Filename.from_utf8 (text, (ssize_t) text.length, null, null);
173

174
175
176
177
178
            // check if parent directories exist, if not, create it
            File parent = location.get_parent ();
            if (parent != null && ! parent.query_exists ())
                parent.make_directory_with_parents ();

179
            location.replace_contents (text.data, etag, make_backup,
Sébastien Wilmet's avatar
Sébastien Wilmet committed
180
                FileCreateFlags.NONE, out _etag, null);
181

Sébastien Wilmet's avatar
Sébastien Wilmet committed
182
            set_modified (false);
183
184

            RecentManager.get_default ().add_item (location.get_uri ());
185
            backup_made = true;
186
187

            save_metadata ();
Sébastien Wilmet's avatar
Sébastien Wilmet committed
188
        }
189
        catch (Error e)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
190
        {
191
192
            if (e is IOError.WRONG_ETAG)
            {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
193
                string primary_msg = _("The file %s has been modified since reading it.")
194
                    .printf (location.get_parse_name ());
Sébastien Wilmet's avatar
Sébastien Wilmet committed
195
196
                string secondary_msg =
                    _("If you save it, all the external changes could be lost. Save it anyway?");
197
                Gtef.InfoBar infobar = tab.add_message (primary_msg, secondary_msg,
198
                    MessageType.WARNING);
199
200
                infobar.add_button (_("_Save Anyway"), ResponseType.YES);
                infobar.add_button (_("_Don't Save"), ResponseType.CANCEL);
201
202
203
204
205
206
207
208
209
                infobar.response.connect ((response_id) =>
                {
                    if (response_id == ResponseType.YES)
                        save (false);
                    infobar.destroy ();
                });
            }
            else
            {
210
                warning ("%s", e.message);
211

Sébastien Wilmet's avatar
Sébastien Wilmet committed
212
                string primary_msg = _("Impossible to save the file.");
213
                Gtef.InfoBar infobar = tab.add_message (primary_msg, e.message,
Sébastien Wilmet's avatar
Sébastien Wilmet committed
214
                    MessageType.ERROR);
215
                infobar.add_close_button ();
216
            }
Sébastien Wilmet's avatar
Sébastien Wilmet committed
217
218
        }
    }
Sébastien Wilmet's avatar
Sébastien Wilmet committed
219

220
221
222
223
224
225
    private string to_utf8 (string text) throws ConvertError
    {
        foreach (string charset in Encodings.CHARSETS)
        {
            try
            {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
226
                string utf8_text = convert (text, (ssize_t) text.length, "UTF-8",
227
228
229
230
231
232
233
234
235
236
237
238
239
                    charset);
                encoding = charset;
                return utf8_text;
            }
            catch (ConvertError e)
            {
                continue;
            }
        }
        throw new GLib.ConvertError.FAILED (
            _("Error trying to convert the document to UTF-8"));
    }

Sébastien Wilmet's avatar
Sébastien Wilmet committed
240
241
    private void update_syntax_highlighting ()
    {
Sébastien Wilmet's avatar
Sébastien Wilmet committed
242
        Gtk.SourceLanguageManager lm = Gtk.SourceLanguageManager.get_default ();
Sébastien Wilmet's avatar
Sébastien Wilmet committed
243
244
245
        string content_type = null;
        try
        {
246
            FileInfo info = location.query_info (FileAttribute.STANDARD_CONTENT_TYPE,
Sébastien Wilmet's avatar
Sébastien Wilmet committed
247
248
249
250
251
252
253
254
                FileQueryInfoFlags.NONE, null);
            content_type = info.get_content_type ();
        }
        catch (Error e) {}

        var lang = lm.guess_language (location.get_parse_name (), content_type);
        set_language (lang);
    }
255

Sébastien Wilmet's avatar
Sébastien Wilmet committed
256
257
    private void update_project_id ()
    {
258
259
        int i = 0;
        foreach (Project project in Projects.get_default ())
Sébastien Wilmet's avatar
Sébastien Wilmet committed
260
        {
261
            if (location.has_prefix (project.directory))
Sébastien Wilmet's avatar
Sébastien Wilmet committed
262
263
264
265
            {
                project_id = i;
                return;
            }
266
            i++;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
267
        }
268
269

        project_id = -1;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
270
271
    }

272
273
274
275
276
    public string get_uri_for_display ()
    {
        if (location == null)
            return get_unsaved_document_name ();

277
        return Latexila.utils_replace_home_dir_with_tilde (location.get_parse_name ());
278
279
280
281
282
283
284
285
286
287
288
    }

    public string get_short_name_for_display ()
    {
        if (location == null)
            return get_unsaved_document_name ();

        return location.get_basename ();
    }

    private string get_unsaved_document_name ()
289
    {
290
        uint num = get_unsaved_document_num ();
291
        return _("Untitled Document") + @" $num";
292
293
294
295
296
297
298
299
300
301
302
    }

    private uint get_unsaved_document_num ()
    {
        return_val_if_fail (location == null, 0);

        if (_unsaved_doc_num > 0)
            return _unsaved_doc_num;

        // get all unsaved document numbers
        uint[] all_nums = {};
303
        foreach (Document doc in LatexilaApp.get_instance ().get_documents ())
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
        {
            // avoid infinite loop
            if (doc == this)
                continue;

            if (doc.location == null)
                all_nums += doc.get_unsaved_document_num ();
        }

        // take the first free num
        uint num;
        for (num = 1 ; num in all_nums ; num++);

        _unsaved_doc_num = num;
        return num;
319
    }
320
321
322
323
324
325
326
327
328
329
330
331
332

    public bool is_local ()
    {
        if (location == null)
            return false;
        return location.has_uri_scheme ("file");
    }

    public bool is_externally_modified ()
    {
        if (location == null)
            return false;

333
334
335
        string current_etag = null;
        try
        {
336
            FileInfo file_info = location.query_info (FileAttribute.ETAG_VALUE,
337
338
339
340
341
342
343
344
345
                FileQueryInfoFlags.NONE, null);
            current_etag = file_info.get_etag ();
        }
        catch (GLib.Error e)
        {
            return false;
        }

        return current_etag != null && current_etag != _etag;
346
347
    }

348
    public void comment_selected_lines ()
Sébastien Wilmet's avatar
Sébastien Wilmet committed
349
    {
350
351
        TextIter start;
        TextIter end;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
352
353
        get_selection_bounds (out start, out end);

354
355
356
357
        comment_between (start, end);
    }

    // comment the lines between start_iter and end_iter included
Sébastien Wilmet's avatar
Sébastien Wilmet committed
358
359
    public void comment_between (TextIter start_iter, TextIter end_iter,
        bool end_iter_set = true)
360
361
362
363
    {
        int start_line = start_iter.get_line ();
        int end_line = start_line;

Sébastien Wilmet's avatar
Sébastien Wilmet committed
364
        if (end_iter_set)
365
366
367
368
            end_line = end_iter.get_line ();

        TextIter cur_iter;
        get_iter_at_line (out cur_iter, start_line);
Sébastien Wilmet's avatar
Sébastien Wilmet committed
369
370

        begin_user_action ();
371

372
373
        for (int line_num = start_line ; line_num <= end_line ; line_num++)
        {
374
375
376
377
378
379
            if (cur_iter.ends_line ())
                // Don't insert a trailing space.
                insert (ref cur_iter, "%", -1);
            else
                insert (ref cur_iter, "% ", -1);

380
            cur_iter.forward_line ();
Sébastien Wilmet's avatar
Sébastien Wilmet committed
381
        }
382

Sébastien Wilmet's avatar
Sébastien Wilmet committed
383
384
385
386
387
388
389
390
        end_user_action ();
    }

    public void uncomment_selected_lines ()
    {
        TextIter start, end;
        get_selection_bounds (out start, out end);

Sébastien Wilmet's avatar
Sébastien Wilmet committed
391
392
393
        int start_line = start.get_line ();
        int end_line = end.get_line ();
        int line_count = get_line_count ();
Sébastien Wilmet's avatar
Sébastien Wilmet committed
394
395
396

        begin_user_action ();

Sébastien Wilmet's avatar
Sébastien Wilmet committed
397
        for (int i = start_line ; i <= end_line ; i++)
Sébastien Wilmet's avatar
Sébastien Wilmet committed
398
399
400
401
402
403
404
405
406
        {
            get_iter_at_line (out start, i);

            // if last line
            if (i == line_count - 1)
                get_end_iter (out end);
            else
                get_iter_at_line (out end, i + 1);

Sébastien Wilmet's avatar
Sébastien Wilmet committed
407
            string line = get_text (start, end, false);
Sébastien Wilmet's avatar
Sébastien Wilmet committed
408
409

            /* find the first '%' character */
Sébastien Wilmet's avatar
Sébastien Wilmet committed
410
411
412
            int j = 0;
            int start_delete = -1;
            int stop_delete = -1;
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
            while (line[j] != '\0')
            {
                if (line[j] == '%')
                {
                    start_delete = j;
                    stop_delete = j + 1;
                    if (line[j + 1] == ' ')
                        stop_delete++;
                    break;
                }

                else if (line[j] != ' ' && line[j] != '\t')
                    break;

                j++;
            }

            if (start_delete == -1)
                continue;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
432
433
434

            get_iter_at_line_offset (out start, i, start_delete);
            get_iter_at_line_offset (out end, i, stop_delete);
435
            this.delete (ref start, ref end);
Sébastien Wilmet's avatar
Sébastien Wilmet committed
436
437
438
439
        }

        end_user_action ();
    }
Sébastien Wilmet's avatar
Sébastien Wilmet committed
440

441
442
443
444
445
446
447
448
    public Project? get_project ()
    {
        if (project_id == -1)
            return null;

        return Projects.get_default ().get (project_id);
    }

Sébastien Wilmet's avatar
Sébastien Wilmet committed
449
450
451
452
453
    public File? get_main_file ()
    {
        if (location == null)
            return null;

454
        Project? project = get_project ();
455
456
        if (project == null)
            return location;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
457

458
        return project.main_file;
Sébastien Wilmet's avatar
Sébastien Wilmet committed
459
460
    }

461
462
463
464
465
466
467
468
469
470
    public bool is_main_file_a_tex_file ()
    {
        File? main_file = get_main_file ();
        if (main_file == null)
            return false;

        string path = main_file.get_parse_name ();
        return path.has_suffix (".tex");
    }

471
472
473
    public DocumentStructure get_structure ()
    {
        if (_structure == null)
474
        {
475
            _structure = new DocumentStructure (this);
476
477
            _structure.parse ();
        }
478
479
480
        return _structure;
    }

481
482
483
484
485
486
487
488
    public bool set_tmp_location ()
    {
        /* Create a temporary directory (most probably in /tmp/) */
        string template = "latexila-XXXXXX";
        string tmp_dir;

        try
        {
489
            tmp_dir = DirUtils.make_tmp (template);
490
491
492
493
494
495
496
497
498
499
        }
        catch (FileError e)
        {
            warning ("Impossible to create temporary directory: %s", e.message);
            return false;
        }

        /* Set the location as 'tmp.tex' in the temporary directory */
        this.location = File.new_for_path (Path.build_filename (tmp_dir, "tmp.tex"));

500
501
        /* Warn the user that the file can be lost */

502
        Gtef.InfoBar infobar = tab.add_message (
503
504
505
506
            _("The file has a temporary location. The data can be lost after rebooting your computer."),
            _("Do you want to save the file in a safer place?"),
            MessageType.WARNING);

Sébastien Wilmet's avatar
Sébastien Wilmet committed
507
508
        infobar.add_button (_("Save _As"), ResponseType.YES);
        infobar.add_button (_("Cancel"), ResponseType.NO);
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523

        infobar.response.connect ((response_id) =>
        {
            if (response_id == ResponseType.YES)
            {
                unowned MainWindow? main_window =
                    Utils.get_toplevel_window (tab) as MainWindow;

                if (main_window != null)
                    main_window.save_document (this, true);
            }

            infobar.destroy ();
        });

524
525
        return true;
    }
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

    private void save_metadata ()
    {
        return_if_fail (_metadata_info != null);

        if (this.location == null)
            return;

        try
        {
            this.location.set_attributes_from_info (_metadata_info,
                FileQueryInfoFlags.NONE);
        }
        catch (Error error)
        {
            warning ("Set document metadata failed: %s", error.message);
        }
    }

    public void set_metadata (string key, string? val)
    {
        return_if_fail (_metadata_info != null);

        if (val != null)
            _metadata_info.set_attribute_string (key, val);
        else
            // Unset the key
            _metadata_info.set_attribute (key, FileAttributeType.INVALID, null);

        save_metadata ();
    }

    public string? get_metadata (string key)
    {
        return_val_if_fail (_metadata_info != null, null);

        if (_metadata_info.has_attribute (key) &&
            _metadata_info.get_attribute_type (key) == FileAttributeType.STRING)
            return _metadata_info.get_attribute_string (key);

        return null;
    }
Sébastien Wilmet's avatar
Sébastien Wilmet committed
568
}