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 void Changes::merge()
303 ChangeTable::iterator it = table_.begin();
305 while (it != table_.end()) {
306 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
307 << " and range (" << it->range.start << ", " << it->range.end
310 if (it->range.start == it->range.end) {
311 LYXERR(Debug::CHANGES, "removing empty range for pos "
320 if (it + 1 == table_.end())
323 if (it->change.isSimilarTo((it + 1)->change)
324 && it->range.end == (it + 1)->range.start) {
325 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
326 << it->range.end << ") and (" << (it + 1)->range.start << ", "
327 << (it + 1)->range.end << ")");
329 (it + 1)->range.start = it->range.start;
330 (it + 1)->change.changetime = max(it->change.changetime,
331 (it + 1)->change.changetime);
345 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
346 docstring const & chgTime,
347 OutputParams const & runparams)
352 docstring uncodable_author;
353 odocstringstream ods;
356 // convert utf8 author name to something representable
357 // in the current encoding
358 pair<docstring, docstring> author_latexed =
359 runparams.encoding->latexString(author, runparams.dryrun);
360 if (!author_latexed.second.empty()) {
361 LYXERR0("Omitting uncodable characters '"
362 << author_latexed.second
363 << "' in change author name!");
364 uncodable_author = author;
366 ods << author_latexed.first << "}{" << chgTime << "}{";
368 // warn user (once) if we found uncodable glyphs.
369 if (!uncodable_author.empty()) {
370 static std::set<docstring> warned_authors;
371 static Mutex warned_mutex;
372 Mutex::Locker locker(&warned_mutex);
373 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
374 frontend::Alert::warning(_("Uncodable character in author name"),
375 support::bformat(_("The author name '%1$s',\n"
376 "used for change tracking, contains the following glyphs that\n"
377 "cannot be represented in the current encoding: %2$s.\n"
378 "These glyphs will be omitted in the exported LaTeX file.\n\n"
379 "Choose an appropriate document encoding (such as utf8)\n"
380 "or change the spelling of the author name."),
381 uncodable_author, author_latexed.second));
382 warned_authors.insert(uncodable_author);
392 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
393 Change const & oldChange, Change const & change,
394 OutputParams const & runparams)
396 if (!bparams.output_changes || oldChange == change)
401 if (oldChange.type != Change::UNCHANGED) {
402 // close \lyxadded or \lyxdeleted
405 if (oldChange.type == Change::DELETED)
406 --runparams.inulemcmd;
410 chgTime += asctime(gmtime(&change.changetime));
411 // remove trailing '\n'
412 chgTime.erase(chgTime.end() - 1);
415 if (change.type == Change::DELETED) {
416 macro_beg = from_ascii("\\lyxdeleted{");
417 ++runparams.inulemcmd;
419 else if (change.type == Change::INSERTED)
420 macro_beg = from_ascii("\\lyxadded{");
422 docstring str = getLaTeXMarkup(macro_beg,
423 bparams.authors().get(change.author).name(),
427 column += str.size();
433 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
434 Change const & old, Change const & change)
441 int const buffer_id = bparams.authors().get(change.author).bufferId();
443 switch (change.type) {
444 case Change::UNCHANGED:
445 os << "\n\\change_unchanged\n";
448 case Change::DELETED:
449 os << "\n\\change_deleted " << buffer_id
450 << " " << change.changetime << "\n";
453 case Change::INSERTED:
454 os << "\n\\change_inserted " << buffer_id
455 << " " << change.changetime << "\n";
461 void Changes::checkAuthors(AuthorList const & authorList)
463 ChangeTable::const_iterator it = table_.begin();
464 ChangeTable::const_iterator endit = table_.end();
465 for ( ; it != endit ; ++it)
466 if (it->change.type != Change::UNCHANGED)
467 authorList.get(it->change.author).setUsed(true);
471 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
472 bool output_active) const
477 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
478 AuthorList const & author_list = buffer.params().authors();
479 DocIterator dit = cdit;
481 ChangeTable::const_iterator it = table_.begin();
482 ChangeTable::const_iterator const itend = table_.end();
483 for (; it != itend; ++it) {
485 switch (it->change.type) {
486 case Change::UNCHANGED:
488 case Change::DELETED:
489 // 0x2702 is a scissors symbol in the Dingbats unicode group.
490 str.push_back(0x2702);
492 case Change::INSERTED:
493 // 0x270d is the hand writting symbol in the Dingbats unicode group.
494 str.push_back(0x270d); break;
496 dit.pos() = it->range.start;
497 Paragraph const & par = dit.paragraph();
498 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
499 if (it->range.end > par.size())
500 // the end of paragraph symbol from the Punctuation group
501 str.push_back(0x204B);
502 docstring const & author = author_list.get(it->change.author).name();
503 Toc::iterator it = change_list->item(0, author);
504 if (it == change_list->end()) {
505 change_list->push_back(TocItem(dit, 0, author, output_active));
506 change_list->push_back(TocItem(dit, 1, str, output_active,
507 support::wrapParas(str, 4)));
510 for (++it; it != change_list->end(); ++it) {
511 if (it->depth() == 0 && it->str() != author)
514 change_list->insert(it, TocItem(dit, 1, str, output_active,
515 support::wrapParas(str, 4)));