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 // ✂ U+2702 BLACK SCISSORS
490 str.push_back(0x2702);
492 case Change::INSERTED:
493 // ✍ U+270D WRITING HAND
494 str.push_back(0x270d);
497 dit.pos() = it->range.start;
498 Paragraph const & par = dit.paragraph();
499 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
500 if (it->range.end > par.size())
501 // ¶ U+00B6 PILCROW SIGN
503 docstring const & author = author_list.get(it->change.author).name();
504 Toc::iterator it = change_list->item(0, author);
505 if (it == change_list->end()) {
506 change_list->push_back(TocItem(dit, 0, author, true));
507 change_list->push_back(TocItem(dit, 1, str, output_active,
508 support::wrapParas(str, 4)));
511 for (++it; it != change_list->end(); ++it) {
512 if (it->depth() == 0 && it->str() != author)
515 change_list->insert(it, TocItem(dit, 1, str, output_active,
516 support::wrapParas(str, 4)));