| /*=============================================================================# |
| # Copyright (c) 2014, 2020 Stephan Wahlbrink and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 |
| # which is available at https://www.apache.org/licenses/LICENSE-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 |
| # |
| # Contributors: |
| # Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ltk.ui.sourceediting.actions; |
| |
| import java.util.IdentityHashMap; |
| import java.util.Map; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.core.commands.AbstractHandler; |
| import org.eclipse.core.commands.ExecutionEvent; |
| import org.eclipse.core.commands.ExecutionException; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.text.AbstractDocument; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPartitioningException; |
| import org.eclipse.jface.text.DocumentRewriteSession; |
| import org.eclipse.jface.text.DocumentRewriteSessionType; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITextOperationTarget; |
| import org.eclipse.jface.text.ITextSelection; |
| import org.eclipse.jface.text.ITypedRegion; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.swt.custom.BusyIndicator; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.InsertEdit; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.TextEdit; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| import org.eclipse.ui.texteditor.IEditorStatusLine; |
| |
| import org.eclipse.statet.ecommons.ui.SharedUIResources; |
| |
| import org.eclipse.statet.internal.ltk.ui.EditingMessages; |
| import org.eclipse.statet.ltk.ui.EditorUtils; |
| import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor1; |
| import org.eclipse.statet.ltk.ui.sourceediting.SourceEditorViewer; |
| |
| |
| public class ToggleCommentHandler extends AbstractHandler { |
| |
| |
| private static final ConcurrentHashMap<String, Pattern> DEFAULT_PREFIX_MAP= new ConcurrentHashMap<>(); |
| |
| protected static Pattern getDefaultPrefixPattern(final String prefix) { |
| Pattern pattern= DEFAULT_PREFIX_MAP.get(prefix); |
| if (pattern == null) { |
| pattern= Pattern.compile("^[ \\t]*(" + Pattern.quote(prefix) + ")"); //$NON-NLS-1$ //$NON-NLS-2$ |
| DEFAULT_PREFIX_MAP.putIfAbsent(prefix, pattern); |
| } |
| return pattern; |
| } |
| |
| protected static final Pattern HTML_SPACE_PREFIX_PATTERN= Pattern.compile("^[ \\t]*(" + Pattern.quote("<!--") + " ?)"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| protected static final Pattern HTML_SPACE_POSTFIX_PATTERN= Pattern.compile("( ?" + Pattern.quote("-->") + "[ \\t]*)$"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| |
| |
| private final SourceEditor1 editor; |
| |
| /** The text operation target */ |
| private ITextOperationTarget operationTarget; |
| |
| private final Map<String, String[]> contentTypePrefixes= new IdentityHashMap<>(); |
| |
| |
| public ToggleCommentHandler(final SourceEditor1 editor) { |
| this.editor= editor; |
| |
| setBaseEnabled(false); |
| } |
| |
| |
| protected SourceEditor1 getEditor() { |
| return this.editor; |
| } |
| |
| @Override |
| public void setEnabled(final Object evaluationContext) { |
| if (this.editor.getViewer() instanceof SourceEditorViewer |
| && !this.editor.isEditorInputModifiable()) { |
| setBaseEnabled(false); |
| return; |
| } |
| |
| if (this.operationTarget == null) { |
| this.operationTarget= this.editor.getAdapter(ITextOperationTarget.class); |
| } |
| setBaseEnabled(this.operationTarget != null |
| && this.operationTarget.canDoOperation(ITextOperationTarget.PREFIX) |
| && this.operationTarget.canDoOperation(ITextOperationTarget.STRIP_PREFIX) ); |
| } |
| |
| private ITextSelection getSelection() { |
| final ISelection selection= this.editor.getSelectionProvider().getSelection(); |
| return (selection instanceof ITextSelection) ? (ITextSelection) selection : null; |
| } |
| |
| @Override |
| public Object execute(final ExecutionEvent event) throws ExecutionException { |
| if (!this.editor.validateEditorInputState() || !isEnabled()) { |
| return null; |
| } |
| |
| final ITextSelection selection= getSelection(); |
| final AbstractDocument document= (AbstractDocument) this.editor.getViewer().getDocument(); |
| final int operationCode= (isSelectionCommented(document, selection)) ? |
| ITextOperationTarget.STRIP_PREFIX : ITextOperationTarget.PREFIX; |
| |
| final Shell shell= this.editor.getSite().getShell(); |
| if (!this.operationTarget.canDoOperation(operationCode)) { |
| signalError(); |
| return null; |
| } |
| |
| Display display= null; |
| if (shell != null && !shell.isDisposed()) { |
| display= shell.getDisplay(); |
| } |
| |
| BusyIndicator.showWhile(display, new Runnable() { |
| @Override |
| public void run() { |
| ToggleCommentHandler.this.run(document, selection, operationCode); |
| } |
| }); |
| return null; |
| } |
| |
| protected void run(final AbstractDocument document, final ITextSelection selection, |
| final int operationCode) { |
| doRunOperation(operationCode); |
| } |
| |
| protected void doRunOperation(final int operationCode) { |
| this.operationTarget.doOperation(operationCode); |
| } |
| |
| |
| protected String[] getPrefixes(final String contentType) { |
| String[] prefixes= this.contentTypePrefixes.get(contentType); |
| if (prefixes == null) { |
| prefixes= ((SourceEditorViewer) this.editor.getViewer()).getDefaultPrefixes(contentType); |
| if (prefixes == null) { |
| prefixes= new String[0]; |
| } |
| else { |
| int numEmpty= 0; |
| for (int i= 0; i < prefixes.length; i++) { |
| if (prefixes[i].isEmpty()) { |
| numEmpty++; |
| } |
| } |
| if (numEmpty > 0) { |
| final String[] nonemptyPrefixes= new String[prefixes.length - numEmpty]; |
| for (int i= 0, j= 0; i < prefixes.length; i++) { |
| final String prefix= prefixes[i]; |
| if (!prefix.isEmpty()) { |
| nonemptyPrefixes[j++]= prefix; |
| } |
| } |
| prefixes= nonemptyPrefixes; |
| } |
| } |
| this.contentTypePrefixes.put(contentType, prefixes); |
| } |
| return prefixes; |
| } |
| |
| protected Pattern getPrefixPattern(final String contentType, final String prefix) { |
| return getDefaultPrefixPattern(prefix); |
| } |
| |
| protected Pattern getPostfixPattern(final String contentType, final String prefix) { |
| return null; |
| } |
| |
| /** |
| * Is the given selection single-line commented? |
| * |
| * @param selection Selection to check |
| * @return <code>true</code> iff all selected lines are commented |
| * @throws BadPartitioningException |
| */ |
| private boolean isSelectionCommented(final AbstractDocument document, final ITextSelection selection) { |
| if (selection.getStartLine() < 0 || selection.getEndLine() < 0) { |
| return false; |
| } |
| |
| try { |
| final IRegion block= EditorUtils.getTextBlockFromSelection(document, |
| selection.getOffset(), selection.getLength() ); |
| final ITypedRegion[] regions= document.computePartitioning( |
| this.editor.getDocumentContentInfo().getPartitioning(), |
| block.getOffset(), block.getLength(), false ); |
| |
| final int[] lines= new int[regions.length * 2]; // [startline, endline, startline, endline, ...] |
| for (int i= 0, j= 0; i < regions.length; i++, j+= 2) { |
| // start line of region |
| lines[j]= getFirstCompleteLineOfRegion(document, regions[i]); |
| // end line of region |
| final int length= regions[i].getLength(); |
| int offset= regions[i].getOffset() + length; |
| if (length > 0) { |
| offset--; |
| } |
| lines[j + 1]= (lines[j] == -1) ? -1 : document.getLineOfOffset(offset); |
| } |
| |
| // Perform the check |
| for (int i= 0, j= 0; i < regions.length; i++, j+= 2) { |
| final String[] prefixes= getPrefixes(regions[i].getType()); |
| if (prefixes != null && prefixes.length > 0 && lines[j] >= 0 && lines[j + 1] >= 0) { |
| if (!isBlockCommented(lines[j], lines[j + 1], prefixes, document)) { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| catch (final BadLocationException | BadPartitioningException e) { |
| log(e); |
| } |
| return false; |
| } |
| |
| /** |
| * Determines whether each line is prefixed by one of the prefixes. |
| * |
| * @param startLine Start line in document |
| * @param endLine End line in document |
| * @param prefixes Possible comment prefixes |
| * @param document The document |
| * @return <code>true</code> iff each line from <code>startLine</code> |
| * to and including <code>endLine</code> is prepended by one |
| * of the <code>prefixes</code>, ignoring whitespace at the |
| * begin of line |
| */ |
| private boolean isBlockCommented(final int startLine, final int endLine, final String[] prefixes, |
| final IDocument document) { |
| try { |
| // check for occurrences of prefixes in the given lines |
| for (int i= startLine; i <= endLine; i++) { |
| |
| final IRegion lineInfo= document.getLineInformation(i); |
| final String text= document.get(lineInfo.getOffset(), lineInfo.getLength()); |
| |
| final int[] found= TextUtilities.indexOf(prefixes, text, 0); |
| |
| if (found[0] == -1) { |
| // found a line which is not commented |
| return false; |
| } |
| |
| final String beforePrefix= text.substring(0, found[0]); |
| if (beforePrefix.trim().length() != 0) { |
| // found a line which is not commented |
| return false; |
| } |
| } |
| return true; |
| } |
| catch (final BadLocationException e) { |
| log(e); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the index of the first line whose start offset is in the given text range. |
| * |
| * @param document The document |
| * @param region the text range in characters where to find the line |
| * @return the first line whose start index is in the given range, -1 if there is no such line |
| * @throws BadLocationException |
| */ |
| protected int getFirstCompleteLineOfRegion(final IDocument document, final IRegion region) throws BadLocationException { |
| final int startLine= document.getLineOfOffset(region.getOffset()); |
| |
| int offset= document.getLineOffset(startLine); |
| if (offset >= region.getOffset()) { |
| return startLine; |
| } |
| offset= document.getLineOffset(startLine + 1); |
| return (offset > region.getOffset() + region.getLength() ? -1 : startLine + 1); |
| } |
| |
| protected void doPrefix(final AbstractDocument document, final IRegion region, |
| final String prefix) |
| throws BadLocationException, BadPartitioningException { |
| final int startLine= document.getLineOfOffset(region.getOffset()); |
| final int stopLine= (region.getLength() > 0) ? |
| document.getLineOfOffset(region.getOffset()+region.getLength()-1) : |
| startLine; |
| |
| final MultiTextEdit multi= new MultiTextEdit(region.getOffset(), region.getLength()); |
| for (int line= startLine; line <= stopLine; line++) { |
| multi.addChild(new InsertEdit(document.getLineOffset(line), prefix)); |
| } |
| |
| { final DocumentRewriteSession rewriteSession= document.startRewriteSession( |
| DocumentRewriteSessionType.STRICTLY_SEQUENTIAL ); |
| try { |
| multi.apply(document, TextEdit.NONE); |
| } |
| finally { |
| document.stopRewriteSession(rewriteSession); |
| } |
| } |
| } |
| |
| protected void doPrefix(final AbstractDocument document, final IRegion region, |
| final String prefix, final String postfix) |
| throws BadLocationException, BadPartitioningException { |
| final int startLine= document.getLineOfOffset(region.getOffset()); |
| final int stopLine= (region.getLength() > 0) ? |
| document.getLineOfOffset(region.getOffset()+region.getLength()-1) : |
| startLine; |
| |
| final MultiTextEdit multi= new MultiTextEdit(region.getOffset(), region.getLength()); |
| for (int line= startLine; line <= stopLine; line++) { |
| final IRegion lineInfo= document.getLineInformation(line); |
| multi.addChild(new InsertEdit(lineInfo.getOffset(), prefix)); |
| multi.addChild(new InsertEdit(lineInfo.getOffset() + lineInfo.getLength(), postfix)); |
| } |
| |
| { final DocumentRewriteSession rewriteSession= document.startRewriteSession( |
| DocumentRewriteSessionType.STRICTLY_SEQUENTIAL ); |
| try { |
| multi.apply(document, TextEdit.NONE); |
| } |
| finally { |
| document.stopRewriteSession(rewriteSession); |
| } |
| } |
| } |
| |
| protected void doStripPrefix(final AbstractDocument document, final IRegion region) |
| throws BadLocationException, BadPartitioningException { |
| final int startLine= document.getLineOfOffset(region.getOffset()); |
| final int stopLine= (region.getLength() > 0) ? |
| document.getLineOfOffset(region.getOffset()+region.getLength()-1) : |
| startLine; |
| |
| final MultiTextEdit multi= new MultiTextEdit(region.getOffset(), region.getLength()); |
| for (int line= startLine; line <= stopLine; line++) { |
| final IRegion lineInfo= document.getLineInformation(line); |
| final String contentType= document.getContentType( |
| this.editor.getDocumentContentInfo().getPartitioning(), |
| lineInfo.getOffset(), false ); |
| |
| final String[] prefixes= getPrefixes(contentType); |
| final String text= document.get(lineInfo.getOffset(), lineInfo.getLength()); |
| final int[] found= TextUtilities.indexOf(prefixes, text, 0); |
| assert (found[0] >= 0); |
| |
| { final Pattern pattern= getPrefixPattern(contentType, prefixes[found[1]]); |
| if (pattern != null) { |
| final Matcher matcher= pattern.matcher(text); |
| matcher.reset(text); |
| if (matcher.find()) { |
| multi.addChild(new DeleteEdit( |
| lineInfo.getOffset() + matcher.start(1), |
| matcher.end(1) - matcher.start(1) )); |
| } |
| } |
| else { |
| multi.addChild(new DeleteEdit( |
| lineInfo.getOffset() + found[0], |
| prefixes[found[1]].length() )); |
| } |
| } |
| { final Pattern pattern= getPostfixPattern(contentType, prefixes[found[1]]); |
| if (pattern != null) { |
| final Matcher matcher= pattern.matcher(text); |
| if (matcher.find()) { |
| multi.addChild(new DeleteEdit( |
| lineInfo.getOffset() + matcher.start(1), |
| matcher.end(1) - matcher.start(1) )); |
| } |
| } |
| } |
| } |
| |
| { final DocumentRewriteSession rewriteSession= document.startRewriteSession( |
| DocumentRewriteSessionType.STRICTLY_SEQUENTIAL ); |
| try { |
| multi.apply(document, TextEdit.NONE); |
| } |
| finally { |
| document.stopRewriteSession(rewriteSession); |
| } |
| } |
| } |
| |
| |
| protected void signalError() { |
| final IEditorStatusLine statusLine= this.editor.getAdapter(IEditorStatusLine.class); |
| statusLine.setMessage(true, EditingMessages.ToggleCommentAction_error, null); |
| this.editor.getViewer().getTextWidget().getDisplay().beep(); |
| } |
| |
| protected void log(final Exception e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.BUNDLE_ID, 0, |
| "An error ocurred when executing toggle comment operation.", e ), StatusManager.LOG); |
| } |
| |
| } |