Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 26b8ba5fcdbad34cfcdf49c3422df37a74205d2b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Sergey Prigogin (Google)
 *     Anton Leherbauer (Wind River Systems)
 *******************************************************************************/
package org.eclipse.cdt.internal.ui.actions;

import java.util.ResourceBundle;

import org.eclipse.cdt.core.CCorePlugin;
import org.eclipse.cdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.cdt.internal.corext.util.CodeFormatterUtil;
import org.eclipse.cdt.internal.ui.editor.CEditor;
import org.eclipse.cdt.internal.ui.editor.IndentUtil;
import org.eclipse.cdt.internal.ui.text.CHeuristicScanner;
import org.eclipse.cdt.internal.ui.text.CIndenter;
import org.eclipse.cdt.internal.ui.util.EditorUtility;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.IRewriteTarget;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;
import org.eclipse.ui.texteditor.ITextEditor;
import org.eclipse.ui.texteditor.ITextEditorExtension3;
import org.eclipse.ui.texteditor.TextEditorAction;

/**
 * Indents a line or range of lines in a C document to its correct position. No complete
 * AST must be present, the indentation is computed using heuristics. The algorithm used is fast for
 * single lines, but does not store any information and therefore not so efficient for large line
 * ranges.
 *
 * @see org.eclipse.cdt.internal.ui.text.CHeuristicScanner
 * @see org.eclipse.cdt.internal.ui.text.CIndenter
 */
public class IndentAction extends TextEditorAction {
	/** The caret offset after an indent operation. */
	private int fCaretOffset;

	/**
	 * Whether this is the action invoked by TAB. When <code>true</code>, indentation behaves
	 * differently to accommodate normal TAB operation.
	 */
	private final boolean fIsTabAction;

	/**
	 * Creates a new instance.
	 *
	 * @param bundle the resource bundle
	 * @param prefix the prefix to use for keys in <code>bundle</code>
	 * @param editor the text editor
	 * @param isTabAction whether the action should insert tabs if over the indentation
	 */
	public IndentAction(ResourceBundle bundle, String prefix, ITextEditor editor, boolean isTabAction) {
		super(bundle, prefix, editor);
		fIsTabAction = isTabAction;
	}

	@Override
	public void run() {
		// Update has been called by the framework
		if (!isEnabled() || !validateEditorInputState())
			return;

		ITextSelection selection = getSelection();
		final IDocument document = getDocument();

		if (document != null) {
			final int offset = selection.getOffset();
			final int length = selection.getLength();
			final Position end = new Position(offset + length);
			final int firstLine, nLines;
			fCaretOffset = -1;

			try {
				firstLine = document.getLineOfOffset(offset);
				// check for marginal (zero-length) lines
				int minusOne = length == 0 ? 0 : 1;
				nLines = document.getLineOfOffset(offset + length - minusOne) - firstLine + 1;
				document.addPosition(end);
			} catch (BadLocationException e) {
				// will only happen on concurrent modification
				CUIPlugin.log(new Status(IStatus.ERROR, CUIPlugin.getPluginId(), IStatus.OK, "", e)); //$NON-NLS-1$
				return;
			}

			Runnable runnable = new Runnable() {
				@Override
				public void run() {
					IRewriteTarget target = getTextEditor().getAdapter(IRewriteTarget.class);
					if (target != null)
						target.beginCompoundChange();

					try {
						CHeuristicScanner scanner = new CHeuristicScanner(document);
						CIndenter indenter = new CIndenter(document, scanner, getCProject());
						final boolean multiLine = nLines > 1;
						boolean hasChanged = false;
						for (int i = 0; i < nLines; i++) {
							hasChanged |= indentLine(document, firstLine + i, offset, indenter, scanner, multiLine);
						}

						// update caret position: move to new position when indenting just one line
						// keep selection when indenting multiple
						int newOffset, newLength;
						if (!fIsTabAction && multiLine) {
							newOffset = offset;
							newLength = end.getOffset() - offset;
						} else {
							newOffset = fCaretOffset;
							newLength = 0;
						}

						// always reset the selection if anything was replaced
						// but not when we had a single line non-tab invocation
						if (newOffset != -1 && (hasChanged || newOffset != offset || newLength != length))
							selectAndReveal(newOffset, newLength);

					} catch (BadLocationException e) {
						// will only happen on concurrent modification
						CUIPlugin.log(new Status(IStatus.ERROR, CUIPlugin.getPluginId(), IStatus.OK,
								"ConcurrentModification in IndentAction", e)); //$NON-NLS-1$
					} finally {
						document.removePosition(end);
						if (target != null)
							target.endCompoundChange();
					}
				}
			};

			if (nLines > 50) {
				Display display = getTextEditor().getEditorSite().getWorkbenchWindow().getShell().getDisplay();
				BusyIndicator.showWhile(display, runnable);
			} else {
				runnable.run();
			}
		}
	}

