├── .idea ├── .name ├── copyright │ └── profiles_settings.xml ├── vcs.xml ├── ant.xml ├── encodings.xml ├── modules.xml ├── runConfigurations │ ├── Plugin.xml │ └── Unit_tests.xml ├── compiler.xml └── misc.xml ├── CONTRIBUTORS.md ├── screenshots ├── editor.png └── invalid_sdk.png ├── .travis.yml ├── src └── cc │ └── redpen │ └── intellij │ ├── fixes │ ├── HyphenateQuickFix.kt │ ├── StartWithCapitalLetterQuickFix.kt │ ├── SuggestExpressionQuickFix.kt │ ├── SpaceBeginningOfSentenceQuickFix.kt │ ├── EndOfSentenceQuickFix.kt │ ├── SymbolWithSpaceQuickFix.kt │ ├── RemoveQuickFix.kt │ ├── ParagraphStartWithQuickFix.kt │ ├── InvalidSymbolQuickFix.kt │ ├── NumberFormatQuickFix.kt │ └── BaseQuickFix.kt │ ├── RedPenInspectionProvider.kt │ ├── SingleCharEditor.kt │ ├── SettingsManager.kt │ ├── RedPenListErrors.kt │ ├── RedPenInspection.kt │ ├── StatusWidget.kt │ ├── RedPenProvider.kt │ └── SettingsPane.kt ├── ivysettings.xml ├── .gitignore ├── LICENSE-HEADER.txt ├── test └── cc │ └── redpen │ └── intellij │ ├── fixes │ ├── BaseQuickFixTest.kt │ ├── RemoveQuickFixTest.kt │ ├── HyphenateQuickFixTest.kt │ ├── EndOfSentenceQuickFixTest.kt │ ├── StartWithCapitalLetterQuickFixTest.kt │ ├── SpaceBeginningOfSentenceQuickFixTest.kt │ ├── SuggestExpressionQuickFixTest.kt │ ├── SymbolWithSpaceQuickFixTest.kt │ ├── QuickFixCompanionTest.kt │ ├── ParagraphStartWithQuickFixTest.kt │ ├── NumberFormatQuickFixTest.kt │ └── InvalidSymbolQuickFixTest.kt │ ├── SingleCharEditorTest.kt │ ├── RedPenListErrorsTest.kt │ ├── SettingsManagerTest.kt │ ├── StatusWidgetTest.kt │ ├── BaseTest.kt │ ├── RedPenInspectionTest.kt │ ├── RedPenProviderTest.kt │ └── SettingsPaneTest.kt ├── ivy.xml ├── redpen-intellij-plugin.iml ├── README.md ├── META-INF └── plugin.xml ├── redpen-intellij-plugin.xml └── LICENSE.txt /.idea/.name: -------------------------------------------------------------------------------- 1 | redpen-intellij-plugin -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | * Anton Keks 3 | * Dmitri Ess 4 | * Takahiko Ito 5 | -------------------------------------------------------------------------------- /screenshots/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redpen-cc/redpen-intellij-plugin/HEAD/screenshots/editor.png -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /screenshots/invalid_sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redpen-cc/redpen-intellij-plugin/HEAD/screenshots/invalid_sdk.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | install: ant deps download-idea 7 | script: ant all 8 | after_success: ant publish 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/ant.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/HyphenateQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | open class HyphenateQuickFix(text: String) : BaseQuickFix(text) { 4 | override fun fixedText() = text.replace(' ', '-') 5 | } 6 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/StartWithCapitalLetterQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | open class StartWithCapitalLetterQuickFix(text: String) : BaseQuickFix(text) { 4 | override fun fixedText() = text.toUpperCase() 5 | } 6 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/SuggestExpressionQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | open class SuggestExpressionQuickFix(text: String, val errorMessage: String) : BaseQuickFix(text) { 4 | override fun fixedText() = errorMessage.replace(".*\"(.+?)\".*".toRegex(), "$1") 5 | } 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ivysettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/RedPenInspectionProvider.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import com.intellij.codeInspection.InspectionToolProvider 4 | 5 | class RedPenInspectionProvider : InspectionToolProvider { 6 | override fun getInspectionClasses(): Array> { 7 | return arrayOf(RedPenInspection::class.java) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/SpaceBeginningOfSentenceQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.codeInspection.ProblemDescriptor 4 | 5 | open class SpaceBeginningOfSentenceQuickFix(text: String) : BaseQuickFix(text) { 6 | override fun getName() = "Add space" 7 | 8 | override fun fixedText() = " " 9 | 10 | override fun getEnd(problem: ProblemDescriptor) = getStart(problem) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Directory-based project format: 2 | 3 | # User-specific stuff: 4 | .idea/workspace.xml 5 | .idea/tasks.xml 6 | .idea/dictionaries 7 | .idea/shelf 8 | 9 | # Sensitive or high-churn files: 10 | .idea/dataSources.ids 11 | .idea/dataSources.xml 12 | .idea/sqlDataSources.xml 13 | .idea/dynamic.xml 14 | .idea/uiDesigner.xml 15 | 16 | # Gradle: 17 | .idea/gradle.xml 18 | .idea/libraries 19 | 20 | ## Plugin-specific files: 21 | 22 | # IntelliJ 23 | /out/ 24 | /idea/ 25 | 26 | # Ivy 27 | /lib/ 28 | 29 | # Misc 30 | publish-result.html 31 | redpen-intellij-plugin.zip -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/EndOfSentenceQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.codeInspection.ProblemDescriptor 4 | 5 | open class EndOfSentenceQuickFix(text: String) : BaseQuickFix(text) { 6 | override fun getName() = "Swap symbols" 7 | 8 | override fun getEnd(problem: ProblemDescriptor): Int { 9 | val end = super.getEnd(problem) 10 | text += containingDocument(problem.psiElement).text[end] 11 | return end + 1 12 | } 13 | 14 | override fun fixedText() = text.last().toString() + text.first() 15 | } 16 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/SymbolWithSpaceQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | 5 | open class SymbolWithSpaceQuickFix(val config: Configuration, text: String) : BaseQuickFix(text) { 6 | override fun getName() = "Add space" 7 | 8 | override fun fixedText(): String { 9 | val symbol = config.symbolTable.getSymbolByValue(text[0]) 10 | return when { 11 | symbol.isNeedBeforeSpace -> " " + text 12 | symbol.isNeedAfterSpace -> text + " " 13 | else -> text 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.idea/runConfigurations/Plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/RemoveQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.codeInspection.ProblemDescriptor 4 | 5 | open class RemoveQuickFix(text: String) : BaseQuickFix(text) { 6 | override fun getName() = "Remove " + text 7 | 8 | override fun fixedText() = "" 9 | 10 | override fun getStart(problem: ProblemDescriptor): Int { 11 | val text = containingDocument(problem.psiElement).text 12 | var startOffset = super.getStart(problem) 13 | while (startOffset > 0 && text[startOffset - 1] == ' ') startOffset-- 14 | return startOffset 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE-HEADER.txt: -------------------------------------------------------------------------------- 1 | redpen: a text inspection tool 2 | Copyright (c) 2014-2015 Recruit Technologies Co., Ltd. and contributors 3 | (see CONTRIBUTORS.md) 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/BaseQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.intellij.BaseTest 4 | import com.intellij.codeInspection.ProblemDescriptor 5 | import com.intellij.openapi.editor.Document 6 | import com.nhaarman.mockito_kotlin.* 7 | import org.mockito.Mockito 8 | 9 | abstract class BaseQuickFixTest(var quickFix: BaseQuickFix) : BaseTest() { 10 | val problem = mock(Mockito.RETURNS_DEEP_STUBS) 11 | val document = mock() 12 | val psiElement = problem.psiElement 13 | 14 | init { 15 | quickFix = spy(quickFix) 16 | doReturn(document).whenever(quickFix).containingDocument(psiElement) 17 | doNothing().whenever(quickFix).writeAction(any(), any()) 18 | } 19 | } -------------------------------------------------------------------------------- /ivy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/RemoveQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class RemoveQuickFixTest : BaseQuickFixTest(RemoveQuickFix("very")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Remove very", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("foo foo bar") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(5, 8)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(3, 8, "") 26 | } 27 | } -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/ParagraphStartWithQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import com.intellij.codeInspection.ProblemDescriptor 5 | import com.intellij.openapi.util.TextRange 6 | 7 | open class ParagraphStartWithQuickFix(config: Configuration, var fullText: String, val range: TextRange) : 8 | BaseQuickFix(fullText.substring(range.startOffset, range.endOffset)) { 9 | val prefix = config.validatorConfigs.find { it.configurationName == "ParagraphStartWith" }?.getProperty("start_from") ?: "" 10 | 11 | override fun getName() = "Add paragraph prefix " + prefix 12 | 13 | override fun fixedText() = prefix 14 | 15 | override fun getEnd(problem: ProblemDescriptor) = skipWhitespace(fullText, range.startOffset) 16 | 17 | protected fun skipWhitespace(line: String, start: Int): Int { 18 | return (start..line.length - 1).find { !line[it].isWhitespace() } ?: line.length 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/HyphenateQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class HyphenateQuickFixTest : BaseQuickFixTest(HyphenateQuickFix("can do")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Change to can-do", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("mega can do it") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(5, 11)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(5, 11, "can-do") 26 | } 27 | } -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/EndOfSentenceQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class EndOfSentenceQuickFixTest : BaseQuickFixTest(EndOfSentenceQuickFix("\"")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Swap symbols", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("Hello \"world\".") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(12, 13)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(12, 14, ".\"") 26 | } 27 | } -------------------------------------------------------------------------------- /src/cc/redpen/intellij/SingleCharEditor.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import java.awt.Color 4 | import javax.swing.DefaultCellEditor 5 | import javax.swing.JComponent 6 | import javax.swing.JTextField 7 | import javax.swing.border.LineBorder 8 | import javax.swing.text.AttributeSet 9 | import javax.swing.text.PlainDocument 10 | 11 | internal class SingleCharEditor : DefaultCellEditor(JTextField(SingleCharEditor.SingleCharDocument(), null, 1)) { 12 | init { 13 | (component as JComponent).border = LineBorder(Color.black) 14 | } 15 | 16 | override fun stopCellEditing(): Boolean { 17 | return (component as JTextField).text.length == 1 && super.stopCellEditing() 18 | } 19 | 20 | internal class SingleCharDocument : PlainDocument() { 21 | override fun insertString(offset: Int, str: String?, a: AttributeSet?) { 22 | if (str != null && str.length + length == 1) super.insertString(offset, str, a) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/StartWithCapitalLetterQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class StartWithCapitalLetterQuickFixTest : BaseQuickFixTest(StartWithCapitalLetterQuickFix("h")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Change to H", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("hello") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(0, 1)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(0, 1, "H") 26 | } 27 | } -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/SpaceBeginningOfSentenceQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class SpaceBeginningOfSentenceQuickFixTest : BaseQuickFixTest(SpaceBeginningOfSentenceQuickFix("")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Add space", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("First sentence.Second sentence.") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(16, 17)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(16, 16, " ") 26 | } 27 | } -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/InvalidSymbolQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import com.intellij.openapi.util.TextRange 5 | 6 | open class InvalidSymbolQuickFix(config: Configuration, var fullText: String, var range: TextRange) : BaseQuickFix(fullText[range.startOffset].toString()) { 7 | val symbolTable = config.symbolTable 8 | 9 | override fun fixedText(): String { 10 | val c = text[0] 11 | val after = if (range.startOffset < fullText.length - 1) fullText[range.startOffset + 1] else ' ' 12 | val before = if (range.startOffset > 0) fullText[range.startOffset - 1] else ' ' 13 | val symbols = symbolTable.names.map { symbolTable.getSymbol(it) }.filter { c in it.invalidChars } 14 | if (symbols.isEmpty()) 15 | return text 16 | else 17 | return (symbols.find { 18 | it.isNeedAfterSpace && !after.isLetterOrDigit() || it.isNeedBeforeSpace && !before.isLetterOrDigit() 19 | } ?: symbols[0]).value.toString() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/SuggestExpressionQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.nhaarman.mockito_kotlin.capture 5 | import com.nhaarman.mockito_kotlin.eq 6 | import com.nhaarman.mockito_kotlin.verify 7 | import com.nhaarman.mockito_kotlin.whenever 8 | import org.junit.Assert.assertEquals 9 | import org.junit.Test 10 | 11 | class SuggestExpressionQuickFixTest : BaseQuickFixTest(SuggestExpressionQuickFix("info", "Found invalid word \"info\". Use the synonym \"information\" instead.")) { 12 | @Test 13 | fun name() { 14 | assertEquals("Change to information", quickFix.name) 15 | } 16 | 17 | @Test 18 | fun applyFix() { 19 | whenever(document.text).thenReturn("More info here") 20 | whenever(problem.textRangeInElement).thenReturn(TextRange(5, 9)) 21 | 22 | quickFix.applyFix(project, problem) 23 | 24 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 25 | verify(document).replaceString(5, 9, "information") 26 | } 27 | } -------------------------------------------------------------------------------- /test/cc/redpen/intellij/SingleCharEditorTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import org.junit.Assert.* 4 | import org.junit.Test 5 | import javax.swing.JTextField 6 | 7 | class SingleCharEditorTest { 8 | private val editor = SingleCharEditor() 9 | 10 | @Test 11 | fun editingCannotBeStoppedIfEmpty() { 12 | assertFalse(editor.stopCellEditing()) 13 | } 14 | 15 | @Test 16 | fun editingCannotBeStoppedIfMoreChars() { 17 | (editor.component as JTextField).text = "ab" 18 | assertFalse(editor.stopCellEditing()) 19 | } 20 | 21 | @Test 22 | fun editingCanBeStoppedIfSingleChar() { 23 | (editor.component as JTextField).text = "a" 24 | assertTrue(editor.stopCellEditing()) 25 | } 26 | 27 | @Test 28 | fun cannotInputMoreThanOneChar() { 29 | val document = (editor.component as JTextField).document 30 | document.insertString(0, "a", null) 31 | assertEquals("a", document.getText(0, document.length)) 32 | 33 | document.insertString(1, "b", null) 34 | assertEquals("a", document.getText(0, document.length)) 35 | } 36 | } -------------------------------------------------------------------------------- /.idea/runConfigurations/Unit_tests.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/NumberFormatQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import java.math.BigDecimal 5 | import java.text.NumberFormat 6 | import java.util.* 7 | 8 | open class NumberFormatQuickFix(var config: Configuration, text: String) : BaseQuickFix(text) { 9 | override fun fixedText(): String { 10 | try { 11 | val validatorConfig = config.validatorConfigs.find { it.configurationName == "NumberFormat" }!! 12 | val eu = validatorConfig.properties["decimal_delimiter_is_comma"] == "true" 13 | 14 | val format = NumberFormat.getNumberInstance(if (eu) Locale.GERMANY else Locale.US) 15 | val number = BigDecimal(text.replace(',', '.')) 16 | format.minimumFractionDigits = number.scale() 17 | val result = format.format(number) 18 | if (config.key == "ja" && config.variant.startsWith("zenkaku")) return result.replace('.', '・').replace(',', '.') 19 | else return result 20 | } 21 | catch (e: NumberFormatException) { 22 | return text 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/SymbolWithSpaceQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import com.intellij.openapi.util.TextRange 5 | import com.nhaarman.mockito_kotlin.capture 6 | import com.nhaarman.mockito_kotlin.eq 7 | import com.nhaarman.mockito_kotlin.verify 8 | import com.nhaarman.mockito_kotlin.whenever 9 | import org.junit.Assert.assertEquals 10 | import org.junit.Test 11 | 12 | class SymbolWithSpaceQuickFixTest : BaseQuickFixTest(SymbolWithSpaceQuickFix(Configuration.builder().build(), "")) { 13 | @Test 14 | fun name() { 15 | assertEquals("Add space", quickFix.name) 16 | } 17 | 18 | @Test 19 | fun applyFixForSpaceBefore() { 20 | quickFix.text = "(" 21 | whenever(document.text).thenReturn("Hello(World)") 22 | whenever(problem.textRangeInElement).thenReturn(TextRange(5, 6)) 23 | 24 | quickFix.applyFix(project, problem) 25 | 26 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 27 | verify(document).replaceString(5, 6, " (") 28 | } 29 | 30 | @Test 31 | fun applyFixForSpaceAfter() { 32 | quickFix.text = ")" 33 | whenever(document.text).thenReturn("(Hello)World") 34 | whenever(problem.textRangeInElement).thenReturn(TextRange(6, 7)) 35 | 36 | quickFix.applyFix(project, problem) 37 | 38 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 39 | verify(document).replaceString(6, 7, ") ") 40 | } 41 | } -------------------------------------------------------------------------------- /src/cc/redpen/intellij/SettingsManager.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.openapi.options.SearchableConfigurable 5 | import com.intellij.openapi.project.Project 6 | import org.jetbrains.annotations.Nls 7 | import javax.swing.JComponent 8 | 9 | open class SettingsManager(val project: Project) : SearchableConfigurable { 10 | internal var provider = RedPenProvider.forProject(project) 11 | internal var settingsPane = SettingsPane(provider) 12 | 13 | override fun getId(): String { 14 | return helpTopic 15 | } 16 | 17 | override fun enableSearch(s: String): Runnable? { 18 | return null 19 | } 20 | 21 | @Nls override fun getDisplayName(): String { 22 | return "RedPen" 23 | } 24 | 25 | override fun getHelpTopic(): String { 26 | return "reference.settings.ide.settings.redpen" 27 | } 28 | 29 | override fun createComponent(): JComponent { 30 | return settingsPane.createPane() 31 | } 32 | 33 | override fun isModified(): Boolean { 34 | settingsPane.applyChanges() 35 | return provider.configs != settingsPane.configs 36 | } 37 | 38 | override fun apply() { 39 | provider.activeConfig = settingsPane.config 40 | settingsPane.save() 41 | restartInspections() 42 | } 43 | 44 | override fun reset() { 45 | settingsPane.resetChanges() 46 | } 47 | 48 | override fun disposeUIResources() { 49 | } 50 | 51 | open fun restartInspections() { 52 | DaemonCodeAnalyzer.getInstance(project).restart() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/RedPenListErrorsTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.intellij.fixes.RemoveQuickFix 4 | import cc.redpen.parser.DocumentParser 5 | import com.intellij.codeInspection.InspectionManager 6 | import com.intellij.codeInspection.LocalQuickFix 7 | import com.intellij.codeInspection.ProblemHighlightType 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.LangDataKeys 10 | import com.intellij.openapi.actionSystem.PlatformDataKeys 11 | import com.intellij.openapi.util.TextRange 12 | import com.nhaarman.mockito_kotlin.* 13 | import org.junit.Test 14 | 15 | import org.junit.Assert.* 16 | import java.util.* 17 | 18 | class RedPenListErrorsTest : BaseTest() { 19 | internal val redPenListErrors = spy(RedPenListErrors()) 20 | 21 | @Test 22 | fun actionPerformed() { 23 | val file = mockTextFile("Hello\nworld!") 24 | val doc = redPen.parse(DocumentParser.PLAIN, "Hello\nworld!") 25 | whenever(file.name).thenReturn("foo.txt") 26 | whenever(redPen.validate(doc)).thenReturn(Arrays.asList(ErrorGenerator.at(0, 3), ErrorGenerator.at(3, 5))) 27 | 28 | val event = mock() 29 | whenever(event.getData(PlatformDataKeys.PROJECT)).thenReturn(project) 30 | whenever(event.getData(LangDataKeys.PSI_FILE)).thenReturn(file) 31 | 32 | doNothing().whenever(redPenListErrors).showMessage(any(), any(), any()) 33 | 34 | redPenListErrors.actionPerformed(event) 35 | 36 | verify(redPenListErrors).showMessage(project, "foo.txt", "1:0-3 Hello (ErrorGenerator)\n1:3-5 Hello (ErrorGenerator)") 37 | } 38 | } -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/QuickFixCompanionTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.intellij.BaseTest 5 | import cc.redpen.model.Sentence 6 | import cc.redpen.parser.LineOffset 7 | import cc.redpen.validator.ValidationError 8 | import com.intellij.openapi.util.TextRange 9 | import com.nhaarman.mockito_kotlin.* 10 | import org.junit.Assert.* 11 | import org.junit.Test 12 | import java.util.* 13 | 14 | class QuickFixCompanionTest() : BaseTest() { 15 | @Test 16 | fun sentenceLevelErrorHaveNoQuickFixes() { 17 | val error = ErrorGenerator.sentence(Sentence("Too long sentence", 1)) 18 | assertNull(BaseQuickFix.forValidator(error, mock(), "full text", TextRange(0, 0))) 19 | } 20 | 21 | @Test 22 | fun removeQuickFixByDefault() { 23 | val error = ErrorGenerator.at(5, 9) 24 | val quickFix = BaseQuickFix.forValidator(error, mock(), "full text", TextRange(5, 9)) 25 | assertTrue(quickFix is RemoveQuickFix) 26 | assertEquals("text", quickFix?.text) 27 | } 28 | 29 | @Test 30 | fun validatorSpecificQuickFix() { 31 | val error = spy(ErrorGenerator.at(5, 9)) 32 | doReturn("Hyphenation").whenever(error).validatorName 33 | val quickFix = BaseQuickFix.forValidator(error, mock(), "full text", TextRange(5, 9)) 34 | assertTrue(quickFix is HyphenateQuickFix) 35 | } 36 | 37 | @Test 38 | fun supportedSentenceLevelQuickFix() { 39 | val error = spy(ErrorGenerator.sentence(Sentence("Too long sentence", 1))) 40 | doReturn("ParagraphStartWith").whenever(error).validatorName 41 | val quickFix = BaseQuickFix.forValidator(error, mock(), "full text", TextRange(5, 9)) 42 | assertTrue(quickFix is ParagraphStartWithQuickFix) 43 | } 44 | } -------------------------------------------------------------------------------- /redpen-intellij-plugin.iml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/ParagraphStartWithQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.config.ValidatorConfiguration 5 | import com.intellij.openapi.util.TextRange 6 | import com.nhaarman.mockito_kotlin.capture 7 | import com.nhaarman.mockito_kotlin.eq 8 | import com.nhaarman.mockito_kotlin.verify 9 | import com.nhaarman.mockito_kotlin.whenever 10 | import org.junit.Assert.* 11 | import org.junit.Test 12 | 13 | class ParagraphStartWithQuickFixTest : BaseQuickFixTest(ParagraphStartWithQuickFix(Configuration.builder() 14 | .addValidatorConfig(ValidatorConfiguration("ParagraphStartWith").addProperty("start_from", " ")).build(), 15 | "mega can do it", TextRange(0, 1))) { 16 | 17 | @Test 18 | fun name() { 19 | assertEquals("Add paragraph prefix ", quickFix.name) 20 | } 21 | 22 | @Test 23 | fun applyFixNoPrefix() { 24 | (quickFix as ParagraphStartWithQuickFix).let { 25 | whenever(document.text).thenReturn(it.fullText) 26 | whenever(problem.textRangeInElement).thenReturn(it.range) 27 | } 28 | quickFix.applyFix(project, problem) 29 | 30 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 31 | verify(document).replaceString(0, 0, " ") 32 | } 33 | 34 | @Test 35 | fun applyFixWrongPrefix() { 36 | (quickFix as ParagraphStartWithQuickFix).let { 37 | it.fullText = " mega can do it"; 38 | whenever(document.text).thenReturn(it.fullText) 39 | whenever(problem.textRangeInElement).thenReturn(it.range) 40 | } 41 | quickFix.applyFix(project, problem) 42 | 43 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 44 | verify(document).replaceString(0, 2, " ") 45 | } 46 | } -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/NumberFormatQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.config.ValidatorConfiguration 5 | import com.intellij.openapi.util.TextRange 6 | import com.nhaarman.mockito_kotlin.capture 7 | import com.nhaarman.mockito_kotlin.eq 8 | import com.nhaarman.mockito_kotlin.verify 9 | import com.nhaarman.mockito_kotlin.whenever 10 | import org.junit.Assert.assertEquals 11 | import org.junit.Test 12 | 13 | class NumberFormatQuickFixTest : BaseQuickFixTest(NumberFormatQuickFix(createConfig("en"), "7000000,50")) { 14 | companion object { 15 | fun createConfig(lang: String) = Configuration.builder(lang).addValidatorConfig(ValidatorConfiguration("NumberFormat")).build() 16 | } 17 | 18 | @Test 19 | fun name() { 20 | assertEquals("Change to 7,000,000.50", quickFix.name) 21 | } 22 | 23 | @Test 24 | fun applyFixForUS() { 25 | whenever(document.text).thenReturn("Amount: $7000000.50") 26 | whenever(problem.textRangeInElement).thenReturn(TextRange(9, 19)) 27 | 28 | quickFix.applyFix(project, problem) 29 | 30 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 31 | verify(document).replaceString(9, 19, "7,000,000.50") 32 | } 33 | 34 | @Test 35 | fun applyFixForUK() { 36 | (quickFix as NumberFormatQuickFix).config.validatorConfigs[0].properties["decimal_delimiter_is_comma"] = "true" 37 | 38 | whenever(document.text).thenReturn("Amount: £7000000.50") 39 | whenever(problem.textRangeInElement).thenReturn(TextRange(9, 19)) 40 | 41 | quickFix.applyFix(project, problem) 42 | 43 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 44 | verify(document).replaceString(9, 19, "7.000.000,50") 45 | } 46 | 47 | @Test 48 | fun applyFixForJapaneseZenkaku() { 49 | (quickFix as NumberFormatQuickFix).config = createConfig("ja") 50 | 51 | whenever(document.text).thenReturn("7000000.50元") 52 | whenever(problem.textRangeInElement).thenReturn(TextRange(0, 10)) 53 | 54 | quickFix.applyFix(project, problem) 55 | 56 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 57 | verify(document).replaceString(0, 10, "7.000.000・50") 58 | } 59 | } 60 | 61 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/RedPenListErrors.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.RedPen 4 | import cc.redpen.model.Sentence 5 | import cc.redpen.parser.LineOffset 6 | import cc.redpen.validator.ValidationError 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.LangDataKeys 10 | import com.intellij.openapi.actionSystem.PlatformDataKeys 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.ui.Messages 13 | import com.intellij.openapi.ui.Messages.showMessageDialog 14 | import java.util.* 15 | 16 | open class RedPenListErrors : AnAction() { 17 | override fun actionPerformed(event: AnActionEvent) { 18 | val project = event.getData(PlatformDataKeys.PROJECT)!! 19 | val provider = RedPenProvider.forProject(project) 20 | val file = event.getData(LangDataKeys.PSI_FILE) 21 | val title = "RedPen " + RedPen.VERSION 22 | if (file == null) { 23 | showMessage(project, title, "No file currently active") 24 | return 25 | } 26 | 27 | try { 28 | val redPen = provider.getRedPenFor(file) 29 | val text = file.text 30 | val redPenDoc = redPen.parse(provider.getParser(file), text) 31 | val errors = redPen.validate(redPenDoc) 32 | 33 | val message = errors.map { e -> 34 | getLineNumber(e) to getLineNumber(e).toString() + ":" + getOffset(e.startPosition, e.sentence) + "-" + 35 | getOffset(e.endPosition, e.sentence) + " " + e.message + " (" + e.validatorName + ")" 36 | }.sortedBy { it.component1() }.map { it.component2() }.joinToString("\n") 37 | 38 | showMessage(project, file.name, message) 39 | } catch (e: Exception) { 40 | showMessage(project, title, e.toString()) 41 | } 42 | } 43 | 44 | open internal fun showMessage(project: Project, title: String, text: String) { 45 | showMessageDialog(project, text, title, Messages.getInformationIcon()) 46 | } 47 | 48 | private fun getLineNumber(e: ValidationError): Int { 49 | return e.startPosition.orElse(e.sentence.getOffset(0).orElse(null))?.lineNum ?: 0 50 | } 51 | 52 | private fun getOffset(lineOffset: Optional, sentence: Sentence): Int { 53 | return lineOffset.orElse(sentence.getOffset(0).orElse(null))?.offset ?: 0 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/SettingsManagerTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.config.ValidatorConfiguration 4 | import com.intellij.openapi.project.ProjectManager 5 | import com.nhaarman.mockito_kotlin.mock 6 | import com.nhaarman.mockito_kotlin.spy 7 | import com.nhaarman.mockito_kotlin.verify 8 | import com.nhaarman.mockito_kotlin.whenever 9 | import org.junit.Assert.assertFalse 10 | import org.junit.Assert.assertTrue 11 | import org.junit.Before 12 | import org.junit.Test 13 | import org.mockito.Mockito.RETURNS_DEEP_STUBS 14 | import org.mockito.Mockito.doNothing 15 | import java.util.* 16 | 17 | class SettingsManagerTest : BaseTest() { 18 | val config = config("en") 19 | val manager: SettingsManager 20 | 21 | init { 22 | val projectManager = mock(RETURNS_DEEP_STUBS) 23 | whenever(application.getComponent(ProjectManager::class.java)).thenReturn(projectManager) 24 | manager = spy(SettingsManager(project)) 25 | } 26 | 27 | @Before 28 | fun setUp() { 29 | doNothing().whenever(manager).restartInspections() 30 | manager.provider = mock(RETURNS_DEEP_STUBS) 31 | manager.settingsPane = mock() 32 | whenever(manager.settingsPane.config).thenReturn(config); 33 | } 34 | 35 | @Test 36 | fun applyConfigSwitch() { 37 | manager.apply() 38 | verify(manager.provider).activeConfig = config; 39 | } 40 | 41 | @Test 42 | fun applyValidatorsAndSymbols() { 43 | manager.apply() 44 | verify(manager.settingsPane).save() 45 | verify(manager).restartInspections() 46 | } 47 | 48 | @Test 49 | fun reset() { 50 | manager.reset() 51 | verify(manager.settingsPane).resetChanges() 52 | } 53 | 54 | @Test 55 | fun isNotModified() { 56 | val configs = manager.provider.configs 57 | whenever(manager.settingsPane.configs).thenReturn(configs) 58 | assertFalse(manager.isModified) 59 | verify(manager.settingsPane).applyChanges() 60 | } 61 | 62 | @Test 63 | fun isModified() { 64 | whenever(manager.settingsPane.configs).thenReturn(hashMapOf()) 65 | assertTrue(manager.isModified) 66 | verify(manager.settingsPane).applyChanges() 67 | } 68 | 69 | @Test 70 | fun isModified_validatorProperty() { 71 | val config = configWithValidators(listOf(ValidatorConfiguration("blah"))) 72 | val configs = hashMapOf("en" to config.clone()) 73 | configs["en"]!!.validatorConfigs[0].properties["blah"] = "blah"; 74 | whenever(manager.settingsPane.configs).thenReturn(configs) 75 | whenever(manager.provider.configs).thenReturn(hashMapOf("en" to config)) 76 | assertTrue(manager.isModified) 77 | verify(manager.settingsPane).applyChanges() 78 | } 79 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/fixes/BaseQuickFix.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.validator.ValidationError 5 | import com.intellij.codeInspection.LocalQuickFix 6 | import com.intellij.codeInspection.ProblemDescriptor 7 | import com.intellij.openapi.application.Result 8 | import com.intellij.openapi.command.WriteCommandAction 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.util.TextRange 11 | import com.intellij.psi.PsiDocumentManager 12 | import com.intellij.psi.PsiElement 13 | 14 | abstract class BaseQuickFix(var text: String) : LocalQuickFix { 15 | 16 | override fun getFamilyName() = "RedPen" 17 | 18 | open fun containingDocument(psiElement: PsiElement) = PsiDocumentManager.getInstance(psiElement.project).getDocument(psiElement.containingFile)!! 19 | 20 | override fun getName() = "Change to " + fixedText() 21 | 22 | abstract protected fun fixedText(): String 23 | open protected fun getEnd(problem: ProblemDescriptor) = problem.textRangeInElement.endOffset 24 | open protected fun getStart(problem: ProblemDescriptor) = problem.textRangeInElement.startOffset 25 | 26 | override fun applyFix(project: Project, problem: ProblemDescriptor) { 27 | val document = containingDocument(problem.psiElement) 28 | writeAction(project) { 29 | document.replaceString(getStart(problem), getEnd(problem), fixedText()) 30 | } 31 | } 32 | 33 | open internal fun writeAction(project: Project, runnable: () -> Unit) { 34 | object : WriteCommandAction(project) { 35 | override fun run(result: Result) = runnable.invoke() 36 | }.execute() 37 | } 38 | 39 | override fun equals(other: Any?) = other?.javaClass == javaClass && text == (other as RemoveQuickFix).text 40 | override fun hashCode() = text.hashCode() 41 | override fun toString() = javaClass.simpleName + "[" + text + "]" 42 | 43 | companion object { 44 | fun forValidator(error: ValidationError, config: Configuration, fullText: String, range: TextRange): BaseQuickFix? { 45 | val text = fullText.substring(range.startOffset, range.endOffset) 46 | return when (error.validatorName) { 47 | "Hyphenation" -> HyphenateQuickFix(text) 48 | "InvalidSymbol" -> InvalidSymbolQuickFix(config, fullText, range) 49 | "SymbolWithSpace" -> SymbolWithSpaceQuickFix(config, text) 50 | "StartWithCapitalLetter" -> StartWithCapitalLetterQuickFix(text) 51 | "NumberFormat" -> NumberFormatQuickFix(config, text) 52 | "SpaceBeginningOfSentence" -> SpaceBeginningOfSentenceQuickFix(text) 53 | "EndOfSentence" -> EndOfSentenceQuickFix(text) 54 | "SuggestExpression" -> SuggestExpressionQuickFix(text, error.message) 55 | "ParagraphStartWith" -> ParagraphStartWithQuickFix(config, fullText, range) 56 | else -> if (isSentenceLevelError(error)) return null else RemoveQuickFix(text) 57 | } 58 | } 59 | 60 | private fun isSentenceLevelError(error: ValidationError) = !error.startPosition.isPresent 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/fixes/InvalidSymbolQuickFixTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij.fixes 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.config.Symbol 5 | import cc.redpen.config.SymbolType 6 | import cc.redpen.config.SymbolType.* 7 | import com.intellij.openapi.util.TextRange 8 | import com.nhaarman.mockito_kotlin.capture 9 | import com.nhaarman.mockito_kotlin.eq 10 | import com.nhaarman.mockito_kotlin.verify 11 | import com.nhaarman.mockito_kotlin.whenever 12 | import org.junit.Assert.assertEquals 13 | import org.junit.Test 14 | import org.mockito.stubbing.OngoingStubbing 15 | 16 | class InvalidSymbolQuickFixTest : BaseQuickFixTest(InvalidSymbolQuickFix(Configuration.builder("en").build(), "OK!", TextRange(2, 3))) { 17 | @Test 18 | fun name() { 19 | assertEquals("Change to !", quickFix.name) 20 | } 21 | 22 | @Test 23 | fun applyFix() { 24 | whenever(document.text).thenReturn("OK!") 25 | whenever(problem.textRangeInElement).thenReturn(TextRange(2, 3)) 26 | 27 | quickFix.applyFix(project, problem) 28 | 29 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 30 | verify(document).replaceString(2, 3, "!") 31 | } 32 | 33 | private val leftQuote = Symbol(LEFT_DOUBLE_QUOTATION_MARK, '«', "\"", true, false) 34 | private val rightQuote = Symbol(RIGHT_DOUBLE_QUOTATION_MARK, '»', "\"", false, true) 35 | 36 | @Test 37 | fun lookAtSpaceAfterForCorrectSuggestion() { 38 | mockText("test\" ", TextRange(4, 5), leftQuote, rightQuote) 39 | 40 | quickFix.applyFix(project, problem) 41 | 42 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 43 | verify(document).replaceString(4, 5, "»") 44 | } 45 | 46 | @Test 47 | fun lookAtEndOfWordAfterForCorrectSuggestion() { 48 | mockText("test\"!", TextRange(4, 5), leftQuote, rightQuote) 49 | 50 | quickFix.applyFix(project, problem) 51 | 52 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 53 | verify(document).replaceString(4, 5, "»") 54 | } 55 | 56 | @Test 57 | fun lookAtEndOfLineForCorrectSuggestion() { 58 | mockText("test\"", TextRange(4, 5), leftQuote, rightQuote) 59 | 60 | quickFix.applyFix(project, problem) 61 | 62 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 63 | verify(document).replaceString(4, 5, "»") 64 | } 65 | 66 | @Test 67 | fun lookAtSpaceBeforeForCorrectSuggestion() { 68 | mockText(" \"test", TextRange(1, 2), rightQuote, leftQuote) 69 | quickFix.applyFix(project, problem) 70 | 71 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 72 | verify(document).replaceString(1, 2, "«") 73 | } 74 | 75 | @Test 76 | fun lookAtBeginningOfLineForCorrectSuggestion() { 77 | mockText("\"test", TextRange(0, 1), rightQuote, leftQuote) 78 | 79 | quickFix.applyFix(project, problem) 80 | 81 | verify(quickFix).writeAction(eq(project), capture { it.invoke() }) 82 | verify(document).replaceString(0, 1, "«") 83 | } 84 | 85 | private fun mockText(fullText: String, range: TextRange, vararg symbols: Symbol): OngoingStubbing? { 86 | (quickFix as InvalidSymbolQuickFix).let { 87 | symbols.forEach { s -> it.symbolTable.overrideSymbol(s) } 88 | it.text = "\""; 89 | it.fullText = fullText 90 | it.range = range 91 | whenever(document.text).thenReturn(it.fullText) 92 | return whenever(problem.textRangeInElement).thenReturn(it.range) 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedPen plugin for Intellij IDEA and other JetBrains IDEs 2 | 3 | ## About 4 | 5 | This plugin integrates [RedPen](http://redpen.cc) text validation into IDEA and other Intellij products by adding a new RedPen inspection. 6 | 7 | ![editor](screenshots/editor.png "Intellij IDEA Editor with RedPen inspection") 8 | 9 | ### Features 10 | 11 | * Validates text with RedPen as you type 12 | * Supports Plain Text, Properties, Markdown and AsciiDoc file formats (make sure the relevant plugins are also installed) 13 | * Some validation errors can be fixed via quick fix (*Alt+Enter*) 14 | * Validation error messages can also be listed by pressing *Ctrl+Alt+Shift+R* or via menu *Analyze -> RedPen: List Errors*. 15 | * RedPen configuration can be modified in Settings -> Editor -> RedPen 16 | * Supports all default RedPen languages and variants (English, Japanese) 17 | * Language and variant are autodetected for each file and can be manually overridden per file via status bar widget 18 | * Settings are stored per project under *.idea/redpen* directory, so can be shared with fellow developers 19 | * Custom dictionaries can be put to *.idea/redpen* directory and JavaScriptValidator scripts can be put to *.idea/redpen/js*. 20 | 21 | ## Installation 22 | 23 | The plugin is available in the official [JetBrains Plugin Repository](https://plugins.jetbrains.com/plugin/8210). 24 | 25 | Open *Settings -> Plugins -> Browse Repository*, and search for "RedPen". 26 | 27 | ## For developers [![Build Status](https://travis-ci.org/redpen-cc/redpen-intellij-plugin.svg?branch=master)](https://travis-ci.org/redpen-cc/redpen-intellij-plugin) 28 | 29 | ### Setup Intellij IDEA 30 | The steps you need to perform to run/debug the project: 31 | 32 | 1. **Fetch dependencies**. For the project to compile you need to fetch dependencies into *lib* directory: ```ant deps``` 33 | 34 | 2. **Setup Intellij Platform SDK.** Open this directory as a project in IntelliJ IDEA and setup *Intellij Platform SDK* for the project via 35 | *Project Structure -> Project Settings -> Project -> Project SDK*. If valid Intellij IDEA SDK is missing from the list, 36 | then press *New... -> Intellij Platform Plugin SDK*, choose IDEA installation path and Java version 1.8. 37 | Please name the SDK as **IntelliJ IDEA SDK** to keep project files unmodified. 38 | 39 | ![invalid SDK](screenshots/invalid_sdk.png "Invalid Intellij IDEA SDK") 40 | 41 | 3. **Run configuration "Plugin".** 42 | New instance of IDEA will start in a sandbox with the plugin activated, where you can create a dummy project for testing 43 | or open any existing project, which contains plain text or other supported files. Note: if you are testing 44 | other types of files that need additional plugins (e.g. markdown), then install the corresponding plugin again 45 | in the sandboxed IDEA. 46 | 47 | ### Command-line 48 | To build the plugin on command-line you will still need libs from a copy of Intellij IDEA. 49 | 50 | You can download the necessary files from the latest IDEA Community Edition into *idea* subdirectory: 51 | 52 | ```ant download-idea``` 53 | 54 | Then you can buld the plugin and run the unit tests: 55 | 56 | ```ant all``` 57 | 58 | To publish the current build to *JetBrains Plugin Repository*, set environment variables *$JETBRAINS_USER* and *$JETBRAINS_PWD*, then: 59 | 60 | ```ant publish``` 61 | 62 | Publishing is done by [Travis](https://travis-ci.org/redpen-cc/redpen-intellij-plugin) on every successful build. 63 | The repository normally accepts new releases only if plugin version in *META-INF/plugin.xml* has changed. 64 | 65 | Please update *META-INF/plugin.xml* with the new version and change-notes for every release. 66 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/RedPenInspection.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.RedPen 4 | import cc.redpen.intellij.fixes.BaseQuickFix 5 | import cc.redpen.parser.LineOffset 6 | import cc.redpen.validator.ValidationError 7 | import com.intellij.codeHighlighting.HighlightDisplayLevel 8 | import com.intellij.codeInsight.daemon.GroupNames 9 | import com.intellij.codeInspection.InspectionManager 10 | import com.intellij.codeInspection.LocalInspectionTool 11 | import com.intellij.codeInspection.ProblemDescriptor 12 | import com.intellij.codeInspection.ProblemHighlightType.GENERIC_ERROR_OR_WARNING 13 | import com.intellij.openapi.diagnostic.Logger 14 | import com.intellij.openapi.util.TextRange 15 | import com.intellij.psi.PsiFile 16 | import com.intellij.testFramework.LightVirtualFile 17 | import com.intellij.util.xmlb.SerializationFilter 18 | 19 | open class RedPenInspection : LocalInspectionTool() { 20 | override fun getShortName() = "RedPen" 21 | override fun getDisplayName() = "RedPen Validation" 22 | override fun getGroupDisplayName() = GroupNames.STYLE_GROUP_NAME 23 | override fun isEnabledByDefault() = true 24 | override fun getDefaultLevel() = HighlightDisplayLevel.ERROR 25 | override fun getStaticDescription() = 26 | "Validates text with RedPen, a proofreading tool.\nConfigure specific validators in Settings -> Editor -> RedPen." 27 | 28 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { 29 | if (file.virtualFile is LightVirtualFile) return null 30 | if (file.children.isEmpty()) return null 31 | 32 | val provider = RedPenProvider.forProject(file.project) 33 | val parser = provider.getParser(file) ?: return null 34 | 35 | val redPen = provider.getRedPenFor(file) 36 | 37 | updateStatus(file, redPen) 38 | 39 | val text = file.text 40 | val redPenDoc = redPen.parse(parser, text) 41 | val errors = redPen.validate(redPenDoc) 42 | 43 | val element = file.children[0] 44 | val lines = text.split("(?<=\n)".toRegex()) 45 | 46 | return errors.map { e -> 47 | try { 48 | val range = toRange(e, lines) 49 | manager.createProblemDescriptor(element, range, 50 | e.message + " (" + e.validatorName + ")", GENERIC_ERROR_OR_WARNING, isOnTheFly, 51 | BaseQuickFix.forValidator(e, redPen.configuration, text, range)) 52 | } catch (ex: Exception) { 53 | Logger.getInstance(javaClass.name).warn(e.message + ": " + ex.toString()); 54 | null 55 | } 56 | }.filterNotNull().toTypedArray() 57 | } 58 | 59 | open fun updateStatus(file: PsiFile, redPen: RedPen) { 60 | StatusWidget.forProject(file.project).update(redPen.configuration.key) 61 | } 62 | 63 | internal open fun toRange(e: ValidationError, lines: List): TextRange { 64 | val start = e.startPosition.orElse(e.sentence.getOffset(0).orElse(null)) 65 | val end = e.endPosition.orElse(addOne(e.sentence.getOffset(0).orElse(null))) 66 | return TextRange(toGlobalOffset(start, lines), toGlobalOffset(end, lines)) 67 | } 68 | 69 | private fun addOne(lineOffset: LineOffset): LineOffset { 70 | return LineOffset(lineOffset.lineNum, lineOffset.offset + 1) 71 | } 72 | 73 | internal fun toGlobalOffset(lineOffset: LineOffset?, lines: List): Int { 74 | if (lineOffset == null) return 0 75 | var result = 0 76 | for (i in 1..lineOffset.lineNum - 1) { 77 | result += lines[i - 1].length 78 | } 79 | return result + lineOffset.offset 80 | } 81 | 82 | public override fun getSerializationFilter() = SerializationFilter { a, o -> false } 83 | } 84 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/StatusWidgetTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.openapi.actionSystem.ActionManager 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.LangDataKeys 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.fileEditor.FileEditorManagerEvent 9 | import com.intellij.openapi.vfs.VirtualFile 10 | import com.intellij.openapi.wm.StatusBar 11 | import com.intellij.psi.PsiFile 12 | import com.intellij.psi.PsiManager 13 | import com.nhaarman.mockito_kotlin.* 14 | import org.junit.Assert.assertEquals 15 | import org.junit.Test 16 | import org.mockito.Mockito.RETURNS_DEEP_STUBS 17 | 18 | class StatusWidgetTest : BaseTest() { 19 | val psiManager = mock(RETURNS_DEEP_STUBS) 20 | val widget: StatusWidget 21 | val newFile = mock() 22 | 23 | init { 24 | whenever(project.basePath).thenReturn("/foo/bar") 25 | whenever(project.getComponent(PsiManager::class.java)).thenReturn(psiManager) 26 | whenever(application.invokeLater(any())).thenAnswer { (it.arguments[0] as Runnable).run() } 27 | widget = StatusWidget(project) 28 | } 29 | 30 | @Test 31 | fun selectionChanged() { 32 | val psiFile = mockTextFile("hello") 33 | whenever(psiManager.findFile(newFile)).thenReturn(psiFile) 34 | whenever(provider.getConfigKeyFor(psiFile)).thenReturn("ja") 35 | 36 | widget.selectionChanged(FileEditorManagerEvent(mock(), mock(), mock(), newFile, mock())) 37 | assertEquals("ja", widget.component.text) 38 | } 39 | 40 | @Test 41 | fun selectionChanged_noParser() { 42 | whenever(provider.getParser(psiManager.findFile(newFile)!!)).thenReturn(null) 43 | widget.selectionChanged(FileEditorManagerEvent(mock(), mock(), mock(), newFile, mock())) 44 | assertEquals("n/a", widget.component.text) 45 | } 46 | 47 | @Test 48 | fun selectionChanged_noFile() { 49 | widget.selectionChanged(FileEditorManagerEvent(mock(), mock(), mock(), null, mock())) 50 | assertEquals("n/a", widget.component.text) 51 | } 52 | 53 | @Test 54 | fun remembersManuallySelectedFile() { 55 | val event = mock(RETURNS_DEEP_STUBS) 56 | val file = mock(RETURNS_DEEP_STUBS) 57 | val config = config("en") 58 | val codeAnalyzer = mock() 59 | val actionManager = mock() 60 | whenever(ApplicationManager.getApplication().getComponent(ActionManager::class.java)).thenReturn(actionManager) 61 | whenever(event.getData(LangDataKeys.PSI_FILE)).thenReturn(file) 62 | whenever(event.project!!.getComponent(DaemonCodeAnalyzer::class.java)).thenReturn(codeAnalyzer) 63 | whenever(provider.configs).thenReturn(hashMapOf("en" to config)) 64 | whenever(project.basePath).thenReturn("/foo/bar") 65 | 66 | widget.registerActions() 67 | widget.actionGroup!!.childActionsOrStubs[0].actionPerformed(event) 68 | 69 | verify(provider).setConfigFor(file, "en") 70 | verify(codeAnalyzer).restart() 71 | verify(actionManager).registerAction("RedPen /foo/bar", widget.actionGroup!!) 72 | } 73 | 74 | @Test 75 | fun actionIsUnregisteredOnProjectClose() { 76 | val actionManager = mock() 77 | whenever(ApplicationManager.getApplication().getComponent(ActionManager::class.java)).thenReturn(actionManager) 78 | 79 | val statusBar = mock() 80 | widget.install(statusBar) 81 | 82 | widget.projectClosed() 83 | 84 | val order = inOrder(statusBar, actionManager) 85 | order.verify(statusBar).removeWidget(widget.ID()) 86 | order.verify(actionManager).unregisterAction("RedPen /foo/bar") 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.RedPen 4 | import cc.redpen.config.Configuration 5 | import cc.redpen.config.Symbol 6 | import cc.redpen.config.ValidatorConfiguration 7 | import cc.redpen.model.Sentence 8 | import cc.redpen.validator.ValidationError 9 | import cc.redpen.validator.document.WordFrequencyValidator 10 | import com.intellij.openapi.application.Application 11 | import com.intellij.openapi.application.ApplicationManager 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.psi.PsiFile 14 | import com.nhaarman.mockito_kotlin.any 15 | import com.nhaarman.mockito_kotlin.mock 16 | import com.nhaarman.mockito_kotlin.whenever 17 | import org.junit.BeforeClass 18 | import org.mockito.Mockito.RETURNS_DEEP_STUBS 19 | import java.util.* 20 | 21 | abstract class BaseTest { 22 | val project = mock(RETURNS_DEEP_STUBS) 23 | val redPen: RedPen = mock(RETURNS_DEEP_STUBS) 24 | var provider: RedPenProvider = mock(RETURNS_DEEP_STUBS) 25 | var statusWidget: StatusWidget = mock(RETURNS_DEEP_STUBS) 26 | 27 | companion object { 28 | val application = mock(RETURNS_DEEP_STUBS) 29 | 30 | @BeforeClass @JvmStatic 31 | fun initStatics() { 32 | ApplicationManager.setApplication(application, mock()) 33 | } 34 | } 35 | 36 | init { 37 | whenever(project.getComponent(RedPenProvider::class.java)).thenReturn(provider) 38 | whenever(project.getComponent(StatusWidget::class.java)).thenReturn(statusWidget) 39 | whenever(provider.getRedPen()).thenReturn(redPen) 40 | whenever(provider.getRedPenFor(any())).thenReturn(redPen) 41 | whenever(provider.getParser(any())).thenCallRealMethod() 42 | whenever(redPen.configuration.key).thenReturn("en") 43 | } 44 | 45 | protected fun configWithValidators(validatorConfigs: List): Configuration { 46 | val builder = Configuration.builder() 47 | validatorConfigs.forEach { builder.addValidatorConfig(it) } 48 | return builder.build() 49 | } 50 | 51 | protected fun configWithSymbols(symbols: List): Configuration { 52 | val builder = Configuration.builder() 53 | symbols.forEach { builder.addSymbol(it) } 54 | return builder.build() 55 | } 56 | 57 | protected fun cloneableConfig(key: String): Configuration { 58 | val config = config(key) 59 | val configClone = config(key) 60 | whenever(config.clone()).thenReturn(configClone) 61 | whenever(configClone.clone()).thenReturn(configClone) 62 | return config 63 | } 64 | 65 | protected fun config(key: String): Configuration { 66 | val config = mock() 67 | whenever(config.key).thenReturn(key) 68 | return config 69 | } 70 | 71 | protected fun mockTextFile(text: String): PsiFile { 72 | return mockFileOfType("PLAIN_TEXT", "txt", text) 73 | } 74 | 75 | protected open fun mockFileOfType(typeName: String, extension: String, text: String): PsiFile { 76 | val file = mock(RETURNS_DEEP_STUBS) 77 | whenever(file.text).thenReturn(text) 78 | whenever(file.children).thenReturn(arrayOf(mock())) 79 | whenever(file.virtualFile.path).thenReturn("/path") 80 | whenever(file.project).thenReturn(project) 81 | whenever(file.fileType.name).thenReturn(typeName) 82 | whenever(file.name).thenReturn("sample." + extension) 83 | return file 84 | } 85 | 86 | object ErrorGenerator : WordFrequencyValidator() { 87 | fun at(start: Int, end: Int): ValidationError { 88 | val errors = ArrayList() 89 | setErrorList(errors) 90 | addErrorWithPosition("Hello", Sentence("Hello", 1), start, end) 91 | return errors[0] 92 | } 93 | 94 | fun sentence(sentence: Sentence): ValidationError { 95 | val errors = ArrayList() 96 | setErrorList(errors) 97 | addError("Hello", sentence) 98 | return errors[0] 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | cc.redpen.intellij 3 | RedPen Plugin 4 | 1.8.1 5 | RedPen 6 | 7 | RedPen is a proofreading tool to help writers or programmers who write technical documents or manuals that need to adhere to a writing standard.

9 |

The plugin validates text files with RedPen by adding a 'RedPen' inspection, which is enabled by default after installation.

10 |

Some errors can be fixed via quick fixes (Alt + Enter)

11 |

Language and variant are autodetected per file, however you can manually override them in IDEA status bar.

12 |

RedPen settings can be configured and imported/exported using native RedPen config format. All settings are stored per project.

13 |

Custom RedPen dictionaries can be put to .idea/redpen directory and JavaScriptValidator scripts can be put to .idea/redpen/js.

14 | 15 |

The following file types are supported (provided you have necessary plugins installed): 16 |

    17 |
  • Plain text
  • 18 |
  • Properties
  • 19 |
  • Markdown
  • 20 |
  • AsciiDoc
  • 21 |
  • Re:VIEW
  • 22 |
  • LaTeX
  • 23 |
  • reStructuredText
  • 24 |
25 | 26 |

Please report any issues on GitHub.

27 | ]]>
28 | 29 | 1.8.1 31 |
    32 |
  • Fix a failure
  • 33 |
34 | 1.8.0 35 |
    36 |
  • Upgraded to RedPen 1.9.0
  • 37 |
  • reStructuredText format support
  • 38 |
39 | 1.6.0 40 |
    41 |
  • Upgraded to RedPen 1.7.0
  • 42 |
43 | 1.5.0 44 |
    45 |
  • Added LaTeX file format support
  • 46 |
  • Added Re:VIEW file format support
  • 47 |
48 | 1.4.0 49 |
    50 |
  • Upgraded to RedPen 1.6.1
  • 51 |
52 | 1.3.2 53 |
    54 |
  • Upgraded to RedPen 1.5.5
  • 55 |
56 | 57 | 1.3.1 58 |
    59 |
  • Upgraded to RedPen 1.5.3
  • 60 |
61 | 62 | 1.3 63 |
    64 |
  • Upgraded to RedPen 1.5.2
  • 65 |
  • Added Russian language
  • 66 |
  • Added/improved quick fixes
  • 67 |
68 | 69 | 1.2 70 |
    71 |
  • Upgraded to RedPen 1.5.0
  • 72 |
  • All available RedPen validators can now be used
  • 73 |
  • All available validator properties are now shown in Settings dialog
  • 74 |
  • Added Java Properties file format support
  • 75 |
  • Added quick fixes for some validator errors
  • 76 |
77 | 78 | 1.1.1 79 |
    80 |
  • Do not support plugin for Intellij Platform builds with Kotlin version below 1.0
  • 81 |
82 | 83 | 1.1 84 |
    85 |
  • Bug fixes for Settings
  • 86 |
  • Allow modifying configuration files manually while IDEA is open
  • 87 |
88 | 89 | 1.0 90 |
    91 |
  • Save/load of configurations for non-standard languages
  • 92 |
  • Support for JavaScriptValidator
  • 93 |
  • Fixed memory leak after closing the project
  • 94 |
95 | 96 | 0.9.x 97 |
    98 |
  • Support for files opened with MultiMarkdown plugin
  • 99 |
  • A few bugfixes with Settings
  • 100 |
101 | 102 | 0.9 103 |
    104 |
  • Initial public release
  • 105 |
106 | ]]> 107 |
108 | 109 | 110 | com.intellij.modules.lang 111 | 112 | 113 | cc.redpen.intellij.RedPenProvider 114 | cc.redpen.intellij.StatusWidget 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/StatusWidget.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.icons.AllIcons 5 | import com.intellij.ide.DataManager 6 | import com.intellij.openapi.actionSystem.* 7 | import com.intellij.openapi.actionSystem.LangDataKeys.PSI_FILE 8 | import com.intellij.openapi.actionSystem.impl.SimpleDataContext 9 | import com.intellij.openapi.application.ApplicationManager 10 | import com.intellij.openapi.components.ProjectComponent 11 | import com.intellij.openapi.fileEditor.FileEditorManagerEvent 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.openapi.ui.popup.JBPopupFactory 14 | import com.intellij.openapi.util.Disposer 15 | import com.intellij.openapi.wm.CustomStatusBarWidget 16 | import com.intellij.openapi.wm.StatusBarWidget 17 | import com.intellij.openapi.wm.StatusBarWidget.WidgetBorder.WIDE 18 | import com.intellij.openapi.wm.WindowManager 19 | import com.intellij.openapi.wm.impl.status.EditorBasedWidget 20 | import com.intellij.openapi.wm.impl.status.TextPanel 21 | import com.intellij.psi.PsiManager 22 | import com.intellij.ui.ClickListener 23 | import com.intellij.ui.awt.RelativePoint 24 | import com.intellij.util.ui.UIUtil 25 | import java.awt.Component 26 | import java.awt.Graphics 27 | import java.awt.Point 28 | import java.awt.event.MouseEvent 29 | 30 | open class StatusWidget constructor(project: Project) : EditorBasedWidget(project), CustomStatusBarWidget, ProjectComponent { 31 | val provider = RedPenProvider.forProject(project) 32 | var enabled: Boolean = false 33 | val actionGroupId = "RedPen " + project.basePath 34 | 35 | companion object { 36 | fun forProject(project: Project) = project.getComponent(StatusWidget::class.java)!! 37 | } 38 | 39 | var actionGroup: DefaultActionGroup? = null 40 | private val component = object: TextPanel.ExtraSize() { 41 | override fun paintComponent(g: Graphics) { 42 | super.paintComponent(g) 43 | if (enabled && text != null) { 44 | val arrows = AllIcons.Ide.Statusbar_arrows 45 | arrows.paintIcon(this, g, bounds.width - insets.right - arrows.iconWidth - 2, 46 | bounds.height / 2 - arrows.iconHeight / 2) 47 | } 48 | } 49 | } 50 | 51 | override fun projectOpened() { 52 | install(WindowManager.getInstance().getStatusBar(project)) 53 | myStatusBar.addWidget(this, "before Encoding") 54 | 55 | object : ClickListener() { 56 | override fun onClick(e: MouseEvent, clickCount: Int): Boolean { 57 | showPopup(e) 58 | return true 59 | } 60 | }.installOn(component) 61 | component.border = WIDE 62 | component.toolTipText = "RedPen language" 63 | registerActions() 64 | } 65 | 66 | override fun projectClosed() { 67 | myStatusBar.removeWidget(ID()) 68 | unregisterActions() 69 | } 70 | 71 | override fun getComponentName(): String = "StatusWidget" 72 | 73 | override fun initComponent() {} 74 | 75 | override fun disposeComponent() {} 76 | 77 | open fun registerActions() { 78 | val actionManager = ActionManager.getInstance() ?: return 79 | actionGroup = DefaultActionGroup() 80 | provider.configs.keys.forEach { key -> 81 | actionGroup!!.add(object : AnAction() { 82 | init { templatePresentation.text = key } 83 | 84 | override fun actionPerformed(e: AnActionEvent) { 85 | provider.setConfigFor(e.getData(PSI_FILE)!!, key) 86 | DaemonCodeAnalyzer.getInstance(e.project).restart() 87 | } 88 | }) 89 | } 90 | actionManager.registerAction(actionGroupId, actionGroup!!) 91 | } 92 | 93 | open internal fun unregisterActions() { 94 | ActionManager.getInstance()?.unregisterAction(actionGroupId) 95 | } 96 | 97 | override fun ID(): String { 98 | return "RedPen" 99 | } 100 | 101 | override fun getPresentation(platformType: StatusBarWidget.PlatformType): StatusBarWidget.WidgetPresentation? { 102 | return null 103 | } 104 | 105 | open fun update(configKey: String) { 106 | if (isDisposed) return 107 | ApplicationManager.getApplication().invokeLater { 108 | component.text = configKey 109 | component.foreground = if (enabled) UIUtil.getActiveTextColor() else UIUtil.getInactiveTextColor() 110 | } 111 | } 112 | 113 | override fun selectionChanged(event: FileEditorManagerEvent) { 114 | val file = if (event.newFile == null) null else PsiManager.getInstance(project!!).findFile(event.newFile!!) 115 | if (file != null && provider.getParser(file) != null) { 116 | enabled = true 117 | update(provider.getConfigKeyFor(file)) 118 | } 119 | else { 120 | enabled = false 121 | update("n/a") 122 | } 123 | } 124 | 125 | override fun getComponent(): TextPanel { 126 | return component 127 | } 128 | 129 | internal fun showPopup(e: MouseEvent) { 130 | if (!enabled) return 131 | val popup = JBPopupFactory.getInstance().createActionGroupPopup( 132 | "RedPen", actionGroup!!, getContext(), JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, false) 133 | val dimension = popup.content.preferredSize 134 | val at = Point(0, -dimension.height) 135 | popup.show(RelativePoint(e.component, at)) 136 | Disposer.register(this, popup) 137 | } 138 | 139 | private fun getContext(): DataContext { 140 | val editor = editor 141 | val parent = DataManager.getInstance().getDataContext(myStatusBar as Component) 142 | return SimpleDataContext.getSimpleContext( 143 | CommonDataKeys.VIRTUAL_FILE_ARRAY.name, 144 | arrayOf(selectedFile!!), 145 | SimpleDataContext.getSimpleContext(CommonDataKeys.PROJECT.name, 146 | project, 147 | SimpleDataContext.getSimpleContext(PlatformDataKeys.CONTEXT_COMPONENT.name, 148 | editor?.component, parent))) 149 | } 150 | 151 | fun rebuild() { 152 | unregisterActions() 153 | registerActions() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/RedPenInspectionTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.intellij.fixes.RemoveQuickFix 4 | import cc.redpen.model.Document 5 | import cc.redpen.model.Sentence 6 | import cc.redpen.parser.DocumentParser 7 | import cc.redpen.parser.LineOffset 8 | import com.intellij.codeInspection.InspectionManager 9 | import com.intellij.codeInspection.LocalQuickFix 10 | import com.intellij.codeInspection.ProblemHighlightType.GENERIC_ERROR_OR_WARNING 11 | import com.intellij.openapi.util.TextRange 12 | import com.intellij.testFramework.LightVirtualFile 13 | import com.nhaarman.mockito_kotlin.* 14 | import org.junit.Assert.* 15 | import org.junit.Test 16 | import java.util.Arrays.asList 17 | import java.util.Collections.emptyList 18 | 19 | class RedPenInspectionTest : BaseTest() { 20 | internal var inspection = spy(RedPenInspection()) 21 | 22 | @Test 23 | fun notSupportedFilesAreIgnored() { 24 | assertNull(inspection.checkFile(mockFileOfType("JAVA", "java", ""), mock(), true)) 25 | assertNull(inspection.checkFile(mockFileOfType("XML", "xml", ""), mock(), true)) 26 | } 27 | 28 | @Test 29 | fun plainTextIsSupported() { 30 | whenever(redPen.validate(any())).thenReturn(emptyList()) 31 | inspection.checkFile(mockTextFile("Hello"), mock(), true) 32 | verify(redPen).parse(DocumentParser.PLAIN, "Hello") 33 | } 34 | 35 | @Test 36 | fun markdownIsSupported() { 37 | whenever(redPen.validate(any())).thenReturn(emptyList()) 38 | inspection.checkFile(mockFileOfType("Markdown", "md", "Hello"), mock(), true) 39 | verify(redPen).parse(DocumentParser.MARKDOWN, "Hello") 40 | } 41 | 42 | @Test 43 | fun asciiDocIsSupported() { 44 | whenever(redPen.validate(any())).thenReturn(emptyList()) 45 | inspection.checkFile(mockFileOfType("AsciiDoc", "asciidoc", "Hello"), mock(), true) 46 | verify(redPen).parse(DocumentParser.ASCIIDOC, "Hello") 47 | } 48 | 49 | @Test 50 | fun ReVIEWIsSupported() { 51 | whenever(redPen.validate(any())).thenReturn(emptyList()) 52 | inspection.checkFile(mockFileOfType("ReVIEW", "re", "Hello"), mock(), true) 53 | verify(redPen).parse(DocumentParser.REVIEW, "Hello") 54 | } 55 | 56 | @Test 57 | fun LaTeXIsSupported() { 58 | whenever(redPen.validate(any())).thenReturn(emptyList()) 59 | inspection.checkFile(mockFileOfType("LaTeX", "tex", "Hello"), mock(), true) 60 | verify(redPen).parse(DocumentParser.LATEX, "Hello") 61 | } 62 | 63 | @Test 64 | fun canParseEmptyDocument() { 65 | whenever(redPen.validate(any())).thenReturn(emptyList()) 66 | inspection.checkFile(mockTextFile(""), mock(), true) 67 | verify(redPen).parse(DocumentParser.PLAIN, "") 68 | } 69 | 70 | @Test 71 | fun toGlobalOffset_noOffset() { 72 | assertEquals(0, inspection.toGlobalOffset(null, listOf(""))) 73 | } 74 | 75 | @Test 76 | fun toGlobalOffset_singleLine() { 77 | assertEquals(3, inspection.toGlobalOffset(LineOffset(1, 3), listOf("Hello"))) 78 | } 79 | 80 | @Test 81 | fun toGlobalOffset_multiLine() { 82 | assertEquals(8, inspection.toGlobalOffset(LineOffset(2, 3), listOf("Hello", "World"))) 83 | } 84 | 85 | @Test 86 | fun toRange() { 87 | val textRange = inspection.toRange(ErrorGenerator.at(5, 5), listOf("Hello")) 88 | assertEquals(TextRange(5, 5), textRange) 89 | } 90 | 91 | @Test 92 | fun toRange_sentenceLevelError() { 93 | val sentence = Sentence("Hello.", listOf(LineOffset(1, 25)), emptyList()) 94 | val textRange = inspection.toRange(ErrorGenerator.sentence(sentence), listOf(sentence.content)) 95 | assertEquals(TextRange(25, 26), textRange) 96 | } 97 | 98 | @Test 99 | fun checkFile_convertsRedPenErrorsIntoIDEAProblemDescriptors() { 100 | val doc = redPen.parse(DocumentParser.PLAIN, "Hello") 101 | whenever(redPen.validate(doc)).thenReturn(asList(ErrorGenerator.at(0, 3), ErrorGenerator.at(3, 5))) 102 | val manager = mock() 103 | whenever(manager.createProblemDescriptor(any(), any(), any(), any(), any(), any())).thenReturn(mock()) 104 | 105 | val file = mockTextFile("Hello") 106 | val problems = inspection.checkFile(file, manager, true) 107 | assertNotNull(problems) 108 | assertEquals(2, problems?.size) 109 | 110 | verify(manager).createProblemDescriptor(file.children[0], TextRange(0, 3), "Hello (ErrorGenerator)", GENERIC_ERROR_OR_WARNING, true, RemoveQuickFix("Hel")) 111 | verify(manager).createProblemDescriptor(file.children[0], TextRange(3, 5), "Hello (ErrorGenerator)", GENERIC_ERROR_OR_WARNING, true, RemoveQuickFix("lo")) 112 | verifyNoMoreInteractions(manager); 113 | } 114 | 115 | @Test 116 | fun checkFile_skipErrorsThatFailToConvertToProblems() { 117 | val doc = redPen.parse(DocumentParser.PLAIN, "Hello") 118 | whenever(redPen.validate(doc)).thenReturn(asList(ErrorGenerator.at(0, 3))) 119 | val manager = mock() 120 | whenever(manager.createProblemDescriptor(any(), any(), any(), any(), any(), any())).thenThrow(RuntimeException()) 121 | 122 | val file = mockTextFile("Hello") 123 | assertEquals(0, inspection.checkFile(file, manager, true)?.size) 124 | } 125 | 126 | @Test 127 | fun checkFile_splitsTextIntoLinesPreservingAllCharacters() { 128 | val doc = redPen.parse(DocumentParser.PLAIN, "Hello\nworld") 129 | val error = ErrorGenerator.at(1, 2) 130 | whenever(redPen.validate(doc)).thenReturn(listOf(error)) 131 | 132 | inspection.checkFile(mockTextFile("Hello\nworld"), mock(), true) 133 | 134 | verify(inspection).toRange(eq(error), capture { 135 | assertEquals(listOf("Hello\n", "world"), it) 136 | }) 137 | } 138 | 139 | @Test 140 | fun checkFile_updatesStatusWidget() { 141 | doCallRealMethod().whenever(inspection).updateStatus(any(), any()) 142 | val file = mockTextFile("Hello") 143 | val config = config("ja") 144 | whenever(redPen.configuration).thenReturn(config) 145 | inspection.checkFile(file, mock(), true) 146 | verify(statusWidget).update("ja") 147 | } 148 | 149 | @Test 150 | fun checkFile_ignoresEditFieldsInDialogs() { 151 | val file = mockTextFile("Hello") 152 | val notReallyAFile = LightVirtualFile() 153 | whenever(file.virtualFile).thenReturn(notReallyAFile) 154 | 155 | assertNull(inspection.checkFile(file, mock(), false)) 156 | } 157 | 158 | @Test 159 | fun doNotSerializeSettings() { 160 | assertFalse(inspection.serializationFilter.accepts(mock(), mock())); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/RedPenProvider.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.RedPen 4 | import cc.redpen.config.Configuration 5 | import cc.redpen.config.ConfigurationExporter 6 | import cc.redpen.config.ConfigurationLoader 7 | import cc.redpen.parser.DocumentParser 8 | import cc.redpen.parser.DocumentParser.* 9 | import cc.redpen.util.LanguageDetector 10 | import com.intellij.openapi.components.SettingsSavingComponent 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.openapi.project.Project.DIRECTORY_STORE_FOLDER 13 | import com.intellij.openapi.util.io.FileUtil 14 | import com.intellij.psi.PsiFile 15 | import org.slf4j.LoggerFactory 16 | import java.io.File 17 | import java.io.FileInputStream 18 | import java.io.FileOutputStream 19 | import java.util.* 20 | 21 | open class RedPenProvider : SettingsSavingComponent { 22 | val project: Project 23 | var configDir: File 24 | 25 | open var initialConfigs : MutableMap = LinkedHashMap() 26 | open var configs : MutableMap = LinkedHashMap() 27 | internal val configLastModifiedTimes: MutableMap = HashMap() 28 | 29 | private var configKey = "en" 30 | internal var configKeysByFile = Properties() 31 | 32 | companion object { 33 | val parsers = mapOf( 34 | "PLAIN_TEXT" to PLAIN, 35 | "Markdown" to MARKDOWN, 36 | "MultiMarkdown" to MARKDOWN, 37 | "AsciiDoc" to ASCIIDOC, 38 | "Properties" to PROPERTIES, 39 | "ReVIEW" to REVIEW, 40 | "LaTeX" to LATEX, 41 | "ReST" to REST) 42 | 43 | val defaultConfigKeys = LinkedHashSet(Configuration.getDefaultConfigKeys()) 44 | 45 | fun forProject(project: Project) = project.getComponent(RedPenProvider::class.java) 46 | } 47 | 48 | internal constructor(project: Project) { 49 | this.project = project 50 | this.configDir = File(project.basePath + '/' + DIRECTORY_STORE_FOLDER, "redpen") 51 | availableConfigKeys().forEach { loadConfig(it) } 52 | loadConfigKeysByFile() 53 | } 54 | 55 | fun guessFileType(fileName: String?): String? { 56 | if (fileName == null) { 57 | return null; 58 | } 59 | val file = File(fileName) 60 | val extension = file.extension 61 | when (extension) { 62 | "txt" -> return "PLAIN_TEXT"; 63 | "adoc", "asciidoc" -> return "AsciiDoc" 64 | "markdown", "md" -> return "Markdown" 65 | "tex", "latex" -> return "LaTeX" 66 | "re", "review" -> return "ReVIEW" 67 | "properties" -> return "Properties" 68 | "rst", "rest" -> return "ReST" 69 | else -> return null; 70 | } 71 | } 72 | 73 | /** For tests */ 74 | internal constructor(project: Project, configs: MutableMap) { 75 | this.project = project 76 | this.configDir = File(System.getProperty("java.io.tmpdir")) 77 | this.configs = configs.map { it.key to it.value.clone() }.toMap(LinkedHashMap()) 78 | this.initialConfigs = configs.map { it.key to it.value.clone() }.toMap(LinkedHashMap()) 79 | } 80 | 81 | internal fun loadConfig(key: String) { 82 | val fileName = key + ".xml" 83 | val loader = ConfigurationLoader() 84 | try { 85 | val file = File(configDir, fileName) 86 | 87 | val initialConfig = if (key in defaultConfigKeys) createInitialConfig(key) else loader.load(file) 88 | initialConfigs[key] = initialConfig 89 | 90 | if (key in defaultConfigKeys && file.exists()) { 91 | val config = loader.load(file) 92 | configs[key] = config 93 | } else { 94 | configs[key] = initialConfig.clone() 95 | } 96 | 97 | configLastModifiedTimes[key] = file.lastModified() 98 | } 99 | catch (e: Exception) { 100 | LoggerFactory.getLogger(javaClass).warn("Failed to load " + fileName, e) 101 | } 102 | } 103 | 104 | private fun createInitialConfig(key: String) = Configuration.builder(key).setBaseDir(configDir).addAvailableValidatorConfigs().build() 105 | 106 | internal fun loadConfigKeysByFile() { 107 | val file = File(configDir, "files.xml") 108 | if (file.exists()) FileInputStream(file).use { configKeysByFile.loadFromXML(it) } 109 | } 110 | 111 | override fun save() { 112 | configDir.mkdirs() 113 | configs.values.forEach { c -> 114 | val file = File(configDir, c.key + ".xml") 115 | 116 | if (file.lastModified() > configLastModifiedTimes[c.key] ?: 0) loadConfig(c.key) 117 | else if (c.key in defaultConfigKeys && c == initialConfigs[c.key]) file.delete() 118 | else { 119 | FileOutputStream(file).use { out -> ConfigurationExporter().export(c, out) } 120 | configLastModifiedTimes[c.key] = file.lastModified() 121 | } 122 | } 123 | 124 | val file = File(configDir, "files.xml") 125 | if (configKeysByFile.isEmpty) file.delete() 126 | else FileOutputStream(file).use { out -> configKeysByFile.storeToXML(out, null) } 127 | 128 | if (configDir.list().isEmpty()) configDir.delete() 129 | } 130 | 131 | internal fun availableConfigKeys() = if (!configDir.exists()) defaultConfigKeys 132 | else defaultConfigKeys + configDir.list().filter { it != "files.xml" && it.endsWith(".xml") }.map { it.replace(".xml", "") } 133 | 134 | infix operator fun plusAssign(config: Configuration) { 135 | initialConfigs[config.key] = config.clone() 136 | configs[config.key] = config.clone() 137 | StatusWidget.forProject(project).rebuild() 138 | } 139 | 140 | open fun getRedPen(): RedPen = RedPen(configs[configKey]) 141 | 142 | open fun getRedPenFor(file: PsiFile): RedPen { 143 | configKey = getConfigKeyFor(file) 144 | return getRedPen() 145 | } 146 | 147 | open fun getConfigKeyFor(file: PsiFile) = configKeysByFile.getProperty(relativePath(file)) ?: LanguageDetector().detectLanguage(file.text) 148 | 149 | open fun getParser(file: PsiFile): DocumentParser? { 150 | val fileType = guessFileType(file.name) 151 | LoggerFactory.getLogger(javaClass).warn("Detected: " + fileType + " for " + file.name) 152 | return parsers[fileType] 153 | } 154 | 155 | open var activeConfig: Configuration 156 | get() = configs[configKey]!! 157 | set(config) { 158 | configKey = config.key 159 | } 160 | 161 | open fun setConfigFor(file: PsiFile, key: String) { 162 | configKey = key 163 | configKeysByFile[relativePath(file)] = key 164 | } 165 | 166 | internal fun relativePath(file: PsiFile) = FileUtil.getRelativePath(project.basePath!!, file.virtualFile.path, File.separatorChar) 167 | } 168 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/RedPenProviderTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.config.ConfigurationLoader 5 | import cc.redpen.config.Symbol 6 | import cc.redpen.config.SymbolType.AMPERSAND 7 | import com.intellij.psi.PsiFile 8 | import com.nhaarman.mockito_kotlin.inOrder 9 | import com.nhaarman.mockito_kotlin.mock 10 | import com.nhaarman.mockito_kotlin.whenever 11 | import org.junit.After 12 | import org.junit.Assert.* 13 | import org.junit.Before 14 | import org.junit.Test 15 | import org.mockito.Mockito.RETURNS_DEEP_STUBS 16 | import java.io.File 17 | import java.io.FileOutputStream 18 | 19 | class RedPenProviderTest : BaseTest() { 20 | val file = mockTextFile("hello") 21 | 22 | @Before 23 | fun setUp() { 24 | val basePath = File(System.getProperty("java.io.tmpdir"), "redpen-tmp-config") 25 | whenever(project.basePath).thenReturn(basePath.absolutePath) 26 | provider = RedPenProvider(project) 27 | } 28 | 29 | @After 30 | fun tearDown() { 31 | provider.configDir.deleteRecursively() 32 | } 33 | 34 | @Test 35 | fun allConfigFilesAreLoaded() { 36 | assertEquals("en", provider.configs["en"]!!.key) 37 | assertEquals("ja", provider.configs["ja"]!!.key) 38 | assertEquals("ja.hankaku", provider.configs["ja.hankaku"]!!.key) 39 | assertEquals("ja.zenkaku2", provider.configs["ja.zenkaku2"]!!.key) 40 | provider.configs.values.forEach { assertEquals(provider.configDir, it.base) } 41 | } 42 | 43 | @Test 44 | fun getRedPenFor_autodetectsLanguage() { 45 | whenever(file.text).thenReturn("Hello") 46 | var redPen = provider.getRedPenFor(file) 47 | assertEquals("en", redPen.configuration.key) 48 | 49 | whenever(file.text).thenReturn("こんにちは") 50 | redPen = provider.getRedPenFor(file) 51 | assertEquals("ja", redPen.configuration.key) 52 | } 53 | 54 | @Test 55 | fun getRedPenFor_autodetectsLanguageOnlyIfLanguageWasNotAlreadySetManually() { 56 | val file = mock(RETURNS_DEEP_STUBS) 57 | provider.configKeysByFile["path/to/foo"] = "ja" 58 | whenever(project.basePath).thenReturn("/foo") 59 | whenever(file.virtualFile.path).thenReturn("/foo/path/to/foo") 60 | 61 | var redPen = provider.getRedPenFor(file) 62 | assertEquals("ja", redPen.configuration.key) 63 | } 64 | 65 | @Test 66 | fun saveAndLoad() { 67 | provider.configs["ja"]!!.symbolTable.overrideSymbol(Symbol(AMPERSAND, '*')) 68 | provider.configKeysByFile["hello.txt"] = "ja" 69 | provider.save() 70 | 71 | assertFalse(File(provider.configDir, "en.xml").exists()) 72 | assertEquals(provider.configs["ja"], ConfigurationLoader().load(File(provider.configDir, "ja.xml"))) 73 | 74 | provider.configLastModifiedTimes["ja"] = 0 75 | provider.loadConfig("ja") 76 | assertNotEquals(0, provider.configLastModifiedTimes["ja"]) 77 | assertEquals('*', provider.configs["ja"]!!.symbolTable.getSymbol(AMPERSAND).value) 78 | assertFalse(provider.initialConfigs["ja"] == provider.configs["ja"]) 79 | assertTrue(provider.initialConfigs["en"] == provider.configs["en"]) 80 | 81 | provider.configKeysByFile.remove("hello.txt") 82 | provider.loadConfigKeysByFile() 83 | assertEquals("ja", provider.configKeysByFile["hello.txt"]) 84 | } 85 | 86 | @Test 87 | fun removeSavedConfigIfSameAsInitial() { 88 | provider.configDir.mkdirs() 89 | val enFile = File(provider.configDir, "en.xml") 90 | FileOutputStream(enFile).use { it.write("".toByteArray()) } 91 | provider.configLastModifiedTimes["en"] = enFile.lastModified() 92 | 93 | provider.save() 94 | assertFalse(enFile.exists()) 95 | assertFalse(provider.configDir.exists()) 96 | } 97 | 98 | @Test 99 | fun alwaysSaveNonDefaultConfigs() { 100 | provider.initialConfigs["za"] = Configuration.builder("za").build() 101 | provider.configs["za"] = Configuration.builder("za").build() 102 | provider.configLastModifiedTimes["za"] = 0 103 | 104 | provider.save() 105 | 106 | assertTrue(File(provider.configDir, "za.xml").exists()) 107 | } 108 | 109 | @Test 110 | fun loadConfigIfItWasModifiedManuallySinceLastSave() { 111 | provider.configDir.mkdirs() 112 | provider.configs["za"] = config("za") 113 | provider.configLastModifiedTimes["za"] = 0 114 | val za = File(provider.configDir, "za.xml") 115 | FileOutputStream(za).use { it.write("".toByteArray()) } 116 | 117 | provider.save() 118 | assertNotNull(provider.configs["za"]) 119 | } 120 | 121 | @Test 122 | fun loadJustImportedCustomConfig() { 123 | provider.configDir.mkdirs() 124 | provider.configs["za"] = config("za") 125 | val za = File(provider.configDir, "za.xml") 126 | FileOutputStream(za).use { it.write("".toByteArray()) } 127 | 128 | provider.save() 129 | assertNotNull(provider.configs["za"]) 130 | } 131 | 132 | @Test 133 | fun availableConfigKeys() { 134 | provider.configDir.mkdirs() 135 | val zaFile = File(provider.configDir, "za.xml") 136 | FileOutputStream(zaFile).use { it.write("".toByteArray()) } 137 | val filesFile = File(provider.configDir, "files.xml") 138 | FileOutputStream(filesFile).use { it.write("".toByteArray()) } 139 | val nonXmlFile = File(provider.configDir, "blah.txt") 140 | FileOutputStream(nonXmlFile).use { it.write("blah".toByteArray()) } 141 | 142 | val configKeys = provider.availableConfigKeys() 143 | assertTrue(configKeys.containsAll(RedPenProvider.defaultConfigKeys)) 144 | assertTrue("za" in configKeys) 145 | assertEquals(RedPenProvider.defaultConfigKeys.size + 1, configKeys.size) 146 | } 147 | 148 | @Test 149 | fun loadConfigForNonDefault() { 150 | provider.configDir.mkdirs() 151 | val zaFile = File(provider.configDir, "za.xml") 152 | FileOutputStream(zaFile).use { it.write("".toByteArray()) } 153 | 154 | provider.loadConfig("za") 155 | assertTrue("za" in provider.configs) 156 | assertTrue("za" in provider.initialConfigs) 157 | } 158 | 159 | @Test 160 | fun setFileConfig() { 161 | val file = mock(RETURNS_DEEP_STUBS) 162 | whenever(project.basePath).thenReturn("/foo") 163 | whenever(file.virtualFile.path).thenReturn("/foo/path/to/foo") 164 | 165 | provider.setConfigFor(file, "en") 166 | 167 | assertEquals("en", provider.configKeysByFile["path/to/foo"]) 168 | } 169 | 170 | @Test 171 | fun addingOfNewConfigRebuildsStatusWidget() { 172 | provider += cloneableConfig("za") 173 | 174 | val order = inOrder(statusWidget) 175 | order.verify(statusWidget).unregisterActions() 176 | order.verify(statusWidget).registerActions() 177 | } 178 | } -------------------------------------------------------------------------------- /redpen-intellij-plugin.xml: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/cc/redpen/intellij/SettingsPane.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.RedPenException 4 | import cc.redpen.config.* 5 | import com.intellij.openapi.ui.Messages 6 | import com.intellij.ui.PopupMenuListenerAdapter 7 | import com.intellij.uiDesigner.core.GridConstraints 8 | import com.intellij.uiDesigner.core.GridConstraints.* 9 | import com.intellij.uiDesigner.core.GridLayoutManager 10 | import com.intellij.uiDesigner.core.Spacer 11 | import java.io.FileOutputStream 12 | import java.io.IOException 13 | import java.util.* 14 | import javax.swing.* 15 | import javax.swing.JFileChooser.APPROVE_OPTION 16 | import javax.swing.event.CellEditorListener 17 | import javax.swing.event.ChangeEvent 18 | import javax.swing.event.PopupMenuEvent 19 | import javax.swing.filechooser.FileNameExtensionFilter 20 | import javax.swing.table.DefaultTableModel 21 | 22 | open class SettingsPane(internal var provider: RedPenProvider) { 23 | open val configs: MutableMap = LinkedHashMap() 24 | internal val root = JPanel() 25 | internal var validators = JTable(createValidatorsModel()) 26 | internal var symbols = JTable(createSymbolsModel()) 27 | internal var language = JComboBox() 28 | internal val exportButton = JButton("Export...") 29 | internal val importButton = JButton("Import...") 30 | internal val resetButton = JButton("Reset to defaults") 31 | internal var fileChooser = JFileChooser() 32 | internal var configurationExporter = ConfigurationExporter() 33 | internal var configurationLoader = ConfigurationLoader() 34 | 35 | init { 36 | cloneConfigs() 37 | fileChooser.fileFilter = FileNameExtensionFilter("RedPen Configuration", "xml") 38 | 39 | root.layout = GridLayoutManager(2, 7) 40 | root.add(JLabel("Language"), GridConstraints(0, 0, 1, 1, ANCHOR_WEST, FILL_NONE, SIZEPOLICY_FIXED, SIZEPOLICY_FIXED, null, null, null)) 41 | root.add(language, GridConstraints(0, 1, 1, 1, ANCHOR_WEST, FILL_HORIZONTAL, SIZEPOLICY_FIXED, SIZEPOLICY_FIXED, null, null, null)) 42 | root.add(Spacer(), GridConstraints(0, 3, 1, 1, ANCHOR_CENTER, FILL_HORIZONTAL, SIZEPOLICY_WANT_GROW, SIZEPOLICY_CAN_SHRINK, null, null, null)) 43 | root.add(exportButton, GridConstraints(0, 5, 1, 1, ANCHOR_CENTER, FILL_HORIZONTAL, SIZEPOLICY_CAN_SHRINK or SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED, null, null, null)) 44 | root.add(importButton, GridConstraints(0, 4, 1, 1, ANCHOR_CENTER, FILL_HORIZONTAL, SIZEPOLICY_CAN_SHRINK or SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED, null, null, null)) 45 | root.add(resetButton, GridConstraints(0, 6, 1, 1, ANCHOR_CENTER, FILL_HORIZONTAL, SIZEPOLICY_CAN_SHRINK or SIZEPOLICY_CAN_GROW, SIZEPOLICY_FIXED, null, null, null)) 46 | 47 | val tabbedPane = JTabbedPane() 48 | root.add(tabbedPane, GridConstraints(1, 0, 1, 7, ANCHOR_CENTER, FILL_BOTH, SIZEPOLICY_CAN_SHRINK or SIZEPOLICY_CAN_GROW, SIZEPOLICY_CAN_SHRINK or SIZEPOLICY_CAN_GROW, null, null, null)) 49 | 50 | tabbedPane.addTab("Validators", JScrollPane(validators)) 51 | validators.rowHeight = (validators.font.size * 1.5).toInt() 52 | validators.getDefaultEditor(String::class.java).addCellEditorListener(object: CellEditorListener { 53 | override fun editingCanceled(e: ChangeEvent?) {} 54 | override fun editingStopped(e: ChangeEvent?) = showValidatorPropertyErrorIfNeeded(e) 55 | }) 56 | 57 | tabbedPane.addTab("Symbols", JScrollPane(symbols)) 58 | symbols.rowHeight = (validators.font.size * 1.5).toInt() 59 | } 60 | 61 | internal fun showValidatorPropertyErrorIfNeeded(e: ChangeEvent?) { 62 | val text = (e?.source as CellEditor).cellEditorValue.toString() 63 | if (!isCorrectValidatorPropertiesFormat(text)) showValidatorPropertyError(text) 64 | } 65 | 66 | open internal fun showValidatorPropertyError(s: String) { 67 | Messages.showMessageDialog("Validator property must be in key=value format: " + s, "Invalid validator property format", Messages.getErrorIcon()) 68 | } 69 | 70 | internal fun isCorrectValidatorPropertiesFormat(text: String) = parseProperties(text) != null 71 | 72 | open internal fun cloneConfigs() { 73 | provider.configs.forEach { configs[it.key] = it.value.clone() } 74 | } 75 | 76 | fun createPane(): JPanel { 77 | initLanguages() 78 | initTabs() 79 | initButtons() 80 | return root 81 | } 82 | 83 | open internal fun initButtons() { 84 | exportButton.addActionListener { exportConfig() } 85 | importButton.addActionListener { importConfig() } 86 | resetButton.addActionListener { resetToDefaults() } 87 | } 88 | 89 | internal fun importConfig() { 90 | try { 91 | if (fileChooser.showOpenDialog(root) != APPROVE_OPTION) return 92 | config = configurationLoader.load(fileChooser.selectedFile) 93 | initTabs() 94 | } catch (e: RedPenException) { 95 | Messages.showMessageDialog("Cannot load: " + e.message, "RedPen", Messages.getErrorIcon()) 96 | } 97 | } 98 | 99 | internal fun exportConfig() { 100 | try { 101 | if (fileChooser.showSaveDialog(root) != APPROVE_OPTION) return 102 | save() 103 | configurationExporter.export(config, FileOutputStream(fileChooser.selectedFile)) 104 | } catch (e: IOException) { 105 | Messages.showMessageDialog("Cannot write to file: " + e.message, "RedPen", Messages.getErrorIcon()) 106 | } 107 | 108 | } 109 | 110 | open internal fun initLanguages() { 111 | provider.configs.keys.forEach { language.addItem(it) } 112 | language.selectedItem = provider.activeConfig.key 113 | language.addPopupMenuListener(object : PopupMenuListenerAdapter() { 114 | override fun popupMenuWillBecomeVisible(e: PopupMenuEvent?) = applyChanges() 115 | }) 116 | language.addActionListener { initTabs() } 117 | } 118 | 119 | open internal fun initTabs() { 120 | initValidators() 121 | initSymbols() 122 | } 123 | 124 | open internal fun initSymbols() { 125 | symbols.model = createSymbolsModel() 126 | symbols.columnModel.getColumn(0).minWidth = 250 127 | symbols.setDefaultEditor(Char::class.javaObjectType, SingleCharEditor()) 128 | 129 | val symbolTable = config.symbolTable 130 | for (key in symbolTable.names) { 131 | val symbol = symbolTable.getSymbol(key) 132 | (symbols.model as DefaultTableModel).addRow(arrayOf(symbol.type.toString(), symbol.value, 133 | String(symbol.invalidChars), symbol.isNeedBeforeSpace, symbol.isNeedAfterSpace)) 134 | } 135 | 136 | symbols.doLayout() 137 | } 138 | 139 | open internal fun initValidators() { 140 | validators.model = createValidatorsModel() 141 | validators.columnModel.getColumn(0).maxWidth = 20 142 | 143 | val validatorConfigs = config.validatorConfigs.groupByName() 144 | for (config in combinedValidatorConfigs()) { 145 | (validators.model as DefaultTableModel).addRow(arrayOf(validatorConfigs.containsKey(config.key), config.key, config.value.asString())) 146 | } 147 | 148 | validators.doLayout() 149 | } 150 | 151 | private fun ValidatorConfiguration.asString() = properties.entries.joinToString("; ") 152 | 153 | fun List.groupByName() = associateBy { it.configurationName } 154 | 155 | fun combinedValidatorConfigs() = provider.initialConfigs[config.key]!!.validatorConfigs.groupByName() + config.validatorConfigs.groupByName() 156 | 157 | open fun getEditedValidators(): List { 158 | val result = ArrayList() 159 | val model = validators.model 160 | val allConfigs = combinedValidatorConfigs() 161 | 162 | val editorText = ((validators.cellEditor as? DefaultCellEditor)?.component as? JTextField)?.text 163 | val editingRow = validators.editingRow 164 | fun editableValueAt(i: Int) = if (editingRow == i && editorText != null) editorText else model.getValueAt(i, 2) as String 165 | 166 | for (i in 0..model.rowCount-1) { 167 | if (model.getValueAt(i, 0) as Boolean) { 168 | val validator = allConfigs[model.getValueAt(i, 1)]!!.clone() 169 | validator.properties.clear() 170 | parseProperties(editableValueAt(i))?.forEach { validator.addProperty(it.key, it.value) } 171 | result.add(validator) 172 | } 173 | } 174 | return result 175 | } 176 | 177 | internal fun parseProperties(text: String): Map? { 178 | return text.split(";\\s*".toRegex()).filter { it.isNotEmpty() } 179 | .map { it.split("=", limit=2) } 180 | .associate { 181 | if (it.size < 2 || it[0].isEmpty()) return null 182 | it[0].trim() to it[1] 183 | } 184 | } 185 | 186 | open fun getEditedSymbols(): List { 187 | val model = symbols.model 188 | 189 | val editorText = ((symbols.cellEditor as? DefaultCellEditor)?.component as? JTextField)?.text 190 | val editingRow = symbols.editingRow 191 | val editingColumn = symbols.editingColumn 192 | fun editableValueAt(i: Int, j: Int) = if (editorText != null && editingRow == i && editingColumn == j) editorText 193 | else model.getValueAt(i, j).toString() 194 | 195 | return (0..model.rowCount-1).map { i -> 196 | Symbol(SymbolType.valueOf(model.getValueAt(i, 0) as String), editableValueAt(i, 1)[0], editableValueAt(i, 2), 197 | model.getValueAt(i, 3) as Boolean, model.getValueAt(i, 4) as Boolean) 198 | } 199 | } 200 | 201 | open internal fun applyValidatorsChanges() { 202 | val validators = config.validatorConfigs 203 | val editedValidators = getEditedValidators() 204 | validators.clear() 205 | validators.addAll(editedValidators) 206 | } 207 | 208 | open internal fun applySymbolsChanges() { 209 | val symbolTable = config.symbolTable 210 | getEditedSymbols().forEach { symbolTable.overrideSymbol(it) } 211 | } 212 | 213 | open internal fun applyChanges() { 214 | applyValidatorsChanges() 215 | applySymbolsChanges() 216 | } 217 | 218 | open fun save() { 219 | applyChanges() 220 | provider.configs.putAll(configs) 221 | cloneConfigs() 222 | } 223 | 224 | open fun resetChanges() { 225 | cloneConfigs() 226 | initTabs() 227 | } 228 | 229 | fun resetToDefaults() { 230 | provider.initialConfigs.forEach { configs[it.key] = it.value.clone() } 231 | initTabs() 232 | } 233 | 234 | internal fun createValidatorsModel(): DefaultTableModel { 235 | val model: DefaultTableModel = object : DefaultTableModel() { 236 | override fun getColumnClass(i: Int): Class<*> { 237 | return if (i == 0) Boolean::class.javaObjectType else String::class.java 238 | } 239 | 240 | override fun isCellEditable(row: Int, column: Int): Boolean { 241 | return column != 1 242 | } 243 | } 244 | model.addColumn("") 245 | model.addColumn("Name") 246 | model.addColumn("Properties") 247 | return model 248 | } 249 | 250 | internal fun createSymbolsModel(): DefaultTableModel { 251 | val model: DefaultTableModel = object : DefaultTableModel() { 252 | override fun getColumnClass(i: Int): Class<*> { 253 | return if (i == 1) Char::class.javaObjectType else if (i == 3 || i == 4) Boolean::class.javaObjectType else String::class.java 254 | } 255 | 256 | override fun isCellEditable(row: Int, column: Int): Boolean { 257 | return column != 0 258 | } 259 | } 260 | model.addColumn("Symbols") 261 | model.addColumn("Value") 262 | model.addColumn("Invalid chars") 263 | model.addColumn("Space before") 264 | model.addColumn("Space after") 265 | return model 266 | } 267 | 268 | open var config: Configuration 269 | get() = configs[language.selectedItem as String]!! 270 | set(config) { 271 | configs[config.key] = config 272 | language.selectedItem = config.key 273 | if (config.key != language.selectedItem) { 274 | provider += config 275 | language.addItem(config.key) 276 | language.selectedItem = config.key 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /test/cc/redpen/intellij/SettingsPaneTest.kt: -------------------------------------------------------------------------------- 1 | package cc.redpen.intellij 2 | 3 | import cc.redpen.config.Configuration 4 | import cc.redpen.config.Symbol 5 | import cc.redpen.config.SymbolType.* 6 | import cc.redpen.config.ValidatorConfiguration 7 | import com.nhaarman.mockito_kotlin.* 8 | import org.junit.Assert.* 9 | import org.junit.Test 10 | import org.mockito.Mockito.RETURNS_DEEP_STUBS 11 | import java.io.File 12 | import java.util.* 13 | import java.util.Arrays.asList 14 | import java.util.Collections.emptyMap 15 | import javax.swing.CellEditor 16 | import javax.swing.DefaultCellEditor 17 | import javax.swing.JFileChooser.APPROVE_OPTION 18 | import javax.swing.JFileChooser.CANCEL_OPTION 19 | import javax.swing.JTextField 20 | import javax.swing.event.ChangeEvent 21 | import javax.swing.table.DefaultTableModel 22 | 23 | class SettingsPaneTest : BaseTest() { 24 | var settingsPane: SettingsPane 25 | 26 | init { 27 | provider = RedPenProvider(project, LinkedHashMap(mapOf("en" to cloneableConfig("en"), "ja" to cloneableConfig("ja")))) 28 | settingsPane = spy(SettingsPane(provider)) 29 | settingsPane.validators = mock(RETURNS_DEEP_STUBS) 30 | settingsPane.symbols = mock(RETURNS_DEEP_STUBS) 31 | } 32 | 33 | @Test 34 | fun allConfigsAreClonedOnCreation() { 35 | assertSame(settingsPane.provider.initialConfigs["en"]!!.clone(), settingsPane.configs["en"]) 36 | assertSame(settingsPane.provider.initialConfigs["ja"]!!.clone(), settingsPane.configs["ja"]) 37 | } 38 | 39 | @Test 40 | fun languagesAndVariantsArePrepopulated() { 41 | provider.activeConfig = provider.configs["ja"]!! 42 | 43 | settingsPane.initLanguages() 44 | 45 | assertEquals(2, settingsPane.language.itemCount.toLong()) 46 | assertEquals("en", settingsPane.language.getItemAt(0)) 47 | assertEquals("ja", settingsPane.language.getItemAt(1)) 48 | assertEquals("ja", settingsPane.language.selectedItem) 49 | } 50 | 51 | @Test 52 | fun getPaneInitsEverything() { 53 | doNothing().whenever(settingsPane).initLanguages() 54 | doNothing().whenever(settingsPane).initValidators() 55 | doNothing().whenever(settingsPane).initSymbols() 56 | doNothing().whenever(settingsPane).initButtons() 57 | 58 | settingsPane.createPane() 59 | 60 | verify(settingsPane).initLanguages() 61 | verify(settingsPane).initValidators() 62 | verify(settingsPane).initSymbols() 63 | verify(settingsPane).initButtons() 64 | } 65 | 66 | @Test 67 | fun changingOfLanguageAppliesOldChangesAndInitsNewValidatorsAndSymbols() { 68 | doNothing().whenever(settingsPane).initTabs() 69 | doNothing().whenever(settingsPane).applyChanges() 70 | 71 | settingsPane.createPane() 72 | assertSame(provider.activeConfig.clone(), settingsPane.config) 73 | verify(settingsPane).initTabs() 74 | 75 | settingsPane.language.firePopupMenuWillBecomeVisible() 76 | verify(settingsPane).applyChanges() 77 | 78 | settingsPane.language.selectedItem = "ja" 79 | assertSame(provider.configs["ja"]!!.clone(), settingsPane.config) 80 | verify(settingsPane, times(2)).initTabs() 81 | } 82 | 83 | @Test 84 | fun validatorsAreListedInSettings() { 85 | val allValidators = asList( 86 | validatorConfig("ModifiedAttributes", mapOf("attr1" to "val1", "attr2" to "val2")), 87 | validatorConfig("InitialAttributes", mapOf("attr1" to "val1", "attr2" to "val2", "space" to " ")), 88 | validatorConfig("NoAttributes", emptyMap())) 89 | 90 | whenever(provider.initialConfigs["en"]!!.validatorConfigs).thenReturn(allValidators) 91 | doReturn(configWithValidators(listOf( 92 | validatorConfig("ModifiedAttributes", mapOf("foo" to "bar")), 93 | validatorConfig("NewValidator", mapOf("key" to "value"))))).whenever(settingsPane).config 94 | 95 | val model = mock() 96 | whenever(settingsPane.validators.model).thenReturn(model) 97 | 98 | settingsPane.initValidators() 99 | 100 | verify(model).addRow(arrayOf(true, "ModifiedAttributes", "foo=bar")) 101 | verify(model).addRow(arrayOf(false, "InitialAttributes", "attr2=val2; attr1=val1; space= ")) 102 | verify(model).addRow(arrayOf(false, "NoAttributes", "")) 103 | verify(model).addRow(arrayOf(true, "NewValidator", "key=value")) 104 | } 105 | 106 | @Test 107 | fun getEditedValidators_returnsOnlySelectedValidators() { 108 | settingsPane.config = cloneableConfig("en") 109 | 110 | settingsPane.initLanguages() 111 | whenever(provider.initialConfigs["en"]!!.validatorConfigs).thenReturn(asList( 112 | ValidatorConfiguration("first"), 113 | ValidatorConfiguration("second one"))) 114 | 115 | whenever(settingsPane.validators.model.rowCount).thenReturn(2) 116 | whenever(settingsPane.validators.model.getValueAt(0, 0)).thenReturn(false) 117 | whenever(settingsPane.validators.model.getValueAt(0, 1)).thenReturn("first") 118 | whenever(settingsPane.validators.model.getValueAt(1, 0)).thenReturn(true) 119 | whenever(settingsPane.validators.model.getValueAt(1, 1)).thenReturn("second one") 120 | whenever(settingsPane.validators.model.getValueAt(1, 2)).thenReturn("") 121 | 122 | val activeValidators = settingsPane.getEditedValidators() 123 | assertEquals(1, activeValidators.size.toLong()) 124 | assertEquals("second one", activeValidators[0].configurationName) 125 | } 126 | 127 | @Test 128 | fun getEditedValidators_modifiesAttributes() { 129 | settingsPane.config = cloneableConfig("en") 130 | settingsPane.initLanguages() 131 | whenever(provider.initialConfigs["en"]!!.validatorConfigs).thenReturn( 132 | listOf(validatorConfig("Hello", mapOf("width" to "100", "height" to "300", "depth" to "1")))) 133 | 134 | whenever(settingsPane.validators.model.rowCount).thenReturn(1) 135 | whenever(settingsPane.validators.model.getValueAt(0, 0)).thenReturn(true) 136 | whenever(settingsPane.validators.model.getValueAt(0, 1)).thenReturn("Hello") 137 | whenever(settingsPane.validators.model.getValueAt(0, 2)).thenReturn(" width=200; height=300; space= ") 138 | 139 | val activeValidators = settingsPane.getEditedValidators() 140 | assertEquals(1, activeValidators.size.toLong()) 141 | assertEquals(mapOf("width" to "200", "height" to "300", "space" to " "), activeValidators[0].properties) 142 | assertNotSame(provider.initialConfigs["en"]!!.validatorConfigs[0], activeValidators[0]) 143 | } 144 | 145 | @Test 146 | fun getEditedValidators_doesNotApplyActiveCellEditorChanges() { 147 | settingsPane.config = cloneableConfig("en") 148 | whenever(settingsPane.validators.isEditing).thenReturn(true) 149 | settingsPane.getEditedValidators() 150 | verify(settingsPane.validators.cellEditor, never()).stopCellEditing() 151 | } 152 | 153 | @Test 154 | fun getEditedValidators_usesCurrentlyOpenCellEditor() { 155 | settingsPane.config = cloneableConfig("en") 156 | settingsPane.initLanguages() 157 | whenever(provider.initialConfigs["en"]!!.validatorConfigs).thenReturn( 158 | listOf(validatorConfig("Hello", emptyMap()))) 159 | 160 | whenever(settingsPane.validators.model.rowCount).thenReturn(1) 161 | whenever(settingsPane.validators.model.getValueAt(0, 0)).thenReturn(true) 162 | whenever(settingsPane.validators.model.getValueAt(0, 1)).thenReturn("Hello") 163 | 164 | val cellEditor = mock() 165 | whenever(settingsPane.validators.cellEditor).thenReturn(cellEditor) 166 | val cellEditorField = mock() 167 | whenever(cellEditor.component).thenReturn(cellEditorField) 168 | whenever(cellEditorField.text).thenReturn("foo=bar") 169 | whenever(settingsPane.validators.editingRow).thenReturn(0) 170 | 171 | val activeValidators = settingsPane.getEditedValidators() 172 | 173 | assertEquals(mapOf("foo" to "bar"), activeValidators[0].properties) 174 | } 175 | 176 | @Test 177 | fun getEditedSymbols_doesNotApplyActiveCellEditorChanges() { 178 | whenever(settingsPane.symbols.isEditing).thenReturn(true) 179 | settingsPane.getEditedSymbols() 180 | verify(settingsPane.symbols.cellEditor, never()).stopCellEditing() 181 | } 182 | 183 | @Test 184 | fun symbolsAreListedInSettings() { 185 | settingsPane.config = configWithSymbols(asList(Symbol(AMPERSAND, '&', "$%", true, false), Symbol(ASTERISK, '*', "", false, true))) 186 | 187 | val model = mock() 188 | whenever(settingsPane.symbols.model).thenReturn(model) 189 | 190 | settingsPane.initSymbols() 191 | 192 | verify(model).addRow(arrayOf(AMPERSAND.toString(), '&', "$%", true, false)) 193 | verify(model).addRow(arrayOf(ASTERISK.toString(), '*', "", false, true)) 194 | } 195 | 196 | @Test 197 | fun getEditedSymbols() { 198 | val model = settingsPane.symbols.model 199 | whenever(model.rowCount).thenReturn(2) 200 | 201 | whenever(model.getValueAt(0, 0)).thenReturn("AMPERSAND") 202 | whenever(model.getValueAt(0, 1)).thenReturn('&') 203 | whenever(model.getValueAt(0, 2)).thenReturn("$%") 204 | whenever(model.getValueAt(0, 3)).thenReturn(true) 205 | whenever(model.getValueAt(0, 4)).thenReturn(false) 206 | 207 | whenever(model.getValueAt(1, 0)).thenReturn("ASTERISK") 208 | whenever(model.getValueAt(1, 1)).thenReturn("*") 209 | whenever(model.getValueAt(1, 2)).thenReturn("") 210 | whenever(model.getValueAt(1, 3)).thenReturn(false) 211 | whenever(model.getValueAt(1, 4)).thenReturn(true) 212 | 213 | val symbols = settingsPane.getEditedSymbols() 214 | assertEquals(asList(Symbol(AMPERSAND, '&', "$%", true, false), Symbol(ASTERISK, '*', "", false, true)), symbols) 215 | } 216 | 217 | 218 | @Test 219 | fun getEditedSymbols_usesCurrentlyOpenCellEditor() { 220 | val model = settingsPane.symbols.model 221 | whenever(model.rowCount).thenReturn(1) 222 | 223 | whenever(model.getValueAt(0, 0)).thenReturn("AMPERSAND") 224 | whenever(model.getValueAt(0, 1)).thenReturn('&') 225 | whenever(model.getValueAt(0, 2)).thenReturn("%*") 226 | whenever(model.getValueAt(0, 3)).thenReturn(true) 227 | whenever(model.getValueAt(0, 4)).thenReturn(false) 228 | 229 | val cellEditor = mock() 230 | whenever(settingsPane.symbols.cellEditor).thenReturn(cellEditor) 231 | val cellEditorField = mock() 232 | whenever(cellEditor.component).thenReturn(cellEditorField) 233 | whenever(cellEditorField.text).thenReturn("$") 234 | whenever(settingsPane.symbols.editingRow).thenReturn(0) 235 | whenever(settingsPane.symbols.editingColumn).thenReturn(1) 236 | 237 | assertEquals('$', settingsPane.getEditedSymbols()[0].value) 238 | 239 | whenever(cellEditorField.text).thenReturn("abc") 240 | whenever(settingsPane.symbols.editingColumn).thenReturn(2) 241 | 242 | assertArrayEquals(charArrayOf('a', 'b', 'c'), settingsPane.getEditedSymbols()[0].invalidChars) 243 | } 244 | 245 | @Test 246 | fun fileChooserUsesXmlFileFilter() { 247 | assertEquals("RedPen Configuration", settingsPane.fileChooser.fileFilter.description) 248 | val file = mock() 249 | 250 | whenever(file.name).thenReturn("blah.xml") 251 | assertTrue(settingsPane.fileChooser.fileFilter.accept(file)) 252 | 253 | whenever(file.name).thenReturn("blah.txt") 254 | assertFalse(settingsPane.fileChooser.fileFilter.accept(file)) 255 | } 256 | 257 | @Test 258 | fun canCancelExportingConfiguration() { 259 | prepareImportExport() 260 | settingsPane.initButtons() 261 | whenever(settingsPane.fileChooser.showSaveDialog(any())).thenReturn(CANCEL_OPTION) 262 | 263 | settingsPane.exportButton.doClick() 264 | 265 | verify(settingsPane, never()).save() 266 | verify(settingsPane.fileChooser).showSaveDialog(settingsPane.root) 267 | verifyNoMoreInteractions(settingsPane.fileChooser) 268 | } 269 | 270 | @Test 271 | fun canExportConfiguration() { 272 | prepareImportExport() 273 | val file = File.createTempFile("redpen-conf", ".xml") 274 | file.deleteOnExit() 275 | 276 | doReturn(config("en")).whenever(settingsPane).config 277 | whenever(settingsPane.fileChooser.showSaveDialog(any())).thenReturn(APPROVE_OPTION) 278 | whenever(settingsPane.fileChooser.selectedFile).thenReturn(file) 279 | 280 | settingsPane.exportConfig() 281 | 282 | verify(settingsPane).save() 283 | verify(settingsPane.fileChooser).showSaveDialog(settingsPane.root) 284 | verify(settingsPane.fileChooser).selectedFile 285 | verify(settingsPane.configurationExporter).export(eq(settingsPane.config), any()) 286 | } 287 | 288 | @Test 289 | fun canCancelImportingConfiguration() { 290 | prepareImportExport() 291 | settingsPane.initButtons() 292 | whenever(settingsPane.fileChooser.showOpenDialog(any())).thenReturn(CANCEL_OPTION) 293 | 294 | settingsPane.importButton.doClick() 295 | 296 | verify(settingsPane.fileChooser).showOpenDialog(settingsPane.root) 297 | verifyNoMoreInteractions(settingsPane.fileChooser) 298 | } 299 | 300 | @Test 301 | fun canImportConfiguration() { 302 | prepareImportExport() 303 | val file = mock() 304 | val config = config("ja.hankaku") 305 | 306 | whenever(settingsPane.fileChooser.showOpenDialog(any())).thenReturn(APPROVE_OPTION) 307 | whenever(settingsPane.fileChooser.selectedFile).thenReturn(file) 308 | whenever(settingsPane.configurationLoader.load(file)).thenReturn(config) 309 | whenever(settingsPane.language.selectedItem).thenReturn("ja.hankaku") 310 | 311 | settingsPane.importConfig() 312 | 313 | verify(settingsPane.fileChooser).showOpenDialog(settingsPane.root) 314 | verify(settingsPane.fileChooser).selectedFile 315 | verify(settingsPane.configurationLoader).load(file) 316 | assertSame(settingsPane.config, config) 317 | verify(settingsPane).initTabs() 318 | verify(settingsPane.language).selectedItem = "ja.hankaku" 319 | } 320 | 321 | @Test 322 | fun canImportConfigurationAddingNewLanguage() { 323 | prepareImportExport() 324 | val config = config("za") 325 | val clone1 = config("za") 326 | val clone2 = config("za") 327 | 328 | whenever(settingsPane.fileChooser.showOpenDialog(any())).thenReturn(APPROVE_OPTION) 329 | whenever(settingsPane.configurationLoader.load(any())).thenReturn(config) 330 | whenever(config.clone()).thenReturn(clone1, clone2) 331 | 332 | settingsPane.importConfig() 333 | 334 | assertSame(clone1, settingsPane.provider.initialConfigs["za"]) 335 | assertSame(clone2, settingsPane.provider.configs["za"]) 336 | verify(settingsPane.language).addItem("za") 337 | verify(settingsPane.language, times(2)).selectedItem = "za" 338 | } 339 | 340 | private fun prepareImportExport() { 341 | settingsPane.fileChooser = mock() 342 | settingsPane.configurationLoader = mock() 343 | settingsPane.configurationExporter = mock() 344 | settingsPane.language = mock() 345 | doNothing().whenever(settingsPane).initTabs() 346 | doNothing().whenever(settingsPane).save() 347 | } 348 | 349 | @Test 350 | fun applyValidatorChanges() { 351 | val allValidators = asList(ValidatorConfiguration("1"), ValidatorConfiguration("2")) 352 | settingsPane.config = configWithValidators(allValidators) 353 | 354 | val activeValidators = ArrayList(allValidators.subList(0, 1)) 355 | doReturn(activeValidators).whenever(settingsPane).getEditedValidators() 356 | 357 | settingsPane.applyValidatorsChanges() 358 | 359 | assertEquals(activeValidators, settingsPane.config.validatorConfigs) 360 | } 361 | 362 | @Test 363 | fun applyValidatorChanges_addsNewValidatorsIfNeeded() { 364 | whenever(provider.initialConfigs["en"]!!.validatorConfigs).thenReturn(asList( 365 | ValidatorConfiguration("1"), 366 | ValidatorConfiguration("2"))) 367 | val validators = asList( 368 | ValidatorConfiguration("2"), 369 | ValidatorConfiguration("active new"), 370 | ValidatorConfiguration("inactive new")) 371 | 372 | doReturn(configWithValidators(validators)).whenever(settingsPane).config 373 | 374 | whenever(settingsPane.validators.model.rowCount).thenReturn(2) 375 | whenever(settingsPane.validators.model.getValueAt(0, 0)).thenReturn(true) 376 | whenever(settingsPane.validators.model.getValueAt(0, 1)).thenReturn("2") 377 | whenever(settingsPane.validators.model.getValueAt(0, 2)).thenReturn("") 378 | 379 | whenever(settingsPane.validators.model.getValueAt(1, 0)).thenReturn(true) 380 | whenever(settingsPane.validators.model.getValueAt(1, 1)).thenReturn("active new") 381 | whenever(settingsPane.validators.model.getValueAt(1, 2)).thenReturn("") 382 | 383 | whenever(settingsPane.validators.model.getValueAt(2, 0)).thenReturn(false) 384 | whenever(settingsPane.validators.model.getValueAt(2, 1)).thenReturn("inactive new") 385 | whenever(settingsPane.validators.model.getValueAt(2, 2)).thenReturn("") 386 | 387 | settingsPane.applyValidatorsChanges() 388 | 389 | assertEquals(validators.subList(0, 2), settingsPane.config.validatorConfigs) 390 | } 391 | 392 | @Test 393 | fun applySymbolsChanges() { 394 | val symbol = Symbol(BACKSLASH, '\\') 395 | doReturn(listOf(symbol)).whenever(settingsPane).getEditedSymbols() 396 | doReturn(mock(RETURNS_DEEP_STUBS)).whenever(settingsPane).config 397 | 398 | settingsPane.applySymbolsChanges() 399 | 400 | verify(settingsPane.config.symbolTable).overrideSymbol(symbol) 401 | } 402 | 403 | @Test 404 | fun saveClonesLocalConfigs() { 405 | doNothing().whenever(settingsPane).applyChanges() 406 | doNothing().whenever(settingsPane).cloneConfigs() 407 | settingsPane.save() 408 | verify(settingsPane).applyChanges() 409 | verify(settingsPane).cloneConfigs() 410 | } 411 | 412 | @Test 413 | fun resetToDefaults() { 414 | settingsPane.initButtons() 415 | doNothing().whenever(settingsPane).initTabs() 416 | 417 | settingsPane.resetButton.doClick() 418 | 419 | assertEquals(settingsPane.provider.initialConfigs["en"]!!.clone(), settingsPane.configs["en"]) 420 | assertEquals(settingsPane.provider.initialConfigs["ja"]!!.clone(), settingsPane.configs["ja"]) 421 | verify(settingsPane).initTabs() 422 | } 423 | 424 | @Test 425 | fun resetChanges() { 426 | doNothing().whenever(settingsPane).cloneConfigs() 427 | doNothing().whenever(settingsPane).initTabs() 428 | 429 | settingsPane.resetChanges() 430 | 431 | verify(settingsPane).cloneConfigs() 432 | verify(settingsPane).initTabs() 433 | } 434 | 435 | @Test 436 | fun applyChanges() { 437 | doNothing().whenever(settingsPane).applySymbolsChanges() 438 | doNothing().whenever(settingsPane).applyValidatorsChanges() 439 | 440 | settingsPane.applyChanges() 441 | 442 | verify(settingsPane).applySymbolsChanges() 443 | verify(settingsPane).applyValidatorsChanges() 444 | } 445 | 446 | @Test 447 | fun isCorrectPropertiesFormat() { 448 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("")) 449 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=")) 450 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar")) 451 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;")) 452 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;foo2=")) 453 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;foo2=bar2")) 454 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;foo2=bar2;")) 455 | assertTrue(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;foo2=bar2;foo3=bar3")) 456 | assertFalse(settingsPane.isCorrectValidatorPropertiesFormat("foo")) 457 | assertFalse(settingsPane.isCorrectValidatorPropertiesFormat("=bar")) 458 | assertFalse(settingsPane.isCorrectValidatorPropertiesFormat("=bar;")) 459 | assertFalse(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;=")) 460 | assertFalse(settingsPane.isCorrectValidatorPropertiesFormat("foo=bar;foo2")) 461 | } 462 | 463 | @Test 464 | fun reportInvalidValidatorAttributesFormatWhenEditorIsStopped() { 465 | doNothing().whenever(settingsPane).showValidatorPropertyError(any()) 466 | val event = mock() 467 | val source = mock() 468 | whenever(event.source).thenReturn(source) 469 | 470 | whenever(source.cellEditorValue).thenReturn("width") 471 | settingsPane.showValidatorPropertyErrorIfNeeded(event) 472 | verify(settingsPane).showValidatorPropertyError("width") 473 | 474 | whenever(source.cellEditorValue).thenReturn("=") 475 | settingsPane.showValidatorPropertyErrorIfNeeded(event) 476 | verify(settingsPane).showValidatorPropertyError("=") 477 | 478 | whenever(source.cellEditorValue).thenReturn("width=120") 479 | settingsPane.showValidatorPropertyErrorIfNeeded(event) 480 | verifyNoMoreInteractions(settingsPane) 481 | } 482 | 483 | @Test 484 | fun parseProperties() { 485 | val map = settingsPane.parseProperties("hello=world; world=hello;") 486 | assertEquals(mapOf("hello" to "world", "world" to "hello"), map) 487 | } 488 | 489 | @Test 490 | fun parseProperties_invalid() { 491 | assertNull(settingsPane.parseProperties("hello=world; world")) 492 | } 493 | 494 | private fun validatorConfig(name: String, attributes: Map): ValidatorConfiguration { 495 | val config = ValidatorConfiguration(name) 496 | attributes.forEach { config.addProperty(it.key, it.value) } 497 | return config 498 | } 499 | } --------------------------------------------------------------------------------