Skip to content

Instantly share code, notes, and snippets.

@dkandalov
Last active March 7, 2018 16:11
Show Gist options
  • Save dkandalov/5557393 to your computer and use it in GitHub Desktop.
Save dkandalov/5557393 to your computer and use it in GitHub Desktop.
IntelliJ micro-plugin to wrap selected text to the column width (copied from https://github.com/abrookins/WrapToColumn)
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.SelectionModel
import com.intellij.openapi.editor.actionSystem.EditorAction
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.project.Project
import com.intellij.psi.codeStyle.CodeStyleSettings
import com.intellij.psi.codeStyle.CodeStyleSettingsManager
import junit.framework.Assert
import junit.framework.TestSuite
import org.apache.commons.lang.WordUtils
import org.junit.Before
import org.junit.Test
import org.junit.internal.TextListener
import org.junit.runner.JUnitCore
import org.junit.runner.notification.Failure
import java.io.StringReader
import java.util.ArrayList
import java.util.regex.Matcher
import java.util.regex.Pattern
import static liveplugin.PluginUtil.*
import static org.junit.Assert.*
// Wraps selected text or, if no text is selected, the current line to the column width
// specified in the editor's "Right Margin (columns)" setting.
//
// This is almost direct copy-paste of this plugin https://github.com/abrookins/WrapToColumn
// (Note that this code was meant to be run within this plugin https://github.com/dkandalov/live-plugin)
if (!runUnitTests(project, CodeWrapperTest.class)) return
def id = "com.andrewbrookins.idea.wrap.WrapAction"
registerAction(id, "shift ctrl meta W", "EditMenu", "Wrap to Column") { event ->
new WrapAction().actionPerformed(event)
}
show("loaded WrapAction")
class WrapAction extends EditorAction {
private static final Logger log = Logger.getInstance(WrapAction.class)
WrapAction() {
super(new WrapHandler())
}
@Override void update(AnActionEvent e) {
super.update(e)
if (ActionPlaces.isPopupPlace(e.place)) {
e.presentation.visible = e.presentation.enabled
}
}
private static class WrapHandler extends EditorActionHandler {
@Override void execute(final Editor editor, final DataContext dataContext) {
ApplicationManager.application.runWriteAction(new Runnable() {
@Override void run() {
def project = LangDataKeys.PROJECT.getData(dataContext)
def selectionModel = editor.selectionModel
if (!selectionModel.hasSelection()) {
selectionModel.selectLineAtCaret()
}
final String text = selectionModel.getSelectedText()
CodeWrapper wrapper = new CodeWrapper(CodeStyleSettingsManager.getSettings(project).RIGHT_MARGIN)
String wrappedText = wrapper.fillParagraphs(text)
editor.document.replaceString(selectionModel.selectionStart, selectionModel.selectionEnd, wrappedText)
}
})
}
}
}
class CodeWrapper {
private static class Options {
// A string with a newline above and below it is a paragraph.
String paragraphSeparatorPattern = "(\\n|\\r\\n)\\s*(\\n|\\r\\n)"
// A string containing a comment or empty space is considered an indent.
String indentPattern = "^\\s*(#+|//+|;+)?\\s*"
// New lines appended to text during wrapping will use this character.
String lineSeparator = System.getProperty("line.separator")
// The column width to wrap text to.
Integer width = 80
}
/*
Data about a line that has been split into two pieces: the indent portion
of the string, if one exists, and the rest of the string.
*/
private static class LineData {
String indent = ""
String rest = ""
LineData(String indent, String rest) {
this.indent = indent
this.rest = rest
}
}
private Options options
CodeWrapper(Options options = new Options()) {
this.options = options
}
CodeWrapper(Integer columnWidth) {
options = new Options()
options.width = columnWidth
}
/**
* Fill multiple paragraphs
*
* Assume that paragraphs are separated by empty lines. Preserve
* the amount of white space between paragraphs.
*
* @param text the text to fill, which may contain multiple paragraphs.
* @return text filled to set column width.
*/
String fillParagraphs(String text) {
StringBuilder result = new StringBuilder()
Pattern pattern = Pattern.compile(options.paragraphSeparatorPattern)
Matcher matcher = pattern.matcher(text)
Integer textLength = text.length()
Integer location = 0
while (matcher.find()) {
String paragraph = text.substring(location, matcher.start())
result.append(fill(paragraph))
result.append(matcher.group())
location = matcher.end()
}
if (location < textLength) {
result.append(fill(text.substring(location, textLength)))
}
String builtResult = result.toString()
// Keep trailing text newline.
if (text.endsWith(options.lineSeparator)) {
builtResult += options.lineSeparator
}
return builtResult
}
/**
* Fill paragraph by joining wrapped lines
*
* @param text the text to fill
* @return text filled with current width
*/
private String fill(String text) {
StringBuilder result = new StringBuilder()
ArrayList<String> wrappedParagraphs = wrap(text)
int size = wrappedParagraphs.size()
for (int i = 0; i < size; i++) {
String paragraph = wrappedParagraphs.get(i)
// If this is a multi-paragraph list and we aren't at the end,
// add a new line.
if (size > 0 && i < size - 1) {
paragraph += options.lineSeparator
}
result.append(paragraph)
}
return result.toString()
}
/**
* Wrap code, and comments in a smart way
*
* Reformat the single paragraph in 'text' so it fits in lines of
* no more than 'width' columns, and return a list of wrapped
* lines.
*
* @param text single paragraph of text
* @return lines filled with current width
*/
private ArrayList<String> wrap(String text) {
text = dewrap(text)
LineData firstLineData = splitIndent(text)
Integer width = options.width - firstLineData.indent.length()
String[] lines = WordUtils.wrap(text, width).split(options.lineSeparator)
ArrayList<String> result = new ArrayList<String>()
for (int i = 0; i < lines.length; i++) {
String line = lines[i]
if (i == 0) {
LineData lineData = splitIndent(line)
line = lineData.rest
}
// Use indent from the first line on it and all subsequent lines.
result.add(firstLineData.indent + line)
}
return result
}
/**
* Convert hard wrapped paragraph to one line.
*
* The indentation and comments of the first line are preserved,
* subsequent lines indent and comments characters are striped.
*
* @param text one paragraph of text, possibly hard-wrapped
* @return one line of text
*/
private String dewrap(String text) {
if (text.isEmpty()) {
return text
}
String[] lines = text.split("[\\r\\n]+")
StringBuilder result = new StringBuilder()
// Add first line as is, keeping indent
result.append(lines[0])
for (int i = 0; i < lines.length; i++) {
if (i == 0) {
continue
}
String unindentedLine = ' ' + splitIndent(lines[i]).rest
// Add rest of lines removing indent
result.append(unindentedLine)
}
return result.toString()
}
/**
* Split text on indent, including comments characters
*
* Example (parsed from left margin):
* // Comment -> ' // ', 'Comment'
*
* @param text text to remove indents from
* @return indent string, rest
*/
private LineData splitIndent(String text) {
Pattern pattern = Pattern.compile(options.indentPattern)
Matcher matcher = pattern.matcher(text)
LineData lineData = new LineData("", text)
// Only break on the first indent-worthy sequence found, to avoid any
// weirdness with comments-embedded-in-comments.
if (matcher.find()) {
lineData.indent = matcher.group()
lineData.rest = text.substring(matcher.end(), text.length())
}
return lineData
}
}
static def runUnitTests(Project project, Class... classes) {
JUnitCore junit = new JUnitCore()
def output = new ByteArrayOutputStream()
boolean hasFailures = false
junit.addListener(new TextListener(new PrintStream(output)) {
@Override void testFailure(Failure failure) {
super.testFailure(failure)
hasFailures = true
}
})
classes.each { junit.run(it) }
if (hasFailures) showInConsole(output.toString(), project)
!hasFailures
}
class CodeWrapperTest {
CodeWrapper wrapper
@Before void initialize() {
wrapper = new CodeWrapper()
}
@Test void testCreateWithoutOptions() throws Exception {
String original = "// This is my text.\n// This is my text.\n"
String text = wrapper.fillParagraphs(original)
assertEquals("// This is my text. This is my text.\n", text)
}
@Test void testFillParagraphsOneLongLine() throws Exception {
String text = wrapper.fillParagraphs("// This is my very long line of text. " +
"This is my very long line of text. This is my very long line of text.\n")
assertEquals("// This is my very long line of text. This is my very long line of text. This\n" +
"// is my very long line of text.\n", text)
}
@Test void testFillParagraphsRetainsSeparateParagraphs() throws Exception {
String text = wrapper.fillParagraphs("// This is my very long line of text. " +
"This is my very long line of text. This is my very long line of text.\n\n" +
"// This is a second paragraph.\n")
assertEquals("// This is my very long line of text. This is my very long line of text. This\n" +
"// is my very long line of text.\n\n// This is a second paragraph.\n", text)
}
@Test void testFillParagraphsWorksWithWindowsNewlines() throws Exception {
String text = wrapper.fillParagraphs("// This is my very long line of text. " +
"This is my very long line of text. This is my very long line of text.\r\n\r\n" +
"// This is a second paragraph.\r\n")
assertEquals("// This is my very long line of text. This is my very long line of text. This\n" +
"// is my very long line of text.\r\n\r\n// This is a second paragraph.\n", text)
}
@Test void testFillParagraphsDoesNotCombineTwoShortLines() throws Exception {
String text = wrapper.fillParagraphs("// This is my text.\n// This is my text.")
assertEquals("// This is my text. This is my text.", text)
}
@Test void testFillParagraphsFillsMultiLineOpener() throws Exception {
// This could be more graceful, I suppose.
String text = wrapper.fillParagraphs("/** This is my text This is my long multi-" +
"line comment opener text. More text please.")
assertEquals("/** This is my text This is my long multi-" +
"line comment opener text. More text\nplease.", text)
}
@Test void testFillParagraphsRetainsSpaceIndent() throws Exception {
String text = wrapper.fillParagraphs(" This is my long indented " +
"string. It's too long to fit on one line, uh oh! What will happen?")
assertEquals(" This is my long indented string. It's too long to fit " +
"on one line, uh oh!\n What will happen?", text)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment