├── .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 |
5 |
6 |
7 |
8 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 | 
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 [](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 | 
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 |
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 | }
--------------------------------------------------------------------------------