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 << ")");
142 is_update_required_ = true;
145 Range const newRange(start, end);
147 ChangeTable::iterator it = table_.begin();
149 for (; it != table_.end(); ) {
150 // current change starts like or follows new change
151 if (it->range.start >= start) {
155 // new change intersects with existing change
156 if (it->range.end > start) {
157 pos_type oldEnd = it->range.end;
158 it->range.end = start;
160 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
161 << " resulting in range (" << it->range.start << ", "
162 << it->range.end << ")");
166 LYXERR(Debug::CHANGES, " inserting tail in range ("
167 << end << ", " << oldEnd << ")");
168 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
176 if (change.type != Change::UNCHANGED) {
177 LYXERR(Debug::CHANGES, " inserting change");
178 it = table_.insert(it, ChangeRange(change, Range(start, end)));
182 for (; it != table_.end(); ) {
183 // new change 'contains' existing change
184 if (newRange.contains(it->range)) {
185 LYXERR(Debug::CHANGES, " removing subrange ("
186 << it->range.start << ", " << it->range.end << ")");
187 it = table_.erase(it);
191 // new change precedes existing change
192 if (it->range.start >= end)
195 // new change intersects with existing change
196 it->range.start = end;
197 LYXERR(Debug::CHANGES, " cutting head of type "
198 << it->change.type << " resulting in range ("
199 << end << ", " << it->range.end << ")");
200 break; // no need for another iteration
207 void Changes::erase(pos_type const pos)
209 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
211 ChangeTable::iterator it = table_.begin();
212 ChangeTable::iterator end = table_.end();
214 for (; it != end; ++it) {
215 // range (pos,pos+x) becomes (pos,pos+x-1)
216 if (it->range.start > pos)
218 // range (pos-x,pos) stays (pos-x,pos)
219 if (it->range.end > pos)
227 void Changes::insert(Change const & change, lyx::pos_type pos)
229 if (change.type != Change::UNCHANGED) {
230 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
231 << " at position " << pos);
234 ChangeTable::iterator it = table_.begin();
235 ChangeTable::iterator end = table_.end();
237 for (; it != end; ++it) {
238 // range (pos,pos+x) becomes (pos+1,pos+x+1)
239 if (it->range.start >= pos)
242 // range (pos-x,pos) stays as it is
243 if (it->range.end > pos)
247 set(change, pos, pos + 1); // set will call merge
251 Change const & Changes::lookup(pos_type const pos) const
253 static Change const noChange = Change(Change::UNCHANGED);
255 ChangeTable::const_iterator it = table_.begin();
256 ChangeTable::const_iterator const end = table_.end();
258 for (; it != end; ++it) {
259 if (it->range.contains(pos))
267 bool Changes::isDeleted(pos_type start, pos_type end) const
269 ChangeTable::const_iterator it = table_.begin();
270 ChangeTable::const_iterator const itend = table_.end();
272 for (; it != itend; ++it) {
273 if (it->range.contains(Range(start, end))) {
274 LYXERR(Debug::CHANGES, "range ("
275 << start << ", " << end << ") fully contains ("
276 << it->range.start << ", " << it->range.end
277 << ") of type " << it->change.type);
278 return it->change.type == Change::DELETED;
285 bool Changes::isChanged(pos_type const start, pos_type const end) const
287 ChangeTable::const_iterator it = table_.begin();
288 ChangeTable::const_iterator const itend = table_.end();
290 for (; it != itend; ++it) {
291 if (it->range.intersects(Range(start, end))) {
292 LYXERR(Debug::CHANGES, "found intersection of range ("
293 << start << ", " << end << ") with ("
294 << it->range.start << ", " << it->range.end
295 << ") of type " << it->change.type);
303 bool Changes::isChanged() const
305 ChangeTable::const_iterator it = table_.begin();
306 ChangeTable::const_iterator const itend = table_.end();
307 for (; it != itend; ++it) {
308 if (it->change.changed())
315 void Changes::merge()
318 ChangeTable::iterator it = table_.begin();
320 while (it != table_.end()) {
321 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
322 << " and range (" << it->range.start << ", " << it->range.end
325 if (it->range.start == it->range.end) {
326 LYXERR(Debug::CHANGES, "removing empty range for pos "
336 if (it + 1 == table_.end())
339 if (it->change.isSimilarTo((it + 1)->change)
340 && it->range.end == (it + 1)->range.start) {
341 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
342 << it->range.end << ") and (" << (it + 1)->range.start << ", "
343 << (it + 1)->range.end << ")");
345 (it + 1)->range.start = it->range.start;
346 (it + 1)->change.changetime = max(it->change.changetime,
347 (it + 1)->change.changetime);
357 if (merged && !isChanged())
358 is_update_required_ = true;
364 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
365 docstring const & chgTime,
366 OutputParams const & runparams)
371 docstring uncodable_author;
372 odocstringstream ods;
375 // convert utf8 author name to something representable
376 // in the current encoding
377 pair<docstring, docstring> author_latexed =
378 runparams.encoding->latexString(author, runparams.dryrun);
379 if (!author_latexed.second.empty()) {
380 LYXERR0("Omitting uncodable characters '"
381 << author_latexed.second
382 << "' in change author name!");
383 uncodable_author = author;
385 ods << author_latexed.first << "}{" << chgTime << "}{";
387 // warn user (once) if we found uncodable glyphs.
388 if (!uncodable_author.empty()) {
389 static std::set<docstring> warned_authors;
390 static Mutex warned_mutex;
391 Mutex::Locker locker(&warned_mutex);
392 if (warned_authors.find(uncodable_author) == warned_authors.end()) {
393 frontend::Alert::warning(_("Uncodable character in author name"),
394 support::bformat(_("The author name '%1$s',\n"
395 "used for change tracking, contains the following glyphs that\n"
396 "cannot be represented in the current encoding: %2$s.\n"
397 "These glyphs will be omitted in the exported LaTeX file.\n\n"
398 "Choose an appropriate document encoding (such as utf8)\n"
399 "or change the spelling of the author name."),
400 uncodable_author, author_latexed.second));
401 warned_authors.insert(uncodable_author);
411 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
412 Change const & oldChange, Change const & change,
413 OutputParams const & runparams)
415 if (!bparams.output_changes || oldChange == change)
420 if (oldChange.type != Change::UNCHANGED) {
421 // close \lyxadded or \lyxdeleted
424 if (oldChange.type == Change::DELETED)
425 --runparams.inulemcmd;
429 chgTime += asctime(gmtime(&change.changetime));
430 // remove trailing '\n'
431 chgTime.erase(chgTime.end() - 1);
434 if (change.type == Change::DELETED) {
435 macro_beg = from_ascii("\\lyxdeleted{");
436 ++runparams.inulemcmd;
438 else if (change.type == Change::INSERTED)
439 macro_beg = from_ascii("\\lyxadded{");
441 docstring str = getLaTeXMarkup(macro_beg,
442 bparams.authors().get(change.author).name(),
446 column += str.size();
452 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
453 Change const & old, Change const & change)
460 int const buffer_id = bparams.authors().get(change.author).bufferId();
462 switch (change.type) {
463 case Change::UNCHANGED:
464 os << "\n\\change_unchanged\n";
467 case Change::DELETED:
468 os << "\n\\change_deleted " << buffer_id
469 << " " << change.changetime << "\n";
472 case Change::INSERTED:
473 os << "\n\\change_inserted " << buffer_id
474 << " " << change.changetime << "\n";
480 void Changes::checkAuthors(AuthorList const & authorList)
482 ChangeTable::const_iterator it = table_.begin();
483 ChangeTable::const_iterator endit = table_.end();
484 for ( ; it != endit ; ++it)
485 if (it->change.type != Change::UNCHANGED)
486 authorList.get(it->change.author).setUsed(true);
490 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer,
491 bool output_active) const
496 shared_ptr<Toc> change_list = buffer.tocBackend().toc("change");
497 AuthorList const & author_list = buffer.params().authors();
498 DocIterator dit = cdit;
500 ChangeTable::const_iterator it = table_.begin();
501 ChangeTable::const_iterator const itend = table_.end();
502 for (; it != itend; ++it) {
504 switch (it->change.type) {
505 case Change::UNCHANGED:
507 case Change::DELETED:
508 // ✂ U+2702 BLACK SCISSORS
509 str.push_back(0x2702);
511 case Change::INSERTED:
512 // ✍ U+270D WRITING HAND
513 str.push_back(0x270d);
516 dit.pos() = it->range.start;
517 Paragraph const & par = dit.paragraph();
518 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
519 if (it->range.end > par.size())
520 // ¶ U+00B6 PILCROW SIGN
522 docstring const & author = author_list.get(it->change.author).name();
523 Toc::iterator it = TocBackend::findItem(*change_list, 0, author);
524 if (it == change_list->end()) {
525 change_list->push_back(TocItem(dit, 0, author, true));
526 change_list->push_back(TocItem(dit, 1, str, output_active,
527 support::wrapParas(str, 4)));
530 for (++it; it != change_list->end(); ++it) {
531 if (it->depth() == 0 && it->str() != author)
534 change_list->insert(it, TocItem(dit, 1, str, output_active,
535 support::wrapParas(str, 4)));
540 void Changes::updateBuffer(Buffer const & buf)
542 is_update_required_ = false;
543 if (!buf.areChangesPresent() && isChanged())
544 buf.setChangesPresent(true);