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"
31 #include "frontends/alert.h"
40 * Class Change has a changetime field that specifies the exact time at which
41 * a specific change was made. The change time is used as a guidance for the
42 * user while editing his document. Presently, it is not considered for LaTeX
44 * When merging two adjacent changes, the changetime is not considered,
45 * only the equality of the change type and author is checked (in method
46 * isSimilarTo(...)). If two changes are in fact merged (in method merge()),
47 * the later change time is preserved.
50 bool Change::isSimilarTo(Change const & change) const
52 if (type != change.type)
55 if (type == Change::UNCHANGED)
58 return author == change.author;
62 Color Change::color() const
64 Color color = Color_none;
67 color = Color_changedtextauthor1;
70 color = Color_changedtextauthor2;
73 color = Color_changedtextauthor3;
76 color = Color_changedtextauthor4;
79 color = Color_changedtextauthor5;
84 color.mergeColor = Color_deletedtextmodifier;
90 bool operator==(Change const & l, Change const & r)
95 // two changes of type UNCHANGED are always equal
96 if (l.type == Change::UNCHANGED)
99 return l.author == r.author && l.changetime == r.changetime;
103 bool operator!=(Change const & l, Change const & r)
109 bool operator==(Changes::Range const & r1, Changes::Range const & r2)
111 return r1.start == r2.start && r1.end == r2.end;
115 bool operator!=(Changes::Range const & r1, Changes::Range const & r2)
121 bool Changes::Range::intersects(Range const & r) const
123 return r.start < end && r.end > start; // end itself is not in the range!
127 void Changes::set(Change const & change, pos_type const pos)
129 set(change, pos, pos + 1);
133 void Changes::set(Change const & change, pos_type const start, pos_type const end)
135 if (change.type != Change::UNCHANGED) {
136 LYXERR(Debug::CHANGES, "setting change (type: " << change.type
137 << ", author: " << change.author
138 << ", time: " << long(change.changetime)
139 << ") in range (" << start << ", " << end << ")");
142 Range const newRange(start, end);
144 ChangeTable::iterator it = table_.begin();
146 for (; it != table_.end(); ) {
147 // current change starts like or follows new change
148 if (it->range.start >= start) {
152 // new change intersects with existing change
153 if (it->range.end > start) {
154 pos_type oldEnd = it->range.end;
155 it->range.end = start;
157 LYXERR(Debug::CHANGES, " cutting tail of type " << it->change.type
158 << " resulting in range (" << it->range.start << ", "
159 << it->range.end << ")");
163 LYXERR(Debug::CHANGES, " inserting tail in range ("
164 << end << ", " << oldEnd << ")");
165 it = table_.insert(it, ChangeRange((it-1)->change, Range(end, oldEnd)));
173 if (change.type != Change::UNCHANGED) {
174 LYXERR(Debug::CHANGES, " inserting change");
175 it = table_.insert(it, ChangeRange(change, Range(start, end)));
179 for (; it != table_.end(); ) {
180 // new change 'contains' existing change
181 if (newRange.contains(it->range)) {
182 LYXERR(Debug::CHANGES, " removing subrange ("
183 << it->range.start << ", " << it->range.end << ")");
184 it = table_.erase(it);
188 // new change precedes existing change
189 if (it->range.start >= end)
192 // new change intersects with existing change
193 it->range.start = end;
194 LYXERR(Debug::CHANGES, " cutting head of type "
195 << it->change.type << " resulting in range ("
196 << end << ", " << it->range.end << ")");
197 break; // no need for another iteration
204 void Changes::erase(pos_type const pos)
206 LYXERR(Debug::CHANGES, "Erasing change at position " << pos);
208 ChangeTable::iterator it = table_.begin();
209 ChangeTable::iterator end = table_.end();
211 for (; it != end; ++it) {
212 // range (pos,pos+x) becomes (pos,pos+x-1)
213 if (it->range.start > pos)
215 // range (pos-x,pos) stays (pos-x,pos)
216 if (it->range.end > pos)
224 void Changes::insert(Change const & change, lyx::pos_type pos)
226 if (change.type != Change::UNCHANGED) {
227 LYXERR(Debug::CHANGES, "Inserting change of type " << change.type
228 << " at position " << pos);
231 ChangeTable::iterator it = table_.begin();
232 ChangeTable::iterator end = table_.end();
234 for (; it != end; ++it) {
235 // range (pos,pos+x) becomes (pos+1,pos+x+1)
236 if (it->range.start >= pos)
239 // range (pos-x,pos) stays as it is
240 if (it->range.end > pos)
244 set(change, pos, pos + 1); // set will call merge
248 Change const & Changes::lookup(pos_type const pos) const
250 static Change const noChange = Change(Change::UNCHANGED);
252 ChangeTable::const_iterator it = table_.begin();
253 ChangeTable::const_iterator const end = table_.end();
255 for (; it != end; ++it) {
256 if (it->range.contains(pos))
264 bool Changes::isDeleted(pos_type start, pos_type end) const
266 ChangeTable::const_iterator it = table_.begin();
267 ChangeTable::const_iterator const itend = table_.end();
269 for (; it != itend; ++it) {
270 if (it->range.contains(Range(start, end))) {
271 LYXERR(Debug::CHANGES, "range ("
272 << start << ", " << end << ") fully contains ("
273 << it->range.start << ", " << it->range.end
274 << ") of type " << it->change.type);
275 return it->change.type == Change::DELETED;
282 bool Changes::isChanged(pos_type const start, pos_type const end) const
284 ChangeTable::const_iterator it = table_.begin();
285 ChangeTable::const_iterator const itend = table_.end();
287 for (; it != itend; ++it) {
288 if (it->range.intersects(Range(start, end))) {
289 LYXERR(Debug::CHANGES, "found intersection of range ("
290 << start << ", " << end << ") with ("
291 << it->range.start << ", " << it->range.end
292 << ") of type " << it->change.type);
300 void Changes::merge()
302 ChangeTable::iterator it = table_.begin();
304 while (it != table_.end()) {
305 LYXERR(Debug::CHANGES, "found change of type " << it->change.type
306 << " and range (" << it->range.start << ", " << it->range.end
309 if (it->range.start == it->range.end) {
310 LYXERR(Debug::CHANGES, "removing empty range for pos "
319 if (it + 1 == table_.end())
322 if (it->change.isSimilarTo((it + 1)->change)
323 && it->range.end == (it + 1)->range.start) {
324 LYXERR(Debug::CHANGES, "merging ranges (" << it->range.start << ", "
325 << it->range.end << ") and (" << (it + 1)->range.start << ", "
326 << (it + 1)->range.end << ")");
328 (it + 1)->range.start = it->range.start;
329 (it + 1)->change.changetime = max(it->change.changetime,
330 (it + 1)->change.changetime);
343 docstring getLaTeXMarkup(docstring const & macro, docstring const & author,
344 docstring const & chgTime,
345 OutputParams const & runparams)
350 static docstring warned_author = docstring();
351 docstring uncodable_author = warned_author;
352 odocstringstream ods;
355 // convert utf8 author name to something representable
356 // in the current encoding
357 pair<docstring, docstring> author_latexed =
358 runparams.encoding->latexString(author, runparams.dryrun);
359 if (!author_latexed.second.empty()) {
360 LYXERR0("Omitting uncodable characters '"
361 << author_latexed.second
362 << "' in change author name!");
363 uncodable_author = author;
365 ods << author_latexed.first << "}{" << chgTime << "}{";
367 // warn user (once) if we found uncodable glyphs.
368 if (uncodable_author != warned_author) {
369 frontend::Alert::warning(_("Uncodable character in author name"),
370 support::bformat(_("The author name '%1$s',\n"
371 "used for change tracking, contains the following glyphs that\n"
372 "cannot be represented in the current encoding: %2$s.\n"
373 "These glyphs will be omitted in the exported LaTeX file.\n\n"
374 "Choose an appropriate document encoding (such as utf8)\n"
375 "or change the spelling of the author name."),
376 uncodable_author, author_latexed.second));
377 warned_author = uncodable_author;
385 int Changes::latexMarkChange(otexstream & os, BufferParams const & bparams,
386 Change const & oldChange, Change const & change,
387 OutputParams const & runparams)
389 if (!bparams.outputChanges || oldChange == change)
394 if (oldChange.type != Change::UNCHANGED) {
395 // close \lyxadded or \lyxdeleted
401 chgTime += ctime(&change.changetime);
402 // remove trailing '\n'
403 chgTime.erase(chgTime.end() - 1);
406 if (change.type == Change::DELETED)
407 macro_beg = from_ascii("\\lyxdeleted{");
408 else if (change.type == Change::INSERTED)
409 macro_beg = from_ascii("\\lyxadded{");
411 docstring str = getLaTeXMarkup(macro_beg,
412 bparams.authors().get(change.author).name(),
416 column += str.size();
422 void Changes::lyxMarkChange(ostream & os, BufferParams const & bparams, int & column,
423 Change const & old, Change const & change)
430 int const buffer_id = bparams.authors().get(change.author).bufferId();
432 switch (change.type) {
433 case Change::UNCHANGED:
434 os << "\n\\change_unchanged\n";
437 case Change::DELETED:
438 os << "\n\\change_deleted " << buffer_id
439 << " " << change.changetime << "\n";
442 case Change::INSERTED:
443 os << "\n\\change_inserted " << buffer_id
444 << " " << change.changetime << "\n";
450 void Changes::checkAuthors(AuthorList const & authorList)
452 ChangeTable::const_iterator it = table_.begin();
453 ChangeTable::const_iterator endit = table_.end();
454 for ( ; it != endit ; ++it)
455 if (it->change.type != Change::UNCHANGED)
456 authorList.get(it->change.author).setUsed(true);
460 void Changes::addToToc(DocIterator const & cdit, Buffer const & buffer) const
465 Toc & change_list = buffer.tocBackend().toc("change");
466 AuthorList const & author_list = buffer.params().authors();
467 DocIterator dit = cdit;
469 ChangeTable::const_iterator it = table_.begin();
470 ChangeTable::const_iterator const itend = table_.end();
471 for (; it != itend; ++it) {
473 switch (it->change.type) {
474 case Change::UNCHANGED:
476 case Change::DELETED:
477 // 0x2702 is a scissors symbol in the Dingbats unicode group.
478 str.push_back(0x2702);
480 case Change::INSERTED:
481 // 0x270d is the hand writting symbol in the Dingbats unicode group.
482 str.push_back(0x270d);
485 dit.pos() = it->range.start;
486 Paragraph const & par = dit.paragraph();
487 str += " " + par.asString(it->range.start, min(par.size(), it->range.end));
488 if (it->range.end > par.size())
489 // the end of paragraph symbol from the Punctuation group
490 str.push_back(0x204B);
491 docstring const & author = author_list.get(it->change.author).name();
492 Toc::iterator it = change_list.item(0, author);
493 if (it == change_list.end()) {
494 change_list.push_back(TocItem(dit, 0, author));
495 change_list.push_back(TocItem(dit, 1, str,
496 support::wrapParas(str, 4)));
499 for (++it; it != change_list.end(); ++it) {
500 if (it->depth() == 0 && it->str() != author)
503 change_list.insert(it, TocItem(dit, 1, str,
504 support::wrapParas(str, 4)));