diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/FillParagraphAction.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/FillParagraphAction.java new file mode 100644 index 000000000000..f4cef0d59ced --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/FillParagraphAction.java @@ -0,0 +1,54 @@ +package com.intellij.codeInsight.editorActions.fillParagraph; + +import com.intellij.codeInsight.CodeInsightActionHandler; +import com.intellij.codeInsight.actions.BaseCodeInsightAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; + +/** + * User : ktisha + * + * Action to re-flow paragraph to fit right margin. + * Glues paragraph and then splits into lines with appropriate length + * + * The action came from Emacs users // PY-4775 + */ +public class FillParagraphAction extends BaseCodeInsightAction { + + @Override + protected CodeInsightActionHandler getHandler() { + return new Handler(); + } + private static class Handler implements CodeInsightActionHandler { + + @Override + public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile file) { + + ParagraphFillHandler paragraphFillHandler = LanguageFillParagraphExtension.INSTANCE.forLanguage(file.getLanguage()); + + final int offset = editor.getCaretModel().getOffset(); + PsiElement element = file.findElementAt(offset); + if (element != null && paragraphFillHandler != null && paragraphFillHandler.isAvailableForFile(file) + && paragraphFillHandler.isAvailableForElement(element)) { + + paragraphFillHandler.performOnElement(element, editor); + } + } + + @Override + public boolean startInWriteAction() { + return true; + } + } + + @Override + protected boolean isValidForFile(Project project, Editor editor, PsiFile file) { + final ParagraphFillHandler handler = + LanguageFillParagraphExtension.INSTANCE.forLanguage(file.getLanguage()); + return handler != null && handler.isAvailableForFile(file); + } + +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/LanguageFillParagraphExtension.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/LanguageFillParagraphExtension.java new file mode 100644 index 000000000000..8125e8e1b8c9 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/LanguageFillParagraphExtension.java @@ -0,0 +1,16 @@ +package com.intellij.codeInsight.editorActions.fillParagraph; + +import com.intellij.lang.LanguageExtension; + +/** + * User : ktisha + */ +public class LanguageFillParagraphExtension extends LanguageExtension { + + public static final String EP_NAME = "com.intellij.codeInsight.fillParagraph"; + public static final LanguageFillParagraphExtension INSTANCE = new LanguageFillParagraphExtension(); + + public LanguageFillParagraphExtension() { + super(EP_NAME, new ParagraphFillHandler()); + } +} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/ParagraphFillHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/ParagraphFillHandler.java new file mode 100644 index 000000000000..29b3f037bc26 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/codeInsight/editorActions/fillParagraph/ParagraphFillHandler.java @@ -0,0 +1,155 @@ +package com.intellij.codeInsight.editorActions.fillParagraph; + +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.CharFilter; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * Defines general re-flow paragraph functionality. + * Serves plain text files. + * + * User : ktisha + */ +public class ParagraphFillHandler { + + protected void performOnElement(@NotNull final PsiElement element, @NotNull final Editor editor) { + final Document document = editor.getDocument(); + + final TextRange textRange = getTextRange(element, editor); + final String text = textRange.substring(element.getContainingFile().getText()); + + final List subStrings = StringUtil.split(text, "\n", true); + final StringBuilder stringBuilder = new StringBuilder(); + for (String string : subStrings) { + stringBuilder.append(StringUtil.trimStart(string.trim(), getPrefix(element))).append(" "); + } + final String replacementText = stringBuilder.toString(); + + CommandProcessor.getInstance().executeCommand(element.getProject(), new Runnable() { + public void run() { + document.replaceString(textRange.getStartOffset(), textRange.getEndOffset(), + getPrefix(element) + replacementText); + final CodeFormatterFacade codeFormatter = new CodeFormatterFacade( + CodeStyleSettingsManager.getSettings(element.getProject())); + codeFormatter.doWrapLongLinesIfNecessary(editor, element.getProject(), document, + textRange.getStartOffset(), + textRange.getStartOffset() + replacementText.length() + 1); + } + }, null, document); + + } + + private TextRange getTextRange(@NotNull final PsiElement element, @NotNull final Editor editor) { + int startOffset = getStartOffset(element, editor); + int endOffset = getEndOffset(element, editor); + return TextRange.create(startOffset, endOffset); + } + + private int getStartOffset(@NotNull final PsiElement element, @NotNull final Editor editor) { + if (element instanceof PsiComment) { + final PsiElement firstCommentElement = getFirstCommentElement(element); + return firstCommentElement != null? firstCommentElement.getTextRange().getStartOffset() + : element.getTextRange().getStartOffset(); + } + final int offset = editor.getCaretModel().getOffset(); + final int elementTextOffset = element.getTextOffset(); + final Document document = editor.getDocument(); + int lineNumber = document.getLineNumber(offset); + + while (lineNumber != document.getLineNumber(elementTextOffset)) { + final String text = document.getText(TextRange.create(document.getLineStartOffset(lineNumber), + document.getLineEndOffset(lineNumber))); + if (StringUtil.isEmptyOrSpaces(text)) { + break; + } + lineNumber -= 1; + } + final int lineStartOffset = document.getLineStartOffset(lineNumber + 1); + final String lineText = document + .getText(TextRange.create(lineStartOffset, document.getLineEndOffset(lineNumber + 1))); + int shift = StringUtil.findFirst(lineText, CharFilter.NOT_WHITESPACE_FILTER); + + return lineStartOffset + shift; + } + + private int getEndOffset(@NotNull final PsiElement element, @NotNull final Editor editor) { + if (element instanceof PsiComment) { + final PsiElement next = getLastCommentElement(element); + return next != null? next.getTextRange().getEndOffset() + : element.getTextRange().getEndOffset(); + } + final int offset = editor.getCaretModel().getOffset(); + final int elementTextOffset = element.getTextRange().getEndOffset(); + final Document document = editor.getDocument(); + int lineNumber = document.getLineNumber(offset); + + while (lineNumber != document.getLineNumber(elementTextOffset)) { + final String text = document.getText(TextRange.create(document.getLineStartOffset(lineNumber), + document.getLineEndOffset(lineNumber))); + if (StringUtil.isEmptyOrSpaces(text)) + break; + lineNumber += 1; + } + return document.getLineEndOffset(lineNumber - 1); + } + + @Nullable + private PsiElement getFirstCommentElement(@NotNull final PsiElement element) { + PsiElement prevSibling = element.getPrevSibling(); + PsiElement result = element; + while (prevSibling instanceof PsiComment || (prevSibling instanceof PsiWhiteSpace && + StringUtil.countChars(prevSibling.getText(), '\n') <= 1)) { + final String text = prevSibling.getText(); + if (prevSibling instanceof PsiComment && StringUtil.isEmptyOrSpaces( + StringUtil.trimStart(text.trim(), getPrefix(element)))) { + break; + } + if (prevSibling instanceof PsiComment) + result = prevSibling; + prevSibling = prevSibling.getPrevSibling(); + } + return result; + } + + @Nullable + private PsiElement getLastCommentElement(@NotNull final PsiElement element) { + PsiElement nextSibling = element.getNextSibling(); + PsiElement result = element; + while (nextSibling instanceof PsiComment || (nextSibling instanceof PsiWhiteSpace && + StringUtil.countChars(nextSibling.getText(), '\n') <= 1)) { + final String text = nextSibling.getText(); + if (nextSibling instanceof PsiComment && StringUtil.isEmptyOrSpaces( + StringUtil.trimStart(text.trim(), getPrefix(element)))) { + break; + } + if (nextSibling instanceof PsiComment) + result = nextSibling; + nextSibling = nextSibling.getNextSibling(); + } + return result; + } + + protected boolean isAvailableForElement(@Nullable final PsiElement element) { + return element != null; + } + + protected boolean isAvailableForFile(@Nullable final PsiFile psiFile) { + return psiFile instanceof PsiPlainTextFile; + } + + @NotNull + protected String getPrefix(@NotNull final PsiElement element) { + return ""; + } + +} diff --git a/platform/platform-resources-en/src/messages/ActionsBundle.properties b/platform/platform-resources-en/src/messages/ActionsBundle.properties index b7cc81522c6d..b9d89c7fc718 100644 --- a/platform/platform-resources-en/src/messages/ActionsBundle.properties +++ b/platform/platform-resources-en/src/messages/ActionsBundle.properties @@ -1354,3 +1354,6 @@ action.Diff.UpdatedFiles.description=Show diff with version before update action.TabList.text=Show All Tabs action.TabList.description=Show dropdown with the list of all tabs in single-row tab layout + +action.FillParagraph.text=Fill paragraph... +action.FillParagraph.description=Reformat string literal or comment to fit maximum width (usually 80) diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index b4df9934e038..8b2ef6109878 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -378,6 +378,7 @@ + diff --git a/platform/platform-resources/src/idea/LangActions.xml b/platform/platform-resources/src/idea/LangActions.xml index 4339965c6be1..a5516e0991ad 100644 --- a/platform/platform-resources/src/idea/LangActions.xml +++ b/platform/platform-resources/src/idea/LangActions.xml @@ -290,6 +290,7 @@ +