SVN commit 1146719 by beschow:
[kate:kate1.git] / part / utils / kateautoindent.cpp
1 /* This file is part of the KDE libraries
2    Copyright (C) 2003 Jesse Yurkovich <yurkjes@iit.edu>
3    Copyright (C) 2004 >Anders Lund <anders@alweb.dk> (KateVarIndent class)
4    Copyright (C) 2005 Dominik Haumann <dhdev@gmx.de> (basic support for config page)
5
6    This library is free software; you can redistribute it and/or
7    modify it under the terms of the GNU Library General Public
8    License version 2 as published by the Free Software Foundation.
9
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14
15    You should have received a copy of the GNU Library General Public License
16    along with this library; see the file COPYING.LIB.  If not, write to
17    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18    Boston, MA 02110-1301, USA.
19 */
20
21 #include "kateautoindent.h"
22 #include "kateautoindent.moc"
23
24 #include "kateconfig.h"
25 #include "katehighlight.h"
26 #include "kateglobal.h"
27 #include "kateindentscript.h"
28 #include "katescriptmanager.h"
29 #include "kateview.h"
30 #include "kateextendedattribute.h"
31 #include "katedocument.h"
32
33 #include <klocale.h>
34 #include <kdebug.h>
35 #include <kmenu.h>
36
37 #include <cctype>
38
39 const QString MODE_NONE = QLatin1String("none");
40 const QString MODE_NORMAL = QLatin1String("normal");
41
42 //BEGIN KateAutoIndent
43
44 QStringList KateAutoIndent::listModes ()
45 {
46   QStringList l;
47
48   for (int i = 0; i < modeCount(); ++i)
49     l << modeDescription(i);
50
51   return l;
52 }
53
54 int KateAutoIndent::modeCount ()
55 {
56   // inbuild modes + scripts
57   return 2 + KateGlobal::self()->scriptManager()->indentationScriptCount();
58 }
59
60
61 QString KateAutoIndent::modeName (int mode)
62 {
63   if (mode == 0 || mode >= modeCount ())
64     return MODE_NONE;
65
66   if (mode == 1)
67     return MODE_NORMAL;
68
69   return KateGlobal::self()->scriptManager()->indentationScriptByIndex(mode-2)->indentHeader().baseName();
70 }
71
72 QString KateAutoIndent::modeDescription (int mode)
73 {
74   if (mode == 0 || mode >= modeCount ())
75     return i18nc ("Autoindent mode", "None");
76
77   if (mode == 1)
78     return i18nc ("Autoindent mode", "Normal");
79
80   return KateGlobal::self()->scriptManager()->indentationScriptByIndex(mode-2)->indentHeader().name();
81 }
82
83 QString KateAutoIndent::modeRequiredStyle(int mode)
84 {
85   if (mode == 0 || mode == 1 || mode >= modeCount())
86     return QString();
87
88   return KateGlobal::self()->scriptManager()->indentationScriptByIndex(mode-2)->indentHeader().requiredStyle();
89 }
90
91 uint KateAutoIndent::modeNumber (const QString &name)
92 {
93   for (int i = 0; i < modeCount(); ++i)
94     if (modeName(i) == name)
95       return i;
96
97   return 0;
98 }
99
100 KateAutoIndent::KateAutoIndent (KateDocument *_doc)
101   : QObject(_doc), doc(_doc), m_script (0)
102 {
103   // don't call updateConfig() here, document might is not ready for that....
104
105   // on script reload, the script pointer is invalid -> force reload
106   connect(KateGlobal::self()->scriptManager(), SIGNAL(reloaded()),
107           this, SLOT(reloadScript()));
108 }
109
110 KateAutoIndent::~KateAutoIndent ()
111 {
112 }
113
114 QString KateAutoIndent::tabString (int length, int align) const
115 {
116   QString s;
117   length = qMin (length, 256); // sanity check for large values of pos
118   int spaces = qBound(0, align - length, 256);
119
120   if (!useSpaces)
121   {
122     s.append (QString (length / tabWidth, '\t'));
123     length = length % tabWidth;
124   }
125   s.append(QString(length + spaces, ' '));
126
127   return s;
128 }
129
130 bool KateAutoIndent::doIndent(int line, int indentDepth, int align)
131 {
132   kDebug (13060) << "doIndent: line: " << line << " indentDepth: " << indentDepth << " align: " << align;
133
134   Kate::TextLine textline = doc->plainKateTextLine(line);
135
136   // textline not found, cu
137   if (!textline)
138     return false;
139
140   // sanity check
141   if (indentDepth < 0)
142     indentDepth = 0;
143
144   const QString oldIndentation = textline->leadingWhitespace();
145
146   // Preserve existing "tabs then spaces" alignment if and only if:
147   //  - no alignment was passed to doIndent and
148   //  - we aren't using spaces for indentation and
149   //  - we aren't rounding indentation up to the next multiple of the indentation width and
150   //  - we aren't using a combination to tabs and spaces for alignment, or in other words
151   //    the indent width is a multiple of the tab width.
152   bool preserveAlignment = !useSpaces && keepExtra && indentWidth % tabWidth == 0;
153   if (align == 0 && preserveAlignment)
154   {
155     // Count the number of consecutive spaces at the end of the existing indentation
156     int i = oldIndentation.size() - 1;
157     while (i >= 0 && oldIndentation.at(i) == ' ')
158       --i;
159     // Use the passed indentDepth as the alignment, and set the indentDepth to
160     // that value minus the number of spaces found (but don't let it get negative).
161     align = indentDepth;
162     indentDepth = qMax(0, align - (oldIndentation.size() - 1 - i));
163   }
164
165   QString indentString = tabString(indentDepth, align);
166
167   // remove leading whitespace, then insert the leading indentation
168   doc->editStart ();
169   doc->editRemoveText (line, 0, oldIndentation.length());
170   doc->editInsertText (line, 0, indentString);
171   doc->editEnd ();
172
173   return true;
174 }
175
176 bool KateAutoIndent::doIndentRelative(int line, int change)
177 {
178   kDebug (13060) << "doIndentRelative: line: " << line << " change: " << change;
179
180   Kate::TextLine textline = doc->plainKateTextLine(line);
181
182   // get indent width of current line
183   int indentDepth = textline->indentDepth (tabWidth);
184   int extraSpaces = indentDepth % indentWidth;
185
186   // add change
187   indentDepth += change;
188
189   // if keepExtra is off, snap to a multiple of the indentWidth
190   if (!keepExtra && extraSpaces > 0)
191   {
192     if (change < 0)
193       indentDepth += indentWidth - extraSpaces;
194     else
195       indentDepth -= extraSpaces;
196   }
197
198   // do indent
199   return doIndent(line, indentDepth);
200 }
201
202 void KateAutoIndent::keepIndent ( int line )
203 {
204   // no line in front, no work...
205   if (line <= 0)
206     return;
207
208   Kate::TextLine prevTextLine = doc->plainKateTextLine(line-1);
209   Kate::TextLine textLine     = doc->plainKateTextLine(line);
210
211   // textline not found, cu
212   if (!prevTextLine || !textLine)
213     return;
214
215   const QString previousWhitespace = prevTextLine->leadingWhitespace();
216
217   // remove leading whitespace, then insert the leading indentation
218   doc->editStart ();
219
220   if (!keepExtra)
221   {
222     const QString currentWhitespace = textLine->leadingWhitespace();
223     doc->editRemoveText (line, 0, currentWhitespace.length());
224   }
225
226   doc->editInsertText (line, 0, previousWhitespace);
227   doc->editEnd ();
228 }
229
230 void KateAutoIndent::reloadScript()
231 {
232   // small trick to force reload
233   m_script = 0; // prevent dangling pointer
234   QString currentMode = m_mode;
235   m_mode = QString();
236   setMode(currentMode);
237 }
238
239 void KateAutoIndent::scriptIndent (KateView *view, const KTextEditor::Cursor &position, QChar typedChar)
240 {
241   QPair<int, int> result = m_script->indent (view, position, typedChar, indentWidth);
242   int newIndentInChars = result.first;
243
244   // handle negative values special
245   if (newIndentInChars < -1)
246     return;
247
248   // reuse indentation of the previous line, just like the "normal" indenter
249   if (newIndentInChars == -1)
250   {
251     // keep indent of previous line
252     keepIndent (position.line());
253
254     return;
255   }
256
257   int align = result.second;
258   if (align > 0)
259     kDebug (13060) << "Align: " << align;
260
261   // we got a positive or zero indent to use...
262   doIndent (position.line(), newIndentInChars, align);
263 }
264
265 bool KateAutoIndent::isStyleProvided(const KateIndentScript *script, const KateHighlighting *highlight)
266 {
267   QString requiredStyle = script->indentHeader().requiredStyle();
268   return (requiredStyle.isEmpty() || requiredStyle == highlight->style());
269 }
270
271 void KateAutoIndent::setMode (const QString &name)
272 {
273   // bail out, already set correct mode...
274   if (m_mode == name)
275     return;
276
277   // cleanup
278   m_script = 0;
279
280   // first, catch easy stuff... normal mode and none, easy...
281   if ( name.isEmpty() || name == MODE_NONE )
282   {
283     m_mode = MODE_NONE;
284     return;
285   }
286
287   if ( name == MODE_NORMAL )
288   {
289     m_mode = MODE_NORMAL;
290     return;
291   }
292
293   // handle script indenters, if any for this name...
294   KateIndentScript *script = KateGlobal::self()->scriptManager()->indentationScript(name);
295   if ( script )
296   {
297     if (isStyleProvided(script, doc->highlight()))
298     {
299       m_script = script;
300       m_mode = name;
301
302       kDebug( 13060 ) << "mode: " << name << "accepted";
303       return;
304     }
305     else
306     {
307       kWarning( 13060 ) << "mode" << name << "requires a different highlight style";
308     }
309   }
310   else
311   {
312     kWarning( 13060 ) << "mode" << name << "does not exist";
313   }
314
315   // Fall back to normal
316   m_mode = MODE_NORMAL;
317 }
318
319 void KateAutoIndent::checkRequiredStyle()
320 {
321   if (m_script)
322   {
323     if (!isStyleProvided(m_script, doc->highlight()))
324     {
325       kDebug( 13060 ) << "mode" << m_mode << "requires a different highlight style";
326       doc->config()->setIndentationMode(MODE_NORMAL);
327     }
328   }
329 }
330
331 void KateAutoIndent::updateConfig ()
332 {
333   KateDocumentConfig *config = doc->config();
334
335   useSpaces   = config->replaceTabsDyn();
336   keepExtra   = config->keepExtraSpaces();
337   tabWidth    = config->tabWidth();
338   indentWidth = config->indentationWidth();
339 }
340
341
342 bool KateAutoIndent::changeIndent (const KTextEditor::Range &range, int change)
343 {
344   QList<int> skippedLines;
345
346   // loop over all lines given...
347   for (int line = range.start().line () < 0 ? 0 : range.start().line ();
348        line <= qMin (range.end().line (), doc->lines()-1); ++line)
349   {
350     // don't indent empty lines
351     if (doc->line(line).isEmpty())
352     {
353       skippedLines.append (line);
354       continue;
355     }
356     // don't indent the last line when the cursor is on the first column
357     if (line == range.end().line() && range.end().column() == 0)
358     {
359       skippedLines.append (line);
360       continue;
361     }
362
363     doIndentRelative(line, change * indentWidth);
364   }
365
366   if (skippedLines.count() > range.numberOfLines())
367   {
368     // all lines were empty, so indent them nevertheless
369     foreach (int line, skippedLines)
370       doIndentRelative(line, change * indentWidth);
371   }
372
373   return true;
374 }
375
376 void KateAutoIndent::indent (KateView *view, const KTextEditor::Range &range)
377 {
378   // no script, do nothing...
379   if (!m_script)
380     return;
381
382   doc->pushEditState();
383   doc->editStart();
384   // loop over all lines given...
385   for (int line = range.start().line () < 0 ? 0 : range.start().line ();
386        line <= qMin (range.end().line (), doc->lines()-1); ++line)
387   {
388     // let the script indent for us...
389     scriptIndent (view, KTextEditor::Cursor (line, 0), QChar());
390   }
391   doc->editEnd ();
392   doc->popEditState();
393 }
394
395 void KateAutoIndent::userTypedChar (KateView *view, const KTextEditor::Cursor &position, QChar typedChar)
396 {
397   // normal mode
398   if (m_mode == MODE_NORMAL)
399   {
400     // only indent on new line, per default
401     if (typedChar != '\n')
402       return;
403
404     // keep indent of previous line
405     keepIndent (position.line());
406
407     return;
408   }
409
410   // no script, do nothing...
411   if (!m_script)
412     return;
413
414   // does the script allow this char as trigger?
415   if (typedChar != '\n' && !m_script->triggerCharacters().contains(typedChar))
416     return;
417
418   // let the script indent for us...
419   scriptIndent (view, position, typedChar);
420 }
421 //END KateAutoIndent
422
423 //BEGIN KateViewIndentAction
424 KateViewIndentationAction::KateViewIndentationAction(KateDocument *_doc, const QString& text, QObject *parent)
425        : KActionMenu (text, parent), doc(_doc)
426 {
427   connect(menu(),SIGNAL(aboutToShow()),this,SLOT(slotAboutToShow()));
428   actionGroup = new QActionGroup(menu());
429 }
430
431 void KateViewIndentationAction::slotAboutToShow()
432 {
433   QStringList modes = KateAutoIndent::listModes ();
434
435   menu()->clear ();
436   foreach (QAction *action, actionGroup->actions()) {
437     actionGroup->removeAction(action);
438   }
439   for (int z=0; z<modes.size(); ++z) {
440     QAction *action = menu()->addAction( '&' + KateAutoIndent::modeDescription(z).replace('&', "&&") );
441     actionGroup->addAction(action);
442     action->setCheckable( true );
443     action->setData( z );
444
445     QString requiredStyle = KateAutoIndent::modeRequiredStyle(z);
446     action->setEnabled(requiredStyle.isEmpty() || requiredStyle == doc->highlight()->style());
447
448     if ( doc->config()->indentationMode() == KateAutoIndent::modeName (z) )
449       action->setChecked( true );
450   }
451
452   disconnect( menu(), SIGNAL( triggered( QAction* ) ), this, SLOT( setMode( QAction* ) ) );
453   connect( menu(), SIGNAL( triggered( QAction* ) ), this, SLOT( setMode( QAction* ) ) );
454 }
455
456 void KateViewIndentationAction::setMode (QAction *action)
457 {
458   // set new mode
459   doc->config()->setIndentationMode(KateAutoIndent::modeName (action->data().toInt()));
460 }
461 //END KateViewIndentationAction
462
463 // kate: space-indent on; indent-width 2; replace-tabs on;