	/**
	 * Selects the given range on the editor.
	 *
	 * @param newOffset the selection offset
	 * @param newLength the selection range
	 */
	private void selectAndReveal(int newOffset, int newLength) {
		Assert.isTrue(newOffset >= 0);
		Assert.isTrue(newLength >= 0);
		ITextEditor editor = getTextEditor();
		if (editor instanceof CEditor) {
			ISourceViewer viewer = ((CEditor) editor).getViewer();
			if (viewer != null)
				viewer.setSelectedRange(newOffset, newLength);
		} else {
			// this is too intrusive, but will never get called anyway
			getTextEditor().selectAndReveal(newOffset, newLength);
		}
	}

	/**
	 * Indents a single line using the heuristic scanner. Multiline comments are
	 * indented as specified by the <code>CCommentAutoIndentStrategy</code>.
	 *
	 * @param document the document
	 * @param line the line to be indented
	 * @param caret the caret position
	 * @param indenter the indenter
	 * @param scanner the heuristic scanner
	 * @param multiLine <code>true</code> if more than one line is being indented
	 * @return <code>true</code> if <code>document</code> was modified, <code>false</code> otherwise
	 * @throws BadLocationException if the document got changed concurrently
	 */
	private boolean indentLine(IDocument document, int line, int caret, CIndenter indenter, CHeuristicScanner scanner,
			boolean multiLine) throws BadLocationException {
		IRegion currentLine = document.getLineInformation(line);
		int offset = currentLine.getOffset();
		int wsStart = offset; // where we start searching for non-WS; after the "//" in single line comments

		String indent = null;
		if (offset < document.getLength()) {
			ITypedRegion partition = TextUtilities.getPartition(document, ICPartitions.C_PARTITIONING, offset, true);
			ITypedRegion startingPartition = TextUtilities.getPartition(document, ICPartitions.C_PARTITIONING, offset,
					false);
			String type = partition.getType();
			if (type.equals(ICPartitions.C_MULTI_LINE_COMMENT) || type.equals(ICPartitions.C_MULTI_LINE_DOC_COMMENT)) {
				indent = computeCommentIndent(document, line, scanner, startingPartition);
			} else if (startingPartition.getType().equals(ICPartitions.C_PREPROCESSOR)) {
				indent = computePreprocessorIndent(document, line, startingPartition);
			} else if (startingPartition.getType().equals(ICPartitions.C_STRING)
					&& offset > startingPartition.getOffset()) {
				// Don't indent inside (raw-)string, but if the indent action was triggered by a Tab key,
				// insert a '\t' character or spaces at the caret position.
				if (!fIsTabAction)
					return false;

				String text = "\t"; //$NON-NLS-1$
				if (useSpaces()) {
					int tabSize = getTabSize();
					if (tabSize == 0)
						return false;

					int numSpaces = tabSize - (caret - offset) % tabSize;
					StringBuilder buf = new StringBuilder(numSpaces);
					for (int i = 0; i < numSpaces; i++) {
						buf.append(' ');
					}
					text = buf.toString();
				}
				document.replace(caret, 0, text);
				fCaretOffset = caret + text.length();
				return true;
			} else if (!fIsTabAction && startingPartition.getOffset() == offset
					&& startingPartition.getType().equals(ICPartitions.C_SINGLE_LINE_COMMENT)) {
				// line comment starting at position 0 -> indent inside
				if (indentInsideLineComments()) {
					int max = document.getLength() - offset;
					int slashes = 2;
					while (slashes < max - 1 && document.get(offset + slashes, 2).equals("//")) //$NON-NLS-1$
						slashes += 2;

					wsStart = offset + slashes;

					StringBuilder computed = indenter.computeIndentation(offset);
					if (computed == null)
						computed = new StringBuilder(0);
					int tabSize = getTabSize();
					while (slashes > 0 && computed.length() > 0) {
						char c = computed.charAt(0);
						if (c == '\t') {
							if (slashes > tabSize)
								slashes -= tabSize;
							else
								break;
						} else if (c == ' ') {
							slashes--;
						} else {
							break;
						}

						computed.deleteCharAt(0);
					}

					indent = document.get(offset, wsStart - offset) + computed;
				}
			}
		}

		// standard C code indentation
		if (indent == null) {
			StringBuilder computed = indenter.computeIndentation(offset);
			if (computed != null)
				indent = computed.toString();
			else
				indent = ""; //$NON-NLS-1$
		}

		// change document:
		// get current white space
		int lineLength = currentLine.getLength();
		int end = scanner.findNonWhitespaceForwardInAnyPartition(wsStart, offset + lineLength);
		if (end == CHeuristicScanner.NOT_FOUND) {
			// an empty line
			end = offset + lineLength;
			if (multiLine && !indentEmptyLines())
				indent = ""; //$NON-NLS-1$
		}
		int length = end - offset;
		String currentIndent = document.get(offset, length);

		// if we are right before the text start / line end, and already after the insertion point
		// then just shift to the right
		if (fIsTabAction && caret == end && whiteSpaceLength(currentIndent) >= whiteSpaceLength(indent)) {
			int indentWidth = whiteSpaceLength(currentIndent) + getIndentSize();
			if (useTabsAndSpaces()) {
				currentIndent = trimSpacesRight(currentIndent);
			}
			String replacement = IndentUtil.changePrefix(currentIndent, indentWidth, getTabSize(), useSpaces());
			document.replace(offset, length, replacement);
			fCaretOffset = offset + replacement.length();
			return true;
		}

		// set the caret offset so it can be used when setting the selection
		if (caret >= offset && caret <= end)
			fCaretOffset = offset + indent.length();
		else
			fCaretOffset = -1;

		// only change the document if it is a real change
		if (!indent.equals(currentIndent)) {
			document.replace(offset, length, indent);
			return true;
		}
		return false;
	}

