3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
9 * Full author contact details are available in file CREDITS.
11 * Record changes in a paragraph.
19 #include "BufferParams.h"
21 #include "LaTeXFeatures.h"
22 #include "OutputParams.h"
23 #include "Paragraph.h"
24 #include "TocBackend.h"
26 #include "support/debug.h"
27 #include "support/gettext.h"
28 #include "support/lassert.h"
29 #include "support/lstrings.h"
30 #include "support/mutex.h"
32 #include "frontends/alert.h"
41 * Class Change has a changetime field that specifies the exact time at which
42 * a specific change was made. The change time is used as a guidance for the
43 * user while editing his document. Presently, it is not considered for LaTeX
45 * When merging two adjacent changes, the changetime is not considered,
46 * only the equality of the change type and author is checked (in method
47 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
48 * the later change time is preserved.
51 bool Change::isSimilarTo(Change const & change) const
53 if (type != change.type)
56 if (type == Change::UNCHANGED)
59 return author == change.author;
63 Color Change::color() const
65 Color color = Color_none;
68 color = Color_changedtextauthor1;
71 color = Color_changedtextauthor2;
74 color = Color_changedtextauthor3;
77 color = Color_changedtextauthor4;
80 color = Color_changedtextauthor5;
85 color.mergeColor = Color_deletedtextmodifier;
91 bool operator==(Change const & l, Change const & r)
96 // two changes of type UNCHANGED are always equal
97 if (l.type == Change::UNCHANGED)
100 return l.author == r.author && l.changetime == r.changetime;
104 bool operator!=(Change const & l, Change const & r)
110 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
112 return r1.start == r2.start && r1.end == r2.end;
116 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
122 bool Changes::Range::intersects(Range const & r) const
124 return r.start < end && r.end > start; // end itself is not in the range!
128 void Changes::set(Change const & change, pos_type const pos)
130 set(change, pos, pos + 1);
134 void Changes::set(Change const & change, pos_type const start, pos_type const end)
136 if (change.type != Change::UNCHANGED) {
137 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
138 << ", author: " << change.author
139 << ", time: " << long(change.changetime)
140 << ") in range (" << start << ", " << end << ")");
143 Range const newRange(start, end);
145 ChangeTable::iterator it = table_.begin();
147 for (; it != table_.end(); ) {
148 // current change starts like or follows new change
149 if (it->range.start >= start) {
153 // new change intersects with existing change
154 if (it->range.end > start) {
155 pos_type oldEnd = it->range.end;
156 it->range.end = start;
158 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
159 << " resulting in range (" << it->range.start << ", "
160 << it->range.end << ")");
164 LYXERR(Debug::CHANGES, " inserting tail in range ("
165 << end << ", " << oldEnd << ")");
166 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
174 if (change.type != Change::UNCHANGED) {
175 LYXERR(Debug::CHANGES, " inserting change");
176 it = table_.insert(it, ChangeRange(change, Range(start, end)));
180 for (; it != table_.end(); ) {
181 // new change 'contains' existing change
182 if (newRange.contains(it->range)) {
183 LYXERR(Debug::CHANGES, " removing subrange ("
184 << it->range.start << ", " << it->range.end << ")");
185 it = table_.erase(it);
189 // new change precedes existing change
190 if (it->range.start >= end)
193 // new change intersects with existing change
194 it->range.start = end;
195 LYXERR(Debug::CHANGES, " cutting head of type "
196 << it->change.type << " resulting in range ("
197 << end << ", " << it->range.end << ")");
198 break; // no need for another iteration
205 void Changes::erase(pos_type const pos)
207 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
209 ChangeTable::iterator it = table_.begin();
210 ChangeTable::iterator end = table_.end();
212 for (; it != end; ++it) {
213 // range (pos,pos+x) becomes (pos,pos+x-1)
214 if (it->range.start > pos)
216 // range (pos-x,pos) stays (pos-x,pos)
217 if (it->range.end > pos)
225 void Changes::insert(Change const & change, lyx::pos_type pos)
227 if (change.type != Change::UNCHANGED) {
228 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
229 << " at position " << pos);
232 ChangeTable::iterator it = table_.begin();
233 ChangeTable::iterator end = table_.end();
235 for (; it != end; ++it) {
236 // range (pos,pos+x) becomes (pos+1,pos+x+1)
237 if (it->range.start >= pos)
240 // range (pos-x,pos) stays as it is
241 if (it->range.end > pos)
245 set(change, pos, pos + 1); // set will call merge
249 Change const & Changes::lookup(pos_type const pos) const
251 static Change const noChange = Change(Change::UNCHANGED);
253 ChangeTable::const_iterator it = table_.begin();
254 ChangeTable::const_iterator const end = table_.end();
256 for (; it != end; ++it) {
257 if (it->range.contains(pos))
265 bool Changes::isDeleted(pos_type start, pos_type end) const
267 ChangeTable::const_iterator it = table_.begin();
268 ChangeTable::const_iterator const itend = table_.end();
270 for (; it != itend; ++it) {
271 if (it->range.contains(Range(start, end))) {
272 LYXERR(Debug::CHANGES, "range ("
273 << start << ", " << end << ") fully contains ("
274 << it->range.start << ", " << it->range.end
275 << ") of type " << it->change.type);
276 return it->change.type == Change::DELETED;
283 bool Changes::isChanged(pos_type const start, pos_type const end) const
285 ChangeTable::const_iterator it = table_.begin();
286 ChangeTable::const_iterator const itend = table_.end();
288 for (; it != itend; ++it) {
289 if (it->range.intersects(Range(start, end))) {
290 LYXERR(Debug::CHANGES, "found intersection of range ("
291 << start << ", " << end << ") with ("
292 << it->range.start << ", " << it->range.end
293 << ") of type " << it->change.type);
301 bool Changes::isChanged() const
303 ChangeTable::const_iterator it = table_.begin();
304 ChangeTable::const_iterator const itend = table_.end();
305 for (; it != itend; ++it) {
306 if (it->change.changed())
313 void Changes::merge()
316 ChangeTable::iterator it = table_.begin();
318 while (it != table_.end()) {
319 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
320 << " and range (" << it->range.start << ", " << it->range.end
323 if (it->range.start == it->range.end) {
324 LYXERR(Debug::CHANGES, "removing empty range for pos "
334 if (it + 1 == table_.end())
337 if (it->change.isSimilarTo((it + 1)->change)
338 && it->range.end == (it + 1)->range.start) {
339 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
340 << it->range.end << ") and (" << (it + 1)->range.start << ", "
341 << (it + 1)->range.end << ")");
343 (it + 1)->range.start = it->range.start;
344 (it + 1)->change.changetime = max(it->change.changetime,
345 (it + 1)->change.changetime);
355 if (merged && !isChanged())
356 is_update_required_ = true;
362 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
363 docstring const & chgTime,
364 OutputParams const & runparams)
369 docstring uncodable_author;
370 odocstringstream ods;
373 // convert utf8 author name to something representable
374 // in the current encoding
375 pair<docstring, docstring> author_latexed =
376 runparams.encoding->latexString(author, runparams.dryrun);
377 if (!author_latexed.second.empty()) {
378 LYXERR0("Omitting uncodable characters '"
379 << author_latexed.second
380 << "' in change author name!");
381 uncodable_author = author;
383 ods << author_latexed.first << "}{" << chgTime << "}{";
385 // warn user (once) if we found uncodable glyphs.
386 if (!uncodable_author.empty()) {
387 static std::set<docstring> warned_authors;
388 static Mutex warned_mutex;
389 Mutex::Locker locker(&warned_mutex);
390 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
391 frontend::Alert::warning(_("Uncodable character in author name"),
392 support::bformat(_("The author name '%1$s',\n"
393 "used for change tracking, contains the following glyphs that\n"
394 "cannot be represented in the current encoding: %2$s.\n"
395 "These glyphs will be omitted in the exported LaTeX file.\n\n"
396 "Choose an appropriate document encoding (such as utf8)\n"
397 "or change the spelling of the author name."),
398 uncodable_author, author_latexed.second));
399 warned_authors.insert(uncodable_author);
409 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
410 Change const & oldChange, Change const & change,
411 OutputParams const & runparams)
413 if (!bparams.output_changes || oldChange == change)
418 if (oldChange.type != Change::UNCHANGED) {
419 // close \lyxadded or \lyxdeleted
422 if (oldChange.type == Change::DELETED)
423 --runparams.inulemcmd;
427 chgTime += asctime(gmtime(&change.changetime));
428 // remove trailing '\n'
429 chgTime.erase(chgTime.end() - 1);
432 if (change.type == Change::DELETED) {
433 macro_beg = from_ascii("\\lyxdeleted{");
434 ++runparams.inulemcmd;
436 else if (change.type == Change::INSERTED)
437 macro_beg = from_ascii("\\lyxadded{");
439 docstring str = getLaTeXMarkup(macro_beg,
440 bparams.authors().get(change.author).name(),
444 column += str.size();
450 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
451 Change const & old, Change const & change)
458 int const buffer_id = bparams.authors().get(change.author).bufferId();
460 switch (change.type) {
461 case Change::UNCHANGED:
462 os << "\n\\change_unchanged\n";
465 case Change::DELETED:
466 os << "\n\\change_deleted " << buffer_id
467 << " " << change.changetime << "\n";
470 case Change::INSERTED:
471 os << "\n\\change_inserted " << buffer_id
472 << " " << change.changetime << "\n";
478 void Changes::checkAuthors(AuthorList const & authorList)
480 ChangeTable::const_iterator it = table_.begin();
481 ChangeTable::const_iterator endit = table_.end();
482 for ( ; it != endit ; ++it)
483 if (it->change.type != Change::UNCHANGED)
484 authorList.get(it->change.author).setUsed(true);
488 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
489 bool output_active) const
494 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
495 AuthorList const & author_list = buffer.params().authors();
496 DocIterator dit = cdit;
498 ChangeTable::const_iterator it = table_.begin();
499 ChangeTable::const_iterator const itend = table_.end();
500 for (; it != itend; ++it) {
502 switch (it->change.type) {
503 case Change::UNCHANGED:
505 case Change::DELETED:
506 // ✂ U+2702 BLACK SCISSORS
507 str.push_back(0x2702);
509 case Change::INSERTED:
510 // ✍ U+270D WRITING HAND
511 str.push_back(0x270d);
514 dit.pos() = it->range.start;
515 Paragraph const & par = dit.paragraph();
516 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
517 if (it->range.end > par.size())
518 // ¶ U+00B6 PILCROW SIGN
520 docstring const & author = author_list.get(it->change.author).name();
521 Toc::iterator it = change_list->item(0, author);
522 if (it == change_list->end()) {
523 change_list->push_back(TocItem(dit, 0, author, true));
524 change_list->push_back(TocItem(dit, 1, str, output_active,
525 support::wrapParas(str, 4)));
528 for (++it; it != change_list->end(); ++it) {
529 if (it->depth() == 0 && it->str() != author)
532 change_list->insert(it, TocItem(dit, 1, str, output_active,
533 support::wrapParas(str, 4)));
538 void Changes::updateBuffer(Buffer const & buf)
540 is_update_required_ = false;
541 if (!buf.areChangesPresent() && isChanged())
542 buf.setChangesPresent(true);