Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ pluginVersion = 2026.1.5
pluginSinceBuild = 251

# IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension
platformType = IU
platformVersion = 2025.1.1
platformType = PS
platformVersion = 2025.3.2

# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ class MagoConfigurable(val project: Project) : Configurable {
browserLink("Documentation", "https://mago.carthage.software/tools/formatter/overview")
.align(AlignX.RIGHT)
}.layout(RowLayout.PARENT_GRID)
row {
cell(OnOffButton())
.label(MagoBundle.message("settings.formatter.formatAfterFix"))
.bindSelected(settings::formatAfterFix)
comment(MagoBundle.message("settings.formatter.formatAfterFix.comment"))
}.layout(RowLayout.PARENT_GRID)
row {
textField()
.label("Additional parameters")
Expand All @@ -130,11 +136,14 @@ class MagoConfigurable(val project: Project) : Configurable {
.bindSelected(settings::linterEnabled)
browserLink("Documentation", "https://mago.carthage.software/tools/linter/overview")
.align(AlignX.RIGHT)
}
.layout(RowLayout.PARENT_GRID)
.visible(true)
.enabled(false)
.comment("Not implemented yet.")
}.layout(RowLayout.PARENT_GRID)
row {
textField()
.label("Additional parameters")
.bindText(settings::lintAdditionalParameters)
.comment("Read more: mago lint --help")
.align(AlignX.FILL)
}.layout(RowLayout.PARENT_GRID)
}
group(MagoBundle.message("settings.guard.title")) {
row {
Expand All @@ -143,11 +152,14 @@ class MagoConfigurable(val project: Project) : Configurable {
.bindSelected(settings::guardEnabled)
browserLink("Documentation", "https://mago.carthage.software/tools/guard/overview")
.align(AlignX.RIGHT)
}
.layout(RowLayout.PARENT_GRID)
.visible(true)
.enabled(false)
.comment("Not implemented yet.")
}.layout(RowLayout.PARENT_GRID)
row {
textField()
.label("Additional parameters")
.bindText(settings::guardAdditionalParameters)
.comment("Read more: mago guard --help")
.align(AlignX.FILL)
}.layout(RowLayout.PARENT_GRID)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@ class MagoProjectConfiguration : QualityToolProjectConfiguration<MagoConfigurati
var guardEnabled = false
var linterEnabled = false
var formatterEnabled = true

var analyzeAdditionalParameters = ""
var lintAdditionalParameters = ""
var guardAdditionalParameters = ""
var formatAdditionalParameters = ""
var formatAfterFix = false

var configurationFile = ""
var debug = false

Expand All @@ -34,4 +39,4 @@ class MagoProjectConfiguration : QualityToolProjectConfiguration<MagoConfigurati
fun getInstance(project: Project): MagoProjectConfiguration =
project.getService(MagoProjectConfiguration::class.java)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,48 @@ open class MagoAnnotatorProxy : QualityToolAnnotator<MagoValidationInspection>()
add("analyze")
add(toWorkspaceRelativePath(workspace, filePath))
add("--reporting-format=json")
// filePath?.let { add(it) }
addAll(ParametersList.parse(settings.analyzeAdditionalParameters))
}.apply {
DebugLogger.inform(
project,
"Analyze options",
"""Options: ${joinToString(" ")}, filePath: $filePath""",
)
}

fun getLintOptions(settings: MagoProjectConfiguration, project: Project, filePath: String) = buildList {
val workspace = findWorkspace(project, filePath)
addWorkspace(workspace, project)
addConfig(workspace, project, settings)

add("lint")
add(toWorkspaceRelativePath(workspace, filePath))
add("--reporting-format=json")
addAll(ParametersList.parse(settings.lintAdditionalParameters))
}.apply {
DebugLogger.inform(
project,
"Lint options",
"""Options: ${joinToString(" ")}, filePath: $filePath""",
)
}

fun getGuardOptions(settings: MagoProjectConfiguration, project: Project, filePath: String) = buildList {
val workspace = findWorkspace(project, filePath)
addWorkspace(workspace, project)
addConfig(workspace, project, settings)

add("guard")
add(toWorkspaceRelativePath(workspace, filePath))
add("--reporting-format=json")
addAll(ParametersList.parse(settings.guardAdditionalParameters))
}.apply {
DebugLogger.inform(
project,
"Guard options",
"""Options: ${joinToString(" ")}, filePath: $filePath""",
)
}
.plus(ParametersList.parse(settings.analyzeAdditionalParameters))
.apply {
DebugLogger.inform(
project,
"Analyze options",
"""Options: ${joinToString(" ")}, filePath: $filePath""",
)
}

private fun toWorkspaceRelativePath(workspace: VirtualFile, absoluteFilePath: String): String {
return toRelativePath(workspace.path, absoluteFilePath)
Expand Down Expand Up @@ -136,7 +168,10 @@ open class MagoAnnotatorProxy : QualityToolAnnotator<MagoValidationInspection>()
checkNotNull(filePath)
val settings = project.getService(MagoProjectConfiguration::class.java)

return getAnalyzeOptions(settings, project, filePath)
val options = mutableListOf<String>()
options.addAll(getAnalyzeOptions(settings, project, filePath))

return options
}

override fun getQualityToolType() = MagoQualityToolType.INSTANCE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package com.github.xepozz.mago.qualityTool

import com.github.xepozz.mago.configuration.MagoProjectConfiguration
import com.github.xepozz.mago.formatter.MagoExternalFormatter
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer
import com.intellij.codeInsight.intention.FileModifier
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInsight.intention.IntentionActionWithOptions
import com.intellij.codeInsight.intention.PriorityAction
import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.FileUtil
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import java.nio.charset.StandardCharsets
import java.nio.file.Paths

enum class ApplyAllScope(val maxSafetyLevel: Int, val label: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please keep 1 class in 1 file

SAFE_ONLY(0, "fixes only safe"),
POTENTIALLY_UNSAFE(1, "potentially unsafe"),
UNSAFE(2, "unsafe")
}

fun safetyLevel(safety: String): Int = when (safety) {
"unsafe" -> 2
"potentiallyunsafe" -> 1
else -> 0
}

fun MagoEdit.maxSafetyLevel(): Int = replacements.maxOfOrNull { safetyLevel(it.safety) } ?: 0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put maxSafetyLevel in the MagoEdit instead


/** Normalize path for comparison so edit path (e.g. with ./) matches IDE path. */
private fun normalizePath(path: String): String = FileUtil.toCanonicalPath(path)

/** True if this edit applies to the given file (path or name match, paths normalized). */
private fun editMatchesFile(edit: MagoEdit, filePath: String?, fileName: String): Boolean {
if (filePath != null) {
if (FileUtil.pathsEqual(normalizePath(filePath), normalizePath(edit.path))) return true
}
if (edit.name == fileName) return true
val editLastName = Paths.get(edit.name).fileName?.toString()
if (editLastName == fileName) return true
if (edit.name.endsWith("/$fileName") || edit.name.endsWith("\\$fileName")) return true
return false
}

/** Keep only replacements with exactly this safety level (safe=0, potentially unsafe=1, unsafe=2). */
fun filterEditsByExactSafety(edits: List<MagoEdit>, level: Int): List<MagoEdit> = edits
.map { edit ->
edit.copy(replacements = edit.replacements.filter { safetyLevel(it.safety) == level })
}
.filter { it.replacements.isNotEmpty() }

class MagoApplyEditAction(
private val edits: List<MagoEdit>,
private val isApplyAll: Boolean = false,
private val applyAllScope: ApplyAllScope? = null,
private val fixDescription: String? = null
) : IntentionAction, PriorityAction, FileModifier {

override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement = currentFile

override fun getFileModifierForPreview(target: PsiFile): FileModifier {
return MagoApplyEditAction(edits, isApplyAll, applyAllScope, fixDescription)
}
override fun getFamilyName() = "Mago"

override fun getPriority(): PriorityAction.Priority {
return if (isApplyAll) PriorityAction.Priority.LOW else PriorityAction.Priority.HIGH
}

override fun getText(): String {
if (applyAllScope != null) {
return "Mago: Apply all suggested fixes (${applyAllScope.label})"
}
val maxSafetyValue = edits.flatMap { it.replacements }.maxOfOrNull { safetyLevel(it.safety) } ?: 0
val safetySuffix = when (maxSafetyValue) {
2 -> " (unsafe)"
1 -> " (potentially unsafe)"
else -> ""
}
return when {
!fixDescription.isNullOrBlank() -> "Mago: " + fixDescription.trim() + safetySuffix
isApplyAll -> "Mago: Apply all suggested fixes$safetySuffix"
else -> "Mago: Apply suggested fix$safetySuffix"
}
}
Comment on lines +79 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use string builder instead


override fun invoke(project: Project, editor: Editor, file: PsiFile) {
val filePath = file.virtualFile?.path?.let { normalizePath(it) }
val fileName = file.name
val currentFileEdits = edits.filter { editMatchesFile(it, filePath, fileName) }
if (currentFileEdits.isEmpty()) return
val fileText = file.text
val doc = editor.document
val allReplacements = currentFileEdits.flatMap { edit ->
edit.replacements.map { r ->
val startChar = byteOffsetToCharOffset(fileText, r.start)
val endChar = byteOffsetToCharOffset(fileText, r.end)
Triple(startChar, endChar, r.newText)
}
}.sortedByDescending { it.first }
val inPreview = IntentionPreviewUtils.isIntentionPreviewActive()
if (inPreview) {
for ((startChar, endChar, newText) in allReplacements) {
if (startChar in 0..endChar && endChar <= doc.textLength) {
doc.replaceString(startChar, endChar, newText)
}
}
} else {
WriteCommandAction.runWriteCommandAction(project) {
for ((startChar, endChar, newText) in allReplacements) {
if (startChar in 0..endChar && endChar <= doc.textLength) {
doc.replaceString(startChar, endChar, newText)
}
}
PsiDocumentManager.getInstance(project).commitDocument(doc)
file.putUserData(MagoGlobalInspection.MAGO_ANNOTATOR_INFO, null)
MagoHtmlAnnotator.clearProblemCache(file)
val settings = project.getService(MagoProjectConfiguration::class.java)
if (settings.formatAfterFix && settings.formatterEnabled) {
val formatter = MagoExternalFormatter()
if (formatter.activeForFile(file)) {
formatter.format(file, file.textRange,
canChangeWhiteSpacesOnly = false,
keepLineBreaks = false,
enableBulkUpdate = false,
cursorOffset = 0
)
}
}
DaemonCodeAnalyzer.getInstance(project).restart(file, "Mago fix applied")
}
}
}

private fun byteOffsetToCharOffset(text: String, byteOffset: Int): Int {
val bytes = text.toByteArray(StandardCharsets.UTF_8)
if (byteOffset <= 0) return 0
if (byteOffset >= bytes.size) return text.length
return String(bytes.copyOf(byteOffset), StandardCharsets.UTF_8).length
}

override fun startInWriteAction() = true
override fun isAvailable(project: Project, editor: Editor, file: PsiFile) = edits.isNotEmpty()
}

class MagoApplyEditSubmenuAction(
private val mainAction: MagoApplyEditAction,
private val subActions: List<IntentionAction>
) : IntentionAction, IntentionActionWithOptions, PriorityAction, FileModifier {
override fun getFamilyName() = mainAction.familyName
override fun getText() = mainAction.text
override fun invoke(project: Project, editor: Editor, file: PsiFile) {
mainAction.invoke(project, editor, file)
}

override fun getOptions(): List<IntentionAction> {
// Don't duplicate the main entry in the submenu (IntentionOptionsOnly shows only getOptions())
return subActions.filter { it != mainAction }
}

override fun getCombiningPolicy(): IntentionActionWithOptions.CombiningPolicy {
return IntentionActionWithOptions.CombiningPolicy.IntentionOptionsOnly
}
override fun getPriority() = mainAction.priority
override fun startInWriteAction() = mainAction.startInWriteAction()
override fun isAvailable(project: Project, editor: Editor, file: PsiFile) = mainAction.isAvailable(project, editor, file)

override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement = mainAction.getElementToMakeWritable(currentFile)

override fun getFileModifierForPreview(target: PsiFile): FileModifier? {
val mainCopy = mainAction.getFileModifierForPreview(target) as? MagoApplyEditAction ?: return null
val subCopies = subActions.mapNotNull { (it as? FileModifier)?.getFileModifierForPreview(target) }
if (subCopies.size != subActions.size) return null
return MagoApplyEditSubmenuAction(mainCopy, subCopies.filterIsInstance<IntentionAction>())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ class MagoGlobalInspection : QualityToolValidationGlobalInspection(), ExternalAn
override fun getSharedLocalInspectionTool() = MagoValidationInspection()

companion object {
private val MAGO_ANNOTATOR_INFO = Key.create<List<ProblemDescription>>("ANNOTATOR_INFO_MAGO")
val MAGO_ANNOTATOR_INFO = Key.create<List<ProblemDescription>>("ANNOTATOR_INFO_MAGO")
}
}
Loading
Loading