diff options
Diffstat (limited to 'src/main/kotlin/org/polyfrost/sorbet')
6 files changed, 554 insertions, 0 deletions
diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/Preprocessor.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/Preprocessor.kt new file mode 100644 index 0000000..03cb762 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/Preprocessor.kt @@ -0,0 +1,16 @@ +package org.polyfrost.sorbet.intelliprocessor + +val ALLOWED_TYPES = listOf("JAVA", "KOTLIN") + +enum class PreprocessorState { + NONE, + IF, + ELSE, +} + +enum class PreprocessorDirective { + IF, + IFDEF, + ELSE, + ENDIF, +} diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorCompletion.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorCompletion.kt new file mode 100644 index 0000000..b5f8417 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorCompletion.kt @@ -0,0 +1,35 @@ +package org.polyfrost.sorbet.intelliprocessor + +import com.intellij.codeInsight.completion.* +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.openapi.project.DumbAware +import com.intellij.patterns.PlatformPatterns.psiComment +import com.intellij.patterns.StandardPatterns +import com.intellij.util.ProcessingContext + +class PreprocessorCompletion : CompletionContributor(), DumbAware { + init { + extend( + CompletionType.BASIC, + psiComment().withText( + StandardPatterns.or( + StandardPatterns.string().startsWith("//"), + StandardPatterns.string().startsWith("#"), + ), + ), + PreprocessorCompletionProvider, + ) + } + + object PreprocessorCompletionProvider : CompletionProvider<CompletionParameters>() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet, + ) { + for (keyword in KEYWORDS) result.addElement(LookupElementBuilder.create(keyword).bold()) + } + + private val KEYWORDS = listOf("#if", "#else", "#elseif", "#endif", "#ifdef") + } +} diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorExtend.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorExtend.kt new file mode 100644 index 0000000..db59737 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorExtend.kt @@ -0,0 +1,56 @@ +package org.polyfrost.sorbet.intelliprocessor + +import com.intellij.codeInsight.editorActions.EnterHandler +import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegate.Result +import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.util.Ref +import com.intellij.psi.PsiFile +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.refactoring.suggested.startOffset +import java.util.Locale + +class PreprocessorExtend : EnterHandlerDelegateAdapter(), DumbAware { + override fun preprocessEnter( + file: PsiFile, + editor: Editor, + caretOffset: Ref<Int>, + caretAdvance: Ref<Int>, + dataContext: DataContext, + originalHandler: EditorActionHandler?, + ): Result { + if ( + EnterHandler.getLanguage(dataContext) + ?.associatedFileType + ?.name?.uppercase(Locale.getDefault()) !in ALLOWED_TYPES + ) { + return Result.Continue + } + + val caret: Int = caretOffset.get().toInt() + val psiAtOffset = file.findElementAt(caret) + + if (psiAtOffset is PsiCommentImpl) { + if (!psiAtOffset.text.startsWith("//$$")) return Result.Continue + val posInText = caret - psiAtOffset.startOffset + if (posInText < 4) return Result.DefaultForceIndent + + editor.document.insertString(editor.caretModel.offset, "//$$ ") + caretAdvance.set(5) + return Result.DefaultForceIndent + } else if (psiAtOffset?.prevSibling is PsiCommentImpl) { + if (!psiAtOffset.prevSibling.text.startsWith("//$$")) return Result.Continue + val posInText = caret - psiAtOffset.prevSibling.startOffset + if (posInText < 4) return Result.DefaultForceIndent + + editor.document.insertString(editor.caretModel.offset, "//$$ ") + caretAdvance.set(5) + return Result.DefaultForceIndent + } + + return Result.Continue + } +} diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorFolding.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorFolding.kt new file mode 100644 index 0000000..25d5ba0 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorFolding.kt @@ -0,0 +1,63 @@ +package org.polyfrost.sorbet.intelliprocessor + +import com.intellij.lang.ASTNode +import com.intellij.lang.LanguageCommenters +import com.intellij.lang.folding.FoldingBuilderEx +import com.intellij.lang.folding.FoldingDescriptor +import com.intellij.openapi.editor.Document +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiComment +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiWhiteSpace +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.suggested.endOffset +import com.intellij.refactoring.suggested.startOffset + +class PreprocessorFolding : FoldingBuilderEx(), DumbAware { + override fun getPlaceholderText(node: ASTNode): String { + if (node !is PsiComment) return "...11".also { println("Not a comment? Is $node") } + val directivePrefix = ( + LanguageCommenters.INSTANCE.forLanguage(node.language).lineCommentPrefix + ?: return "...222".also { println("Null comment prefix?") } + ) + return (node as ASTNode).text.substring(directivePrefix.length) + } + + override fun buildFoldRegions( + root: PsiElement, + document: Document, + quick: Boolean, + ): Array<FoldingDescriptor> { + val descriptors = mutableListOf<FoldingDescriptor>() + val directivePrefix = + ( + LanguageCommenters.INSTANCE.forLanguage(root.language).lineCommentPrefix + ?: return emptyArray() + ) + "#" + val allDirectives = + PsiTreeUtil.findChildrenOfType(root, PsiComment::class.java) + .filter { it.text.startsWith(directivePrefix) } + + for ((index, directive) in allDirectives.withIndex()) if (directive.text.run { + startsWith(directivePrefix + "if") || + startsWith(directivePrefix + "ifdef") || + startsWith(directivePrefix + "else") + } && index + 1 < allDirectives.size + ) { + val nextDirective = allDirectives[index + 1] + val endOffset = + when { + nextDirective.text.startsWith(directivePrefix + "endif") -> nextDirective.endOffset + nextDirective.prevSibling is PsiWhiteSpace -> nextDirective.prevSibling.startOffset + else -> nextDirective.startOffset + } + + descriptors.add(FoldingDescriptor(directive, TextRange(directive.startOffset, endOffset))) + } + + return descriptors.toTypedArray() + } + + override fun isCollapsedByDefault(node: ASTNode) = false +} diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorHighlight.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorHighlight.kt new file mode 100644 index 0000000..9eba31b --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorHighlight.kt @@ -0,0 +1,325 @@ +package org.polyfrost.sorbet.intelliprocessor + +import com.intellij.codeInsight.daemon.impl.HighlightInfo +import com.intellij.codeInsight.daemon.impl.HighlightInfoType +import com.intellij.codeInsight.daemon.impl.HighlightVisitor +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder +import com.intellij.lang.Commenter +import com.intellij.lang.LanguageCommenters +import com.intellij.lang.annotation.HighlightSeverity +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.colors.TextAttributesKey +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.fileTypes.SyntaxHighlighter +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.impl.source.tree.PsiCommentImpl +import com.intellij.refactoring.suggested.endOffset +import java.awt.Font +import java.util.* +import java.util.regex.Pattern + +class PreprocessorHighlight(private val project: Project) : HighlightVisitor, DumbAware { + private lateinit var holder: HighlightInfoHolder + private lateinit var commenter: Commenter + private lateinit var highlighter: SyntaxHighlighter + + private var preprocessorState = ArrayDeque<PreprocessorState>() + + override fun suitableForFile(file: PsiFile): Boolean { + return file.fileType.name.uppercase(Locale.getDefault()) in ALLOWED_TYPES + } + + override fun clone(): PreprocessorHighlight { + return PreprocessorHighlight(project) + } + + override fun analyze( + file: PsiFile, + updateWholeFile: Boolean, + holder: HighlightInfoHolder, + action: Runnable, + ): Boolean { + this.holder = holder + this.commenter = LanguageCommenters.INSTANCE.forLanguage(file.language) + this.highlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(file.language, file.project, file.virtualFile) + + action.run() + return true + } + + override fun visit(element: PsiElement) { + if (element !is PsiCommentImpl) return + val commentSource = element.text + if (commenter.lineCommentPrefix?.let { + commentSource.startsWith(it) + } != true + ) { + return + } + + val prefixLength = commenter.lineCommentPrefix?.length ?: return + + val comment = commentSource.substring(prefixLength) + if (comment.isEmpty()) return + + EditorColorsManager.getInstance() + + if (comment.startsWith("#")) { + val commentSegments = comment.substring(1).split(WHITESPACES_PATTERN, limit = 2) + + when (val directive = commentSegments[0]) { + "if", "elseif" -> { + if (directive == "elseif") { + val existingIf = preprocessorState.pollFirst() + if (existingIf != PreprocessorState.IF) { + fail( + element, + "Preprocessor directive \"elseif\" must have a preceding \"if\" or \"elseif\".", + ) + return + } + } + + preprocessorState.push(PreprocessorState.IF) + + holder.add(directive.toDirectiveHighlight(element, prefixLength)) + + if (commentSegments.size < 2) { + fail(element, "Preprocessor directive \"$directive\" is missing a condition.", eol = true) + return + } + + val conditionsSource = commentSegments[1] + val conditions = conditionsSource.split(SPLIT_PATTERN) + + var nextStartPos = prefixLength + 3 + for (condition in conditions) { + val trimmedCondition = condition.trim() + + val position = commentSource.indexOf(trimmedCondition, nextStartPos) + nextStartPos = position + trimmedCondition.length + + val conditionMatcher = EXPR_PATTERN.find(trimmedCondition) + + if (conditionMatcher == null || conditionMatcher.groups.size < 4) { + val identifierMatcher = IDENTIFIER_PATTERN.matchEntire(trimmedCondition) + + if (identifierMatcher != null) { + holder.add(identifierMatcher.groups[0]?.toNumericOrVariableHighlight(element, position)) + } else { + holder.add(trimmedCondition.toInvalidConditionErrorHighlight(element, position)) + } + + continue + } + + holder.add(conditionMatcher.groups[1]?.toNumericOrVariableHighlight(element, position)) + holder.add(conditionMatcher.groups[3]?.toNumericOrVariableHighlight(element, position)) + } + } + + "ifdef" -> { + preprocessorState.push(PreprocessorState.IF) + + holder.add(directive.toDirectiveHighlight(element, prefixLength)) + + if (commentSegments.size < 2) { + fail(element, "Preprocessor directive \"ifdef\" is missing an identifier.", eol = true) + return + } + + val idInfo = + HighlightInfo + .newHighlightInfo(IDENTIFIER_TYPE) + .range( + element as PsiElement, + element.startOffset + prefixLength + 7, + element.startOffset + prefixLength + 7 + commentSegments[1].length, + ) + .textAttributes(IDENTIFIER_ATTRIBUTES) + .create() + + holder.add(idInfo) + } + + "else" -> { + val state = preprocessorState.pollFirst() + preprocessorState.push(PreprocessorState.ELSE) + + if (state != PreprocessorState.IF) { + fail(element, "Preprocessor directive \"else\" must have an opening if.") + return + } + + if (commentSegments.size > 1) { + fail(element, "Preprocessor directive \"else\" does not require any arguments.") + return + } + + holder.add(directive.toDirectiveHighlight(element, prefixLength)) + } + + "endif" -> { + val state = preprocessorState.pollFirst() + + if (state != PreprocessorState.IF && state != PreprocessorState.ELSE) { + fail(element, "Preprocessor directive \"endif\" must have an opening if.") + return + } + + if (commentSegments.size > 1) { + fail(element, "Preprocessor directive \"endif\" does not require any arguments.") + return + } + + holder.add(directive.toDirectiveHighlight(element, prefixLength)) + } + + else -> { + fail(element, "Unknown preprocessor directive \"$directive\"") + } + } + } else if (comment.startsWith("$$")) { + holder.add("$$".toDirectiveHighlight(element, prefixLength)) + + highlightCodeBlock(element, element.startOffset + prefixLength + 2, comment.substring(2)) + } + } + + private fun highlightCodeBlock( + element: PsiCommentImpl, + startOffset: Int, + text: String, + ) { + val lexer = highlighter.highlightingLexer + + lexer.start(text) + var token = lexer.tokenType + + while (token != null) { + val attributes = + highlighter.getTokenHighlights(token) + .fold(TextAttributes(null, null, null, null, 0)) { first, second -> + TextAttributes.merge(first, SCHEME.getAttributes(second)) + } + + val directiveInfo = + HighlightInfo + .newHighlightInfo(HighlightInfoType.INJECTED_LANGUAGE_FRAGMENT) + .range( + element as PsiElement, + startOffset + lexer.tokenStart, + startOffset + lexer.tokenEnd, + ) + .textAttributes(attributes) + .create() + + holder.add(directiveInfo) + + lexer.advance() + token = lexer.tokenType + } + } + + private fun fail( + element: PsiElement, + text: String, + eol: Boolean = false, + ) { + val info = + HighlightInfo + .newHighlightInfo(HighlightInfoType.ERROR) + .descriptionAndTooltip(text) + .apply { + if (eol) { + endOfLine() + range(element.endOffset, element.endOffset) + } else { + range(element) + } + } + .create() + + holder.add(info) + } + + private fun MatchGroup.toNumericOrVariableHighlight( + element: PsiCommentImpl, + offset: Int = 0, + ): HighlightInfo? { + val builder = + if (value.trim().toIntOrNull() != null) { + HighlightInfo + .newHighlightInfo(NUMBER_TYPE) + .textAttributes(NUMBER_ATTRIBUTES) + } else { + HighlightInfo + .newHighlightInfo(IDENTIFIER_TYPE) + .textAttributes(IDENTIFIER_ATTRIBUTES) + } + + return builder + .range(element, element.startOffset + offset + range.first, element.startOffset + offset + range.last + 1) + .create() + } + + private fun String.toDirectiveHighlight( + element: PsiCommentImpl, + offset: Int = 0, + ): HighlightInfo? { + return HighlightInfo + .newHighlightInfo(DIRECTIVE_TYPE) + .textAttributes(DIRECTIVE_ATTRIBUTES) + .range(element, element.startOffset + offset, element.startOffset + offset + 1 + length) + .create() + } + + private fun String.toInvalidConditionErrorHighlight( + element: PsiCommentImpl, + offset: Int = 0, + ): HighlightInfo? { + return HighlightInfo + .newHighlightInfo(HighlightInfoType.ERROR) + .range(element, element.startOffset + offset, element.startOffset + offset + length) + .descriptionAndTooltip("Invalid condition \"$this\"") + .create() + } + + companion object { + private val BOLD_ATTRIBUTE = TextAttributes(null, null, null, null, Font.BOLD) + val SCHEME = EditorColorsManager.getInstance().globalScheme + + private val DIRECTIVE_COLOR: TextAttributesKey = DefaultLanguageHighlighterColors.KEYWORD + val DIRECTIVE_ATTRIBUTES: TextAttributes = + TextAttributes.merge(SCHEME.getAttributes(DIRECTIVE_COLOR), BOLD_ATTRIBUTE) + val DIRECTIVE_TYPE = HighlightInfoType.HighlightInfoTypeImpl(HighlightSeverity.INFORMATION, DIRECTIVE_COLOR) + + private val OPERATOR_COLOR: TextAttributesKey = DefaultLanguageHighlighterColors.OPERATION_SIGN + val OPERATOR_ATTRIBUTES = SCHEME.getAttributes(OPERATOR_COLOR) + val OPERATOR_TYPE = HighlightInfoType.HighlightInfoTypeImpl(HighlightSeverity.INFORMATION, OPERATOR_COLOR) + + private val IDENTIFIER_COLOR: TextAttributesKey = DefaultLanguageHighlighterColors.IDENTIFIER + val IDENTIFIER_ATTRIBUTES: TextAttributes = + TextAttributes.merge(SCHEME.getAttributes(IDENTIFIER_COLOR), BOLD_ATTRIBUTE) + val IDENTIFIER_TYPE = HighlightInfoType.HighlightInfoTypeImpl(HighlightSeverity.INFORMATION, IDENTIFIER_COLOR) + + private val NUMBER_COLOR: TextAttributesKey = DefaultLanguageHighlighterColors.NUMBER + val NUMBER_ATTRIBUTES: TextAttributes = TextAttributes.merge(SCHEME.getAttributes(NUMBER_COLOR), BOLD_ATTRIBUTE) + val NUMBER_TYPE = HighlightInfoType.HighlightInfoTypeImpl(HighlightSeverity.INFORMATION, NUMBER_COLOR) + + private val LOGGER: Logger = Logger.getInstance(PreprocessorHighlight::class.java) + + private val WHITESPACES_PATTERN = "\\s+".toRegex() + private val EXPR_PATTERN = "(.+)(==|!=|<=|>=|<|>)(.+)".toRegex() + private val IDENTIFIER_PATTERN = "[A-Za-z0-9]+".toRegex() + private val OR_PATTERN = Pattern.quote("||") + private val AND_PATTERN = Pattern.quote("&&") + private val SPLIT_PATTERN = Pattern.compile("$OR_PATTERN|$AND_PATTERN") + } +} diff --git a/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorImport.kt b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorImport.kt new file mode 100644 index 0000000..1072ee0 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/sorbet/intelliprocessor/PreprocessorImport.kt @@ -0,0 +1,59 @@ +package org.polyfrost.sorbet.intelliprocessor + +import com.intellij.lang.ImportOptimizer +import com.intellij.lang.LanguageImportStatements +import com.intellij.lang.java.JavaImportOptimizer +import com.intellij.openapi.util.EmptyRunnable +import com.intellij.psi.* +import com.intellij.psi.codeStyle.JavaCodeStyleManager +import com.intellij.psi.impl.source.tree.PsiCommentImpl + +class PreprocessorImport : ImportOptimizer { + override fun supports(file: PsiFile): Boolean { + return file is PsiJavaFile + } + + override fun processFile(file: PsiFile): Runnable { + if (file !is PsiJavaFile) return EmptyRunnable.getInstance() + val imports = file.importList ?: return EmptyRunnable.getInstance() + + if (!hasPreprocessorDirectives(imports)) { + return LanguageImportStatements.INSTANCE + .allForLanguage(file.language) + .first { it !is JavaImportOptimizer } + .processFile(file) + } + + val optimizedImportList = + JavaCodeStyleManager + .getInstance(file.project) + .prepareOptimizeImportsResult(file) + + return Runnable { + val manager = PsiDocumentManager.getInstance(file.project) + val document = manager.getDocument(file) + if (document != null) manager.commitDocument(document) + + for (import in imports.importStatements) + if (optimizedImportList.findSingleClassImportStatement(import.qualifiedName) == null) { + import.delete() + } + + if (imports.firstChild is PsiWhiteSpace) imports.firstChild.delete() + } + } + + private fun hasPreprocessorDirectives(imports: PsiImportList): Boolean { + var import = imports.firstChild + + while (import != null) { + if (import is PsiCommentImpl && import.text.startsWith("//#")) { + return true + } else { + import = import.nextSibling + } + } + + return false + } +} |