	/**
	 * Strip trailing space characters.
	 *
	 * @param indent
	 * @return string with trailing spaces removed
	 */
	private String trimSpacesRight(String indent) {
		int i = indent.length() - 1;
		while (i >= 0 && indent.charAt(i) == ' ') {
			--i;
		}
		return indent.substring(0, i + 1);
	}

	/**
	 * Computes and returns the indentation for a block comment line.
	 *
	 * @param document the document
	 * @param line the line in document
	 * @param scanner the scanner
	 * @param partition the comment partition
	 * @return the indent, or <code>null</code> if not computable
	 * @throws BadLocationException
	 */
	private String computeCommentIndent(IDocument document, int line, CHeuristicScanner scanner, ITypedRegion partition)
			throws BadLocationException {
		return IndentUtil.computeCommentIndent(document, line, scanner, partition);
	}

	/**
	 * Computes and returns the indentation for a preprocessor line.
	 *
	 * @param document the document
	 * @param line the line in document
	 * @param partition the comment partition
	 * @return the indent, or <code>null</code> if not computable
	 * @throws BadLocationException
	 */
	private String computePreprocessorIndent(IDocument document, int line, ITypedRegion partition)
			throws BadLocationException {
		return IndentUtil.computePreprocessorIndent(document, line, partition);
	}

	/**
	 * Returns the size in characters of a string. All characters count one, tabs count the editor's
	 * preference for the tab display
	 *
	 * @param indent the string to be measured.
	 * @return the size in characters of a string
	 */
	private int whiteSpaceLength(String indent) {
		if (indent == null)
			return 0;
		return IndentUtil.computeVisualLength(indent, getTabSize());
	}

	/**
	 * Returns whether spaces should be used exclusively for indentation, depending on the editor and
	 * formatter preferences.
	 *
	 * @return <code>true</code> if only spaces should be used
	 */
	private boolean useSpaces() {
		return CCorePlugin.SPACE.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR));
	}

	/**
	 * Returns whether mixed tabs/spaces should be used for indentation, depending on the editor and
	 * formatter preferences.
	 *
	 * @return <code>true</code> if tabs and spaces should be used
	 */
	private boolean useTabsAndSpaces() {
		return DefaultCodeFormatterConstants.MIXED
				.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR));
	}

	/**
	 * Returns the tab size used by the editor, which is deduced from the
	 * formatter preferences.
	 *
	 * @return the tab size as defined in the current formatter preferences
	 */
	private int getTabSize() {
		return getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE, 4);
	}

	/**
	 * Returns the indent size used by the editor, which is deduced from the
	 * formatter preferences.
	 *
	 * @return the indent size as defined in the current formatter preferences
	 */
	private int getIndentSize() {
		return CodeFormatterUtil.getIndentWidth(getCProject());
	}

	/**
	 * Returns <code>true</code> if empty lines should be indented, <code>false</code> otherwise.
	 *
	 * @return <code>true</code> if empty lines should be indented, <code>false</code> otherwise
	 */
	private boolean indentEmptyLines() {
		return DefaultCodeFormatterConstants.TRUE
				.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_EMPTY_LINES));
	}

	/**
	 * Returns <code>true</code> if line comments at column 0 should be indented inside, <code>false</code> otherwise.
	 *
	 * @return <code>true</code> if line comments at column 0 should be indented inside, <code>false</code> otherwise.
	 */
	private boolean indentInsideLineComments() {
		return DefaultCodeFormatterConstants.TRUE
				.equals(getCoreFormatterOption(DefaultCodeFormatterConstants.FORMATTER_INDENT_INSIDE_LINE_COMMENTS));
	}

	/**
	 * Returns the possibly project-specific core preference defined under <code>key</code>.
	 *
	 * @param key the key of the preference
	 * @return the value of the preference
	 */
	private String getCoreFormatterOption(String key) {
		ICProject project = getCProject();
		if (project == null)
			return CCorePlugin.getOption(key);
		return project.getOption(key, true);
	}

	/**
	 * Returns the possibly project-specific core preference defined under <code>key</code>, or
	 * <code>def</code> if the value is not a integer.
	 *
	 * @param key the key of the preference
	 * @param def the default value
	 * @return the value of the preference
	 */
	private int getCoreFormatterOption(String key, int def) {
		try {
			return Integer.parseInt(getCoreFormatterOption(key));
		} catch (NumberFormatException e) {
			return def;
		}
	}

	/**
	 * Returns the <code>ICProject</code> of the current editor input, or
	 * <code>null</code> if it cannot be found.
	 *
	 * @return the <code>ICProject</code> of the current editor input, or
	 *         <code>null</code> if it cannot be found
	 */
	private ICProject getCProject() {
		ITextEditor editor = getTextEditor();
		if (editor == null)
			return null;

		return EditorUtility.getCProject(editor.getEditorInput());
	}

	/**
	 * Returns the editor's selection provider.
	 *
	 * @return the editor's selection provider or <code>null</code>
	 */
	private ISelectionProvider getSelectionProvider() {
		ITextEditor editor = getTextEditor();
		if (editor != null) {
			return editor.getSelectionProvider();
		}
		return null;
	}

	/*
	 * @see org.eclipse.ui.texteditor.IUpdate#update()
	 */
	@Override
	public void update() {
		super.update();

		if (isEnabled()) {
			if (fIsTabAction)
				setEnabled(canModifyEditor() && isSmartMode() && isValidSelection());
			else
				setEnabled(canModifyEditor() && !getSelection().isEmpty());
		}
	}

	/**
	 * Returns if the current selection is valid, i.e. whether it is empty and the caret in the
	 * whitespace at the start of a line, or covers multiple lines.
	 *
	 * @return <code>true</code> if the selection is valid for an indent operation
	 */
	private boolean isValidSelection() {
		ITextSelection selection = getSelection();
		if (selection.isEmpty())
			return false;

		int offset = selection.getOffset();
		int length = selection.getLength();

		IDocument document = getDocument();
		if (document == null)
			return false;

		try {
			IRegion firstLine = document.getLineInformationOfOffset(offset);
			int lineOffset = firstLine.getOffset();

			// either the selection has to be empty and the caret in the WS at the line start
			// or the selection has to extend over multiple lines
			if (length == 0) {
				return document.get(lineOffset, offset - lineOffset).trim().length() == 0;
			}
			//			return lineOffset + firstLine.getLength() < offset + length;
			return false; // only enable for empty selections for now
		} catch (BadLocationException e) {
		}

		return false;
	}

	/**
	 * Returns the smart preference state.
	 *
	 * @return <code>true</code> if smart mode is on, <code>false</code> otherwise
	 */
	private boolean isSmartMode() {
		ITextEditor editor = getTextEditor();

		if (editor instanceof ITextEditorExtension3)
			return ((ITextEditorExtension3) editor).getInsertMode() == ITextEditorExtension3.SMART_INSERT;

		return false;
	}

	/**
	 * Returns the document currently displayed in the editor, or <code>null</code> if none can be
	 * obtained.
	 *
	 * @return the current document or <code>null</code>
	 */
	private IDocument getDocument() {
		ITextEditor editor = getTextEditor();
		if (editor != null) {
			IDocumentProvider provider = editor.getDocumentProvider();
			IEditorInput input = editor.getEditorInput();
			if (provider != null && input != null)
				return provider.getDocument(input);

		}
		return null;
	}

	/**
	 * Returns the selection on the editor or an invalid selection if none can be obtained. Returns
	 * never <code>null</code>.
	 *
	 * @return the current selection, never <code>null</code>
	 */
	private ITextSelection getSelection() {
		ISelectionProvider provider = getSelectionProvider();
		if (provider != null) {
			ISelection selection = provider.getSelection();
			if (selection instanceof ITextSelection)
				return (ITextSelection) selection;
		}

		// null object
		return TextSelection.emptySelection();
	}
}

Back to the top