diff --git a/java/java-syntax/BUILD.bazel b/java/java-syntax/BUILD.bazel index dfee08536ce9..3a4722949250 100644 --- a/java/java-syntax/BUILD.bazel +++ b/java/java-syntax/BUILD.bazel @@ -31,6 +31,7 @@ jvm_library( "//platform/util/base/multiplatform", "//platform/util/multiplatform", "//platform/syntax/syntax-util:util", + "//platform/syntax/syntax-extensions:extensions", ], exports = [ "//platform/syntax/syntax-api:syntax", diff --git a/java/java-syntax/intellij.java.syntax.iml b/java/java-syntax/intellij.java.syntax.iml index 875490376c17..338ea00b50f4 100644 --- a/java/java-syntax/intellij.java.syntax.iml +++ b/java/java-syntax/intellij.java.syntax.iml @@ -32,5 +32,6 @@ + \ No newline at end of file diff --git a/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxDefinition.kt b/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxDefinition.kt index ae5004cb2db9..2b40c4a24e0b 100644 --- a/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxDefinition.kt +++ b/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxDefinition.kt @@ -7,12 +7,16 @@ import com.intellij.java.syntax.lexer.JavaDocLexer import com.intellij.java.syntax.lexer.JavaLexer import com.intellij.java.syntax.lexer.JavaTypeEscapeLexer import com.intellij.platform.syntax.SyntaxElementTypeSet +import com.intellij.platform.syntax.element.SyntaxTokenTypes +import com.intellij.platform.syntax.extensions.SyntaxLanguage import com.intellij.platform.syntax.lexer.Lexer import com.intellij.platform.syntax.syntaxElementTypeSetOf import com.intellij.pom.java.LanguageLevel import kotlin.jvm.JvmStatic object JavaSyntaxDefinition { + val language: SyntaxLanguage = SyntaxLanguage("com.intellij.java") + @JvmStatic fun createLexer(languageLevel: LanguageLevel): Lexer = JavaLexer(languageLevel) @@ -27,4 +31,6 @@ object JavaSyntaxDefinition { JavaSyntaxTokenType.C_STYLE_COMMENT, JavaDocSyntaxElementType.DOC_COMMENT ) + + val whitespaces: SyntaxElementTypeSet = syntaxElementTypeSetOf(SyntaxTokenTypes.WHITE_SPACE) } diff --git a/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxLog.kt b/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxLog.kt new file mode 100644 index 000000000000..06942f0b97b6 --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/JavaSyntaxLog.kt @@ -0,0 +1,9 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.syntax + +import com.intellij.platform.syntax.Logger +import com.intellij.platform.syntax.util.log.logger + +internal object JavaSyntaxLog { + val log: Logger = logger() +} \ No newline at end of file diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JavaBindingPolicy.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JavaBindingPolicy.kt new file mode 100644 index 000000000000..478938b4996e --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JavaBindingPolicy.kt @@ -0,0 +1,30 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.syntax.element + +import com.intellij.platform.syntax.SyntaxElementType +import com.intellij.platform.syntax.element.SyntaxTokenTypes +import com.intellij.platform.syntax.parser.WhitespaceOrCommentBindingPolicy +import com.intellij.platform.syntax.syntaxElementTypeSetOf + +internal object JavaBindingPolicy : WhitespaceOrCommentBindingPolicy { + private val leftBoundTokens = syntaxElementTypeSetOf( + SyntaxTokenTypes.ERROR_ELEMENT, // todo move somewhere? + JavaSyntaxElementType.TYPE_PARAMETER_LIST, + JavaSyntaxElementType.NAME_VALUE_PAIR, + JavaSyntaxElementType.ANNOTATION_PARAMETER_LIST, + JavaSyntaxElementType.EXTENDS_LIST, + JavaSyntaxElementType.IMPLEMENTS_LIST, + JavaSyntaxElementType.EXTENDS_BOUND_LIST, + JavaSyntaxElementType.THROWS_LIST, + JavaSyntaxElementType.PROVIDES_WITH_LIST, + JavaSyntaxElementType.PERMITS_LIST, + JavaSyntaxElementType.REFERENCE_PARAMETER_LIST, + JavaSyntaxElementType.EMPTY_EXPRESSION, + JavaSyntaxElementType.EXPRESSION_LIST, + JavaSyntaxElementType.ANNOTATION_PARAMETER_LIST, + + ) + + override fun isLeftBound(elementType: SyntaxElementType): Boolean = + elementType in leftBoundTokens +} \ No newline at end of file diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JavaCodeBlockParser.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JavaCodeBlockParser.kt new file mode 100644 index 000000000000..0b573a8586fb --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JavaCodeBlockParser.kt @@ -0,0 +1,66 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.syntax.element + +import com.intellij.java.syntax.parser.JavaParser +import com.intellij.platform.syntax.CancellationProvider +import com.intellij.platform.syntax.LazyParser +import com.intellij.platform.syntax.LazyParsingContext +import com.intellij.platform.syntax.lexer.TokenList +import com.intellij.platform.syntax.parser.ProductionResult +import com.intellij.platform.syntax.parser.prepareProduction +import com.intellij.platform.syntax.tree.SyntaxNode +import com.intellij.platform.syntax.util.cancellation.cancellationProvider +import com.intellij.platform.syntax.util.parser.SyntaxBuilderUtil +import com.intellij.pom.java.LanguageLevel + +internal class JavaCodeBlockParser : LazyParser { + override fun parse(parsingContext: LazyParsingContext): ProductionResult { + return doParse( + node = parsingContext.node, + cachedLexemes = parsingContext.tokenList, + text = parsingContext.text, + level = getLanguageLevel(parsingContext), + cancellationProvider = cancellationProvider() + ) + } + + override fun tryReparse(parsingContext: LazyParsingContext): ProductionResult? { + val cancellationProvider = cancellationProvider() + + val level = getLanguageLevel(parsingContext) + val tokens = cachedOrLex( + cachedLexemes = parsingContext.tokenList, + text = parsingContext.text, + languageLevel = level, + cancellationProvider = cancellationProvider + ) + + val hasProperBraceBalance = SyntaxBuilderUtil.hasProperBraceBalance( + tokenList = tokens, + leftBrace = JavaSyntaxTokenType.LBRACE, + rightBrace = JavaSyntaxTokenType.RBRACE, + cancellationProvider = cancellationProvider + ) + if (!hasProperBraceBalance) return null + + return doParse( + node = parsingContext.node, + cachedLexemes = tokens, + text = parsingContext.text, + level = level, + cancellationProvider = cancellationProvider + ) + } + + private fun doParse( + node: SyntaxNode, + cachedLexemes: TokenList? = null, + text: CharSequence, + level: LanguageLevel, + cancellationProvider: CancellationProvider?, + ): ProductionResult { + val builder = createSyntaxBuilder(node, text, level, cachedLexemes, cancellationProvider) + JavaParser(level).statementParser.parseCodeBlockDeep(builder, true) + return prepareProduction(builder) + } +} \ No newline at end of file diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JavaLanguageLevelProvider.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JavaLanguageLevelProvider.kt new file mode 100644 index 000000000000..f39f8f441650 --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JavaLanguageLevelProvider.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.java.syntax.element + +import com.intellij.platform.syntax.LazyParsingContext +import com.intellij.platform.syntax.extensions.ExtensionPointKey +import com.intellij.platform.syntax.extensions.ExtensionSupport +import com.intellij.platform.syntax.tree.SyntaxTree +import com.intellij.pom.java.LanguageLevel +import org.jetbrains.annotations.ApiStatus + +/** + * Implement this extension point to provide the custom language level for Java lazy parsers. + */ +@ApiStatus.OverrideOnly +interface JavaLanguageLevelProvider { + fun getLanguageLevel(syntaxTree: SyntaxTree): LanguageLevel +} + +internal fun getLanguageLevel(parsingContext: LazyParsingContext): LanguageLevel { + val languageLevelProvider = ExtensionSupport().getExtensions(languageLevelExtensionPoint).firstOrNull() + val languageLevel = languageLevelProvider?.getLanguageLevel(parsingContext.tree) ?: LanguageLevel.HIGHEST + return languageLevel +} + +private val languageLevelExtensionPoint: ExtensionPointKey = ExtensionPointKey("com.intellij.java.syntax.languageLevelProvider") diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxElementType.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxElementType.kt index 8cf6e2efc99b..f6b1112259c0 100644 --- a/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxElementType.kt +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxElementType.kt @@ -119,7 +119,7 @@ object JavaSyntaxElementType { @JvmField val DEFAULT_CASE_LABEL_ELEMENT: SyntaxElementType = SyntaxElementType("DEFAULT_CASE_LABEL_ELEMENT") @JvmField val CASE_LABEL_ELEMENT_LIST: SyntaxElementType = SyntaxElementType("CASE_LABEL_ELEMENT_LIST") - @JvmField val CODE_BLOCK: SyntaxElementType = SyntaxElementType("CODE_BLOCK") + @JvmField val CODE_BLOCK: SyntaxElementType = SyntaxElementType("CODE_BLOCK", lazyParser = JavaCodeBlockParser()) @JvmField val MEMBERS: SyntaxElementType = SyntaxElementType("MEMBERS") diff --git a/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxUtil.kt b/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxUtil.kt new file mode 100644 index 000000000000..d8b7d6cef63a --- /dev/null +++ b/java/java-syntax/src/com/intellij/java/syntax/element/JavaSyntaxUtil.kt @@ -0,0 +1,50 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:JvmName("JavaSyntaxUtil") + +package com.intellij.java.syntax.element + +import com.intellij.java.syntax.JavaSyntaxDefinition +import com.intellij.java.syntax.JavaSyntaxLog +import com.intellij.platform.syntax.CancellationProvider +import com.intellij.platform.syntax.lexer.TokenList +import com.intellij.platform.syntax.lexer.performLexing +import com.intellij.platform.syntax.parser.SyntaxTreeBuilder +import com.intellij.platform.syntax.parser.SyntaxTreeBuilderFactory +import com.intellij.platform.syntax.tree.SyntaxNode +import com.intellij.pom.java.LanguageLevel +import kotlin.jvm.JvmName + +internal fun createSyntaxBuilder( + chameleon: SyntaxNode, + text: CharSequence, + languageLevel: LanguageLevel, + cachedLexemes: TokenList? = null, + cancellationProvider: CancellationProvider?, +): SyntaxTreeBuilder { + val lexemes = cachedOrLex(cachedLexemes, text, languageLevel, cancellationProvider) + val builder = SyntaxTreeBuilderFactory.builder( + text = text, + whitespaces = JavaSyntaxDefinition.whitespaces, + comments = JavaSyntaxDefinition.commentSet, + tokenList = lexemes, + ) + .withStartOffset(chameleon.startOffset) + .withLanguage(JavaSyntaxDefinition.language.id) + .withWhitespaceOrCommentBindingPolicy(JavaBindingPolicy) + .withCancellationProvider(cancellationProvider) + .withLogger(JavaSyntaxLog.log) + + return builder.build() +} + +internal fun cachedOrLex( + cachedLexemes: TokenList?, + text: CharSequence, + languageLevel: LanguageLevel, + cancellationProvider: CancellationProvider?, +): TokenList { + return cachedLexemes ?: run { + val lexer = JavaSyntaxDefinition.createLexer(languageLevel) + performLexing(text, lexer, cancellationProvider, JavaSyntaxLog.log) + } +} diff --git a/platform/syntax/syntax-api/api-dump-experimental.txt b/platform/syntax/syntax-api/api-dump-experimental.txt index 49e9d6b443db..7eabfcc34f6f 100644 --- a/platform/syntax/syntax-api/api-dump-experimental.txt +++ b/platform/syntax/syntax-api/api-dump-experimental.txt @@ -1,5 +1,17 @@ *:com.intellij.platform.syntax.CancellationProvider - a:checkCancelled():V +*:com.intellij.platform.syntax.LazyParser +- a:parse(com.intellij.platform.syntax.LazyParsingContext):com.intellij.platform.syntax.parser.ProductionResult +- tryReparse(com.intellij.platform.syntax.LazyParsingContext):com.intellij.platform.syntax.parser.ProductionResult +*f:com.intellij.platform.syntax.LazyParserKt +- *sf:parseLazyNode(com.intellij.platform.syntax.LazyParsingContext):com.intellij.platform.syntax.parser.ProductionResult +- *sf:tryReparseLazyNode(com.intellij.platform.syntax.LazyParsingContext):com.intellij.platform.syntax.parser.ProductionResult +*f:com.intellij.platform.syntax.LazyParsingContext +- (com.intellij.platform.syntax.tree.SyntaxTree,com.intellij.platform.syntax.tree.SyntaxNode,java.lang.CharSequence,com.intellij.platform.syntax.lexer.TokenList):V +- f:getNode():com.intellij.platform.syntax.tree.SyntaxNode +- f:getText():java.lang.CharSequence +- f:getTokenList():com.intellij.platform.syntax.lexer.TokenList +- f:getTree():com.intellij.platform.syntax.tree.SyntaxTree *:com.intellij.platform.syntax.Logger - a:debug(java.lang.String,java.lang.Throwable):V - bs:debug$default(com.intellij.platform.syntax.Logger,java.lang.String,java.lang.Throwable,I,java.lang.Object):V @@ -20,8 +32,11 @@ - equals(java.lang.Object):Z - f:getIndex():I - hashCode():I +- f:isLazyParseable():Z *f:com.intellij.platform.syntax.SyntaxElementTypeKt - *sf:SyntaxElementType(java.lang.String):com.intellij.platform.syntax.SyntaxElementType +- *sf:SyntaxElementType(java.lang.String,com.intellij.platform.syntax.LazyParser):com.intellij.platform.syntax.SyntaxElementType +- *bs:SyntaxElementType$default(java.lang.String,com.intellij.platform.syntax.LazyParser,I,java.lang.Object):com.intellij.platform.syntax.SyntaxElementType *f:com.intellij.platform.syntax.SyntaxElementTypeSet - java.util.Set - kotlin.jvm.internal.markers.KMappedMarker @@ -189,3 +204,17 @@ - f:defaultRightBinder():com.intellij.platform.syntax.parser.WhitespacesAndCommentsBinder - f:greedyLeftBinder():com.intellij.platform.syntax.parser.WhitespacesAndCommentsBinder - f:greedyRightBinder():com.intellij.platform.syntax.parser.WhitespacesAndCommentsBinder +*:com.intellij.platform.syntax.tree.SyntaxNode +- a:getEndOffset():I +- a:getErrorMessage():java.lang.String +- a:getFirstChild():com.intellij.platform.syntax.tree.SyntaxNode +- a:getLastChild():com.intellij.platform.syntax.tree.SyntaxNode +- a:getNextSibling():com.intellij.platform.syntax.tree.SyntaxNode +- a:getParent():com.intellij.platform.syntax.tree.SyntaxNode +- a:getPrevSibling():com.intellij.platform.syntax.tree.SyntaxNode +- a:getStartOffset():I +- a:getText():java.lang.CharSequence +- a:getType():com.intellij.platform.syntax.SyntaxElementType +*:com.intellij.platform.syntax.tree.SyntaxTree +- a:getRoot():com.intellij.platform.syntax.tree.SyntaxNode +- a:getText():java.lang.CharSequence diff --git a/platform/syntax/syntax-api/src/com/intellij/platform/syntax/LazyParser.kt b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/LazyParser.kt new file mode 100644 index 000000000000..3c96370fd359 --- /dev/null +++ b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/LazyParser.kt @@ -0,0 +1,79 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +@file:ApiStatus.Experimental + +package com.intellij.platform.syntax + +import com.intellij.platform.syntax.lexer.TokenList +import com.intellij.platform.syntax.parser.ProductionResult +import com.intellij.platform.syntax.tree.SyntaxNode +import com.intellij.platform.syntax.tree.SyntaxTree +import org.jetbrains.annotations.ApiStatus + +/** + * A parser that is attached to so-called chameleon nodes which allows to parse them lazily on demand. + * + * ### Implementation note: + * + * Provided [SyntaxTree] and [SyntaxNode] might be backed by different tree implementations, + * depending on the syntax-lib client. So please don't make any assumptions on the actual types of the passed instances. + * + * If you want to add platform-specific code, introduce an extension point, see [com.intellij.platform.syntax.extensions.ExtensionSupport]. + * + * @see parseLazyNode + * @see tryReparseLazyNode + */ +@ApiStatus.Experimental +@ApiStatus.OverrideOnly +interface LazyParser { + /** + * Called when the node is requested to be parsed. + * + * @return the result of the parsing operation + */ + fun parse(parsingContext: LazyParsingContext): ProductionResult + + /** + * Called when the node is requested to be reparsed. + * + * @return the result of the parsing operation or `null` if reparsing is not possible + * (e.g., when braces got unbalanced in the next) + */ + fun tryReparse(parsingContext: LazyParsingContext): ProductionResult? = null +} + +/** + * Parses the given node and returns [ProductionResult]. + * + * @see LazyParser.parse + */ +@ApiStatus.Experimental +fun parseLazyNode(parsingContext: LazyParsingContext): ProductionResult { + return parsingContext.lazyParser.parse(parsingContext) +} + +/** + * Tries to reparse the given node and returns [ProductionResult] if possible. + * + * @see LazyParser.tryReparse + */ +@ApiStatus.Experimental +fun tryReparseLazyNode(parsingContext: LazyParsingContext): ProductionResult? { + return parsingContext.lazyParser.tryReparse(parsingContext) +} + +/** + * @param tree the tree being parsed + * @param node the node being parsed + * @param text the text of the node being parsed + * @param tokenList the token list being parsed. Might be missing if the parsing engine does not store this information. + */ +@ApiStatus.Experimental +class LazyParsingContext( + val tree: SyntaxTree, + val node: SyntaxNode, + val text: CharSequence, + val tokenList: TokenList?, +) { + internal val lazyParser: LazyParser + get() = node.type.lazyParser ?: error("Node ${node} has non-lazy element type ${node.type}") +} \ No newline at end of file diff --git a/platform/syntax/syntax-api/src/com/intellij/platform/syntax/SyntaxElementType.kt b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/SyntaxElementType.kt index 0fd853f94d5d..0cf7e21b6f7a 100644 --- a/platform/syntax/syntax-api/src/com/intellij/platform/syntax/SyntaxElementType.kt +++ b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/SyntaxElementType.kt @@ -8,6 +8,7 @@ import org.jetbrains.annotations.ApiStatus import kotlin.concurrent.atomics.AtomicInt import kotlin.concurrent.atomics.ExperimentalAtomicApi import kotlin.concurrent.atomics.fetchAndIncrement +import kotlin.jvm.JvmOverloads /** * A class defining a token or node type. @@ -17,10 +18,19 @@ import kotlin.concurrent.atomics.fetchAndIncrement @ApiStatus.Experimental class SyntaxElementType internal constructor( private val debugName: String, + internal val lazyParser: LazyParser?, @Suppress("unused") unusedParam: Any?, // this parameter is necessary for disambiguation with the factory function ) { val index: Int = counter.fetchAndIncrement() + /** + * Checks if this element type is lazy-parseable. + * For performing reparse, use [parseLazyNode] and [tryReparseLazyNode] functions. + * + * @return `true` if this element type is lazy-parseable. + */ + fun isLazyParseable(): Boolean = lazyParser != null + override fun toString(): String = debugName override fun equals(other: Any?): Boolean = this === other @@ -29,9 +39,11 @@ class SyntaxElementType internal constructor( } @ApiStatus.Experimental +@JvmOverloads fun SyntaxElementType( debugName: String, + lazyParser: LazyParser? = null, ): SyntaxElementType = - SyntaxElementType(debugName, null as Any?) + SyntaxElementType(debugName, lazyParser, null as Any?) private val counter = AtomicInt(0) diff --git a/platform/syntax/syntax-api/src/com/intellij/platform/syntax/tree/SyntaxTree.kt b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/tree/SyntaxTree.kt new file mode 100644 index 000000000000..eeeed8e3876f --- /dev/null +++ b/platform/syntax/syntax-api/src/com/intellij/platform/syntax/tree/SyntaxTree.kt @@ -0,0 +1,40 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.platform.syntax.tree + +import com.intellij.platform.syntax.SyntaxElementType +import org.jetbrains.annotations.ApiStatus + +/** + * API for a Syntax Tree. + * + * The tree is passed to [com.intellij.platform.syntax.LazyParser] as the context for parsing. + */ +@ApiStatus.Experimental +interface SyntaxTree { + val text: CharSequence + val root: SyntaxNode +} + +/** + * API for a Syntax Node. + * + */ +@ApiStatus.Experimental +interface SyntaxNode { + val text: CharSequence + + val type: SyntaxElementType + + val startOffset: Int + val endOffset: Int + + val parent: SyntaxNode? + + val prevSibling: SyntaxNode? + val nextSibling: SyntaxNode? + + val firstChild: SyntaxNode? + val lastChild: SyntaxNode? + + val errorMessage: String? +} diff --git a/platform/syntax/syntax-util/api-dump-experimental.txt b/platform/syntax/syntax-util/api-dump-experimental.txt index 50e2434498cf..1e4bab237301 100644 --- a/platform/syntax/syntax-util/api-dump-experimental.txt +++ b/platform/syntax/syntax-util/api-dump-experimental.txt @@ -109,13 +109,14 @@ - *sf:logger(java.lang.String):com.intellij.platform.syntax.Logger *f:com.intellij.platform.syntax.util.parser.SyntaxBuilderUtil - sf:INSTANCE:com.intellij.platform.syntax.util.parser.SyntaxBuilderUtil -- f:advance(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,I):V -- f:drop(com.intellij.platform.syntax.parser.SyntaxTreeBuilder$Marker[]):V -- f:expect(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementType):Z -- f:expect(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementTypeSet):Z +- sf:advance(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,I):V +- sf:drop(com.intellij.platform.syntax.parser.SyntaxTreeBuilder$Marker[]):V +- sf:expect(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementType):Z +- sf:expect(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementTypeSet):Z +- sf:hasProperBraceBalance(com.intellij.platform.syntax.lexer.TokenList,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.CancellationProvider):Z - sf:hasProperBraceBalance(java.lang.CharSequence,com.intellij.platform.syntax.lexer.Lexer,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.CancellationProvider):Z -- f:parseBlockLazy(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType):com.intellij.platform.syntax.parser.SyntaxTreeBuilder$Marker -- f:rawTokenText(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,I):java.lang.CharSequence +- sf:parseBlockLazy(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType,com.intellij.platform.syntax.SyntaxElementType):com.intellij.platform.syntax.parser.SyntaxTreeBuilder$Marker +- sf:rawTokenText(com.intellij.platform.syntax.parser.SyntaxTreeBuilder,I):java.lang.CharSequence *c:com.intellij.platform.syntax.util.parser.SyntaxTreeBuilderAdapter - com.intellij.platform.syntax.parser.SyntaxTreeBuilder - (com.intellij.platform.syntax.parser.SyntaxTreeBuilder):V diff --git a/platform/syntax/syntax-util/src/com/intellij/platform/syntax/util/parser/SyntaxBuilderUtil.kt b/platform/syntax/syntax-util/src/com/intellij/platform/syntax/util/parser/SyntaxBuilderUtil.kt index bbb8204c63ee..e82ba94ce5b1 100644 --- a/platform/syntax/syntax-util/src/com/intellij/platform/syntax/util/parser/SyntaxBuilderUtil.kt +++ b/platform/syntax/syntax-util/src/com/intellij/platform/syntax/util/parser/SyntaxBuilderUtil.kt @@ -5,6 +5,7 @@ import com.intellij.platform.syntax.CancellationProvider import com.intellij.platform.syntax.SyntaxElementType import com.intellij.platform.syntax.SyntaxElementTypeSet import com.intellij.platform.syntax.lexer.Lexer +import com.intellij.platform.syntax.lexer.TokenList import com.intellij.platform.syntax.parser.SyntaxTreeBuilder import com.intellij.platform.syntax.parser.WhitespacesBinders import com.intellij.util.text.CharSequenceSubSequence @@ -19,6 +20,7 @@ object SyntaxBuilderUtil { * @param this PSI builder to operate on. * @param count number of tokens to skip. */ + @JvmStatic fun SyntaxTreeBuilder.advance(count: Int) { repeat(count) { if (eof()) return @@ -34,6 +36,7 @@ object SyntaxBuilderUtil { * @param expectedType expected token. * @return true if token matches, false otherwise. */ + @JvmStatic fun SyntaxTreeBuilder.expect(expectedType: SyntaxElementType?): Boolean { if (tokenType === expectedType) { advanceLexer() @@ -49,6 +52,7 @@ object SyntaxBuilderUtil { * @param expectedTypes expected token types. * @return true if token matches, false otherwise. */ + @JvmStatic fun SyntaxTreeBuilder.expect(expectedTypes: SyntaxElementTypeSet): Boolean { if (tokenType in expectedTypes) { advanceLexer() @@ -62,12 +66,14 @@ object SyntaxBuilderUtil { * * @param markers markers to drop. */ + @JvmStatic fun drop(vararg markers: SyntaxTreeBuilder.Marker?) { for (marker in markers) { marker?.drop() } } + @JvmStatic fun SyntaxTreeBuilder.rawTokenText(index: Int): CharSequence { return CharSequenceSubSequence( baseSequence = text, @@ -80,6 +86,7 @@ object SyntaxBuilderUtil { * tries to parse a code block with corresponding left and right braces. * @return collapsed marker of the block or `null` if there is no code block at all. */ + @JvmStatic fun SyntaxTreeBuilder.parseBlockLazy( leftBrace: SyntaxElementType, rightBrace: SyntaxElementType, @@ -114,8 +121,8 @@ object SyntaxBuilderUtil { } /** - * Checks if `text` looks like a proper block. - * In particular it + * Checks if [text] looks like a proper block. + * In particular, it * (1) checks brace balance * (2) verifies that the block's closing brace is the last token * @@ -135,15 +142,59 @@ object SyntaxBuilderUtil { cancellationProvider: CancellationProvider?, ): Boolean { lexer.start(text) + return checkBraceBalance( + leftBrace = leftBrace, + rightBrace = rightBrace, + cancellationProvider = cancellationProvider, + advance = lexer::advance, + curType = lexer::getTokenType + ) + } - if (lexer.getTokenType() !== leftBrace) return false + /** + * Checks if [tokenList] looks like a proper block. + * In particular, it + * (1) checks brace balance + * (2) verifies that the block's closing brace is the last token + * + * @param tokenList - tokens to check + * @param leftBrace - left brace element type + * @param rightBrace - right brace element type + * @param cancellationProvider - a hook to stop operation if it's not necessary anymore + * @return true if `text` passes the checks + */ + @JvmStatic + fun hasProperBraceBalance( + tokenList: TokenList, + leftBrace: SyntaxElementType, + rightBrace: SyntaxElementType, + cancellationProvider: CancellationProvider?, + ): Boolean { + var i = 0 + return checkBraceBalance( + leftBrace = leftBrace, + rightBrace = rightBrace, + cancellationProvider = cancellationProvider, + advance = { i++ }, + curType = { tokenList.getTokenType(i) } + ) + } + + private inline fun checkBraceBalance( + leftBrace: SyntaxElementType, + rightBrace: SyntaxElementType, + cancellationProvider: CancellationProvider?, + advance: () -> Unit, + curType: () -> SyntaxElementType?, + ): Boolean { + if (curType() !== leftBrace) return false + advance() - lexer.advance() var balance = 1 while (true) { cancellationProvider?.checkCancelled() - val type = lexer.getTokenType() + val type = curType() if (type == null) { //eof: checking balance @@ -162,7 +213,7 @@ object SyntaxBuilderUtil { balance-- } - lexer.advance() + advance() } } } \ No newline at end of file