├── testData ├── parse │ ├── Paren.pest │ ├── Char.pest │ ├── Issue25.pest │ ├── SimpleRule.pest │ ├── NestedComment.pest │ ├── Builtins.pest │ ├── Issue41.pest │ ├── NestedComment.txt │ ├── Issue25.txt │ ├── Char.txt │ ├── Issue41.txt │ ├── SimpleRule.txt │ ├── Paren.txt │ └── Builtins.txt └── rust │ ├── External.rs │ ├── Inline.rs │ ├── External.txt │ └── Inline.txt ├── .vscode └── settings.json ├── settings.gradle.kts ├── rust ├── .gitignore ├── .cargo │ └── config.toml ├── src │ ├── str4j.rs │ ├── misc.rs │ └── lib.rs ├── Cargo.toml └── README.md ├── res ├── rs │ └── pest │ │ ├── error │ │ ├── token.bin │ │ └── report-bundle.properties │ │ └── pest-bundle.properties ├── inspectionDescriptions │ └── PestDuplicateRule.html ├── fileTemplates │ └── Pest File.pest.ft ├── META-INF │ ├── plugin-rust.xml │ ├── description.html │ ├── pluginIcon_dark.svg │ ├── pluginIcon.svg │ ├── change-notes.html │ └── plugin.xml ├── liveTemplates │ └── Pest.xml ├── colorSchemes │ ├── Pest_dark.xml │ └── Pest.xml └── icons │ ├── pest_dark.svg │ ├── pest.svg │ ├── pest_file_dark.svg │ └── pest_file.svg ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── icons │ └── PestIcons.java └── rs │ └── pest │ ├── action │ ├── ui │ │ ├── RuleSelector.java │ │ ├── PestIdeBridgeInfo.java │ │ ├── PestIntroduceRulePopup.java │ │ ├── RuleSelector.form │ │ ├── ui-impl.kt │ │ ├── PestIntroduceRulePopup.form │ │ └── PestIdeBridgeInfo.form │ ├── create-file.kt │ ├── tools.kt │ ├── inline.kt │ └── introduce.kt │ ├── PestLanguage.java │ ├── livePreview │ ├── LivePreviewLanguage.java │ ├── html.kt │ ├── live-preview.kt │ ├── ffi.kt │ └── live-highlight.kt │ ├── psi │ ├── manipulators.kt │ ├── utils.kt │ ├── types.kt │ ├── impl │ │ ├── psi-impl.kt │ │ └── psi-mixins.kt │ └── PestStringEscaper.java │ ├── editing │ ├── pest-live-templates.kt │ ├── pest-completion.kt │ ├── pest-gutter.kt │ ├── pest-structure.kt │ ├── pest-annotator.kt │ ├── pest-inspection.kt │ └── pest-editing.kt │ ├── format │ ├── block.kt │ └── model.kt │ ├── pest-parser-def.kt │ ├── rust │ └── rust-inject.kt │ ├── pest-infos.kt │ └── pest-constants.kt ├── .editorconfig ├── test └── rs │ └── pest │ ├── generation.kt │ ├── parsing-test.kt │ ├── psi │ └── PestStringEscaperTest.java │ └── rust-interaction-test.kt ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── build.yml ├── appveyor.yml ├── gradlew.bat ├── README.md ├── .gitignore ├── gradlew └── grammar ├── pest.bnf └── pest.flex /testData/parse/Paren.pest: -------------------------------------------------------------------------------- 1 | a = { a ~ (rule ~ b) ~ c } 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false 3 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "intellij-pest" 2 | 3 | -------------------------------------------------------------------------------- /testData/parse/Char.pest: -------------------------------------------------------------------------------- 1 | test = { '\u{00}'..'\u{123456}' } 2 | -------------------------------------------------------------------------------- /rust/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea 4 | .vscode 5 | .vs 6 | -------------------------------------------------------------------------------- /testData/parse/Issue25.pest: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | */ 4 | 5 | rule = { " " } 6 | -------------------------------------------------------------------------------- /testData/parse/SimpleRule.pest: -------------------------------------------------------------------------------- 1 | rule = { rule | "" ~ 'a'..'z' | "bla" } 2 | -------------------------------------------------------------------------------- /testData/parse/NestedComment.pest: -------------------------------------------------------------------------------- 1 | /*Joesph Joestar/*/**/*/ Oh My God*/ 2 | rule={""} 3 | -------------------------------------------------------------------------------- /testData/rust/External.rs: -------------------------------------------------------------------------------- 1 | #[derive(Parser)] 2 | #[grammar = "path/to/my_grammar.pest"] 3 | struct MyParser; 4 | -------------------------------------------------------------------------------- /res/rs/pest/error/token.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pest-parser/intellij-pest/HEAD/res/rs/pest/error/token.bin -------------------------------------------------------------------------------- /testData/rust/Inline.rs: -------------------------------------------------------------------------------- 1 | #[derive(Parser)] 2 | #[grammar_inline = "\ 3 | my_rule = { \"\" } 4 | "] 5 | struct MyParser; 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pest-parser/intellij-pest/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /res/inspectionDescriptions/PestDuplicateRule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Checks that every grammar rule has unique name.

4 | 5 | 6 | -------------------------------------------------------------------------------- /res/fileTemplates/Pest File.pest.ft: -------------------------------------------------------------------------------- 1 | // 2 | // Created by intellij-pest on ${YEAR}-${MONTH}-${DAY} 3 | // ${NAME} 4 | // Author: ${USER} 5 | // 6 | 7 | ${NAME_SNAKE} = { "Hello World!" } 8 | -------------------------------------------------------------------------------- /res/META-INF/plugin-rust.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /testData/parse/Builtins.pest: -------------------------------------------------------------------------------- 1 | // 2 | // Created by intellij-pest on 2019-03-22 3 | // Builtins 4 | // Author: ice1000 5 | // 6 | 7 | builtins = { "Hello World!" } 8 | COMMENT = { ID_CONTINUE } 9 | WHITESPACE = { WHITE_SPACE } 10 | -------------------------------------------------------------------------------- /rust/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | # https://github.com/koute/substrate/blob/2aebf63b859f5f7a01f9b631828d90b5b54b608b/utils/wasm-builder/src/wasm_project.rs#L635C4-L635C7 4 | rustflags = "-C target-feature=-sign-ext" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-rc-2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /testData/parse/Issue41.pest: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Concat : '~~'; 4 | Destruct : '~='; 5 | 6 | /* |&! */ 7 | 8 | DoubleBang : '!!'; 9 | BitNot : '!' | '\uFF01'; //U+FF01 ! 10 | 11 | /* $ @ */ 12 | Curry : '@@@'; 13 | Apply : '@@'; 14 | LetAssign : '@='; 15 | 16 | */ 17 | 18 | rule = { "114514" } 19 | -------------------------------------------------------------------------------- /src/icons/PestIcons.java: -------------------------------------------------------------------------------- 1 | package icons; 2 | 3 | import com.intellij.openapi.util.IconLoader; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import javax.swing.*; 7 | 8 | public interface PestIcons { 9 | @NotNull Icon PEST_FILE = IconLoader.getIcon("/icons/pest_file.png"); 10 | @NotNull Icon PEST = IconLoader.getIcon("/icons/pest.png"); 11 | } 12 | -------------------------------------------------------------------------------- /src/rs/pest/action/ui/RuleSelector.java: -------------------------------------------------------------------------------- 1 | package rs.pest.action.ui; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import javax.swing.*; 6 | 7 | @SuppressWarnings("NullableProblems") 8 | public class RuleSelector { 9 | public @NotNull JPanel mainPanel; 10 | public @NotNull JComboBox ruleCombo; 11 | public @NotNull JButton okButton; 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = false 5 | indent_style = tab 6 | tab_width = 2 7 | 8 | [gradle.properties] 9 | indent_size = 0 10 | 11 | [*.rst] 12 | indent_size = 3 13 | indent_style = space 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.rs] 20 | indent_style = space 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /src/rs/pest/PestLanguage.java: -------------------------------------------------------------------------------- 1 | package rs.pest; 2 | 3 | import com.intellij.lang.Language; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import static rs.pest.Pest_constantsKt.PEST_LANGUAGE_NAME; 7 | 8 | /** 9 | * @author ice1000 10 | */ 11 | public class PestLanguage extends Language { 12 | public static final @NotNull 13 | PestLanguage INSTANCE = new PestLanguage(); 14 | 15 | private PestLanguage() { 16 | super(PEST_LANGUAGE_NAME, "text/" + PEST_LANGUAGE_NAME); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/rs/pest/generation.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import org.junit.Test 4 | 5 | class CodeGeneration { 6 | @Test 7 | fun generateLexer() { 8 | BUILTIN_RULES.forEach { 9 | println("$it { return ${it}_TOKEN; }") 10 | } 11 | } 12 | 13 | @Test 14 | fun generateParser() { 15 | BUILTIN_RULES.forEach { 16 | println(" | ${it}_TOKEN") 17 | } 18 | } 19 | 20 | @Test 21 | fun generateHighlighter() { 22 | BUILTIN_RULES.forEach { 23 | println("PestTypes.${it}_TOKEN,") 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /testData/parse/NestedComment.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PsiComment(block comment)('/*Joesph Joestar/*/**/*/ Oh My God*/') 3 | PsiWhiteSpace('\n') 4 | PestGrammarRuleImpl(GRAMMAR_RULE) 5 | PestValidRuleNameImpl(VALID_RULE_NAME) 6 | PsiElement(IDENTIFIER_TOKEN)('rule') 7 | PsiElement(ASSIGNMENT_OPERATOR)('=') 8 | PestGrammarBodyImpl(GRAMMAR_BODY) 9 | PsiElement(OPENING_BRACE)('{') 10 | PestStringImpl(STRING) 11 | PsiElement(STRING_TOKEN)('""') 12 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /src/rs/pest/livePreview/LivePreviewLanguage.java: -------------------------------------------------------------------------------- 1 | package rs.pest.livePreview; 2 | 3 | import com.intellij.lang.Language; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import static rs.pest.Pest_constantsKt.LP_LANGUAGE_NAME; 7 | 8 | /** 9 | * @author ice1000 10 | */ 11 | public class LivePreviewLanguage extends Language { 12 | public static @NotNull 13 | LivePreviewLanguage INSTANCE = new LivePreviewLanguage(); 14 | 15 | private LivePreviewLanguage() { 16 | super(LP_LANGUAGE_NAME, "text/" + LP_LANGUAGE_NAME); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rust/src/str4j.rs: -------------------------------------------------------------------------------- 1 | use std::alloc::{self, Layout}; 2 | use std::mem; 3 | 4 | #[unsafe(no_mangle)] 5 | pub extern "C" fn alloc(size: usize) -> *mut u8 { 6 | unsafe { 7 | let layout = Layout::from_size_align(size, mem::align_of::()).unwrap(); 8 | alloc::alloc(layout) 9 | } 10 | } 11 | 12 | #[unsafe(no_mangle)] 13 | pub extern "C" fn dealloc(ptr: *mut u8, size: usize) { 14 | unsafe { 15 | let layout = Layout::from_size_align(size, mem::align_of::()).unwrap(); 16 | alloc::dealloc(ptr, layout); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /testData/parse/Issue25.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PsiComment(block comment)('/*\n*\n*/') 3 | PsiWhiteSpace('\n\n') 4 | PestGrammarRuleImpl(GRAMMAR_RULE) 5 | PestValidRuleNameImpl(VALID_RULE_NAME) 6 | PsiElement(IDENTIFIER_TOKEN)('rule') 7 | PsiWhiteSpace(' ') 8 | PsiElement(ASSIGNMENT_OPERATOR)('=') 9 | PsiWhiteSpace(' ') 10 | PestGrammarBodyImpl(GRAMMAR_BODY) 11 | PsiElement(OPENING_BRACE)('{') 12 | PsiWhiteSpace(' ') 13 | PestStringImpl(STRING) 14 | PsiElement(STRING_TOKEN)('" "') 15 | PsiWhiteSpace(' ') 16 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /src/rs/pest/action/ui/PestIdeBridgeInfo.java: -------------------------------------------------------------------------------- 1 | package rs.pest.action.ui; 2 | 3 | import com.intellij.ui.components.labels.LinkLabel; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import javax.swing.*; 7 | 8 | @SuppressWarnings("NullableProblems") 9 | public abstract class PestIdeBridgeInfo { 10 | protected @NotNull JPanel mainPanel; 11 | protected @NotNull LinkLabel websiteLink; 12 | protected @NotNull LinkLabel crateLink; 13 | protected @NotNull JLabel versionLabel; 14 | protected @NotNull JLabel authorLabel; 15 | protected @NotNull JLabel descriptionLabel; 16 | } 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /rust/src/misc.rs: -------------------------------------------------------------------------------- 1 | use std::mem; 2 | 3 | /// Represents a Java string. 4 | pub type JavaStr = *mut u8; 5 | 6 | #[unsafe(no_mangle)] 7 | /// For sanity checking. 8 | pub extern "C" fn connectivity_check_add(a: i32, b: i32) -> i32 { 9 | a + b 10 | } 11 | 12 | #[unsafe(no_mangle)] 13 | pub extern "C" fn crate_info() -> JavaStr { 14 | let version = env!("CARGO_PKG_VERSION"); 15 | let authors = env!("CARGO_PKG_AUTHORS"); 16 | let descrip = env!("CARGO_PKG_DESCRIPTION"); 17 | let s = format!("{}\n{}\n{}", version, authors, descrip); 18 | let p = s.as_ptr(); 19 | mem::forget(s); 20 | p as _ 21 | } 22 | -------------------------------------------------------------------------------- /testData/parse/Char.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PestGrammarRuleImpl(GRAMMAR_RULE) 3 | PestValidRuleNameImpl(VALID_RULE_NAME) 4 | PsiElement(IDENTIFIER_TOKEN)('test') 5 | PsiWhiteSpace(' ') 6 | PsiElement(ASSIGNMENT_OPERATOR)('=') 7 | PsiWhiteSpace(' ') 8 | PestGrammarBodyImpl(GRAMMAR_BODY) 9 | PsiElement(OPENING_BRACE)('{') 10 | PsiWhiteSpace(' ') 11 | PestRangeImpl(RANGE) 12 | PestCharacterImpl(CHARACTER) 13 | PsiElement(CHAR_TOKEN)(''\u{00}'') 14 | PsiElement(RANGE_OPERATOR)('..') 15 | PestCharacterImpl(CHARACTER) 16 | PsiElement(CHAR_TOKEN)(''\u{123456}'') 17 | PsiWhiteSpace(' ') 18 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /res/liveTemplates/Pest.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 24 | -------------------------------------------------------------------------------- /testData/parse/Issue41.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PsiComment(block comment)('/*\n\nConcat : '~~';\nDestruct : '~=';\n\n/* |&! */\n\nDoubleBang : '!!';\nBitNot : '!' | '\uFF01'; //U+FF01 !\n\n/* $ @ */\nCurry : '@@@';\nApply : '@@';\nLetAssign : '@=';\n\n*/') 3 | PsiWhiteSpace('\n\n') 4 | PestGrammarRuleImpl(GRAMMAR_RULE) 5 | PestValidRuleNameImpl(VALID_RULE_NAME) 6 | PsiElement(IDENTIFIER_TOKEN)('rule') 7 | PsiWhiteSpace(' ') 8 | PsiElement(ASSIGNMENT_OPERATOR)('=') 9 | PsiWhiteSpace(' ') 10 | PestGrammarBodyImpl(GRAMMAR_BODY) 11 | PsiElement(OPENING_BRACE)('{') 12 | PsiWhiteSpace(' ') 13 | PestStringImpl(STRING) 14 | PsiElement(STRING_TOKEN)('"114514"') 15 | PsiWhiteSpace(' ') 16 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /src/rs/pest/psi/manipulators.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.psi 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.intellij.psi.AbstractElementManipulator 5 | import rs.pest.psi.impl.PestStringMixin 6 | 7 | class PestStringManipulator : AbstractElementManipulator() { 8 | override fun getRangeInElement(element: PestStringMixin) = getStringTokenRange(element) 9 | override fun handleContentChange(psi: PestStringMixin, range: TextRange, newContent: String): PestStringMixin? { 10 | val oldText = psi.text 11 | val newText = oldText.substring(0, range.startOffset) + newContent + oldText.substring(range.endOffset) 12 | return psi.updateText(newText) 13 | } 14 | 15 | companion object { 16 | fun getStringTokenRange(element: PestStringMixin) = TextRange.from(1, element.textLength - 2) 17 | } 18 | } -------------------------------------------------------------------------------- /res/colorSchemes/Pest_dark.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | 19 | 24 | 30 | 31 | -------------------------------------------------------------------------------- /test/rs/pest/parsing-test.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import com.intellij.testFramework.ParsingTestCase 4 | import org.rust.lang.core.parser.RustParserDefinition 5 | import kotlin.test.Ignore 6 | 7 | class ParsingTest : ParsingTestCase("parse", "pest", PestParserDefinition()) { 8 | override fun getTestDataPath() = "testData" 9 | fun testNestedComment() = doTest(true) 10 | fun testSimpleRule() = doTest(true) 11 | fun testChar() = doTest(true) 12 | fun testBuiltins() = doTest(true) 13 | fun testParen() = doTest(true) 14 | fun testIssue25() = doTest(true) 15 | fun testIssue41() = doTest(true) 16 | } 17 | 18 | /// To inspect Rust plugin's parsed output. 19 | @Ignore 20 | class RustAstStructureTest : ParsingTestCase("rust", "rs", RustParserDefinition()) { 21 | override fun getTestDataPath() = "testData" 22 | fun testExternal() = doTest(true) 23 | fun testInline() = doTest(true) 24 | } 25 | -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-live-templates.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInsight.template.TemplateContextType 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiFile 6 | import com.intellij.psi.util.PsiTreeUtil 7 | import rs.pest.* 8 | import rs.pest.psi.PestGrammarBody 9 | 10 | class PestDefaultContext : TemplateContextType(PEST_DEFAULT_CONTEXT_ID, PEST_LANGUAGE_NAME) { 11 | override fun isInContext(file: PsiFile, offset: Int) = file.fileType == PestFileType 12 | } 13 | 14 | class PestLocalContext : TemplateContextType(PEST_LOCAL_CONTEXT_ID, PEST_LOCAL_CONTEXT_NAME, PestDefaultContext::class.java) { 15 | override fun isInContext(file: PsiFile, offset: Int) = file.fileType == PestFileType && inRule(file.findElementAt(offset)) 16 | private fun inRule(element: PsiElement?) = 17 | PsiTreeUtil.findFirstParent(element) { it is PestGrammarBody } != null 18 | } 19 | -------------------------------------------------------------------------------- /src/rs/pest/format/block.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.format 2 | 3 | import com.intellij.formatting.* 4 | import com.intellij.lang.ASTNode 5 | import com.intellij.psi.TokenType 6 | import com.intellij.psi.formatter.common.AbstractBlock 7 | import rs.pest.psi.childrenWithLeaves 8 | 9 | class PestSimpleBlock( 10 | private val spacing: SpacingBuilder, 11 | node: ASTNode, 12 | wrap: Wrap?, 13 | alignment: Alignment? 14 | ) : AbstractBlock(node, wrap, alignment) { 15 | override fun getSpacing(lhs: Block?, rhs: Block) = spacing.getSpacing(this, lhs, rhs) 16 | override fun isLeaf() = node.firstChildNode == null 17 | override fun getIndent() = Indent.getNoneIndent() 18 | override fun buildChildren(): MutableList = node 19 | .childrenWithLeaves 20 | .filter { it.elementType != TokenType.WHITE_SPACE } 21 | .map { PestSimpleBlock(spacing, it, Wrap.createWrap(WrapType.NONE, false), Alignment.createAlignment()) } 22 | .toMutableList() 23 | } 24 | -------------------------------------------------------------------------------- /rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pest-ide" 3 | version = "0.1.4" 4 | authors = ["ice1000 "] 5 | license = "Apache-2.0" 6 | description = "Helper library for the IntelliJ IDEA plugin for Pest." 7 | categories = ["development-tools"] 8 | keywords = ["pest", "grammar", "ide"] 9 | repository = "https://github.com/pest-parser/intellij-pest/tree/master/rust" 10 | homepage = "https://pest-parser.github.io/" 11 | edition = "2024" 12 | readme = "README.md" 13 | 14 | [badges] 15 | appveyor = { repository = "pest-parser/intellij-pest", service = "github" } 16 | maintenance = { status = "experimental" } 17 | 18 | [lib] 19 | crate-type = ["cdylib"] 20 | 21 | [package.metadata.docs.rs] 22 | rustdoc-args = ["--document-private-items"] 23 | 24 | [dependencies] 25 | pest = "2.8" 26 | pest_meta = "2.8" 27 | pest_vm = "2.8" 28 | 29 | [profile.release] 30 | lto = true 31 | panic = 'abort' 32 | opt-level = 'z' 33 | codegen-units = 1 34 | -------------------------------------------------------------------------------- /res/colorSchemes/Pest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | 21 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2019 2 | 3 | environment: 4 | JAVA_HOME: C:\Program Files\Java\jdk11 5 | JDK_HOME: C:\Program Files\Java\jdk11 6 | JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 7 | # 8 | 9 | install: 10 | - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 11 | - rustup-init -yv --default-toolchain nightly 12 | - refreshenv 13 | - rustup target add wasm32-unknown-unknown --toolchain nightly 14 | - set PATH=%JDK_HOME%\bin;%PATH%;%USERPROFILE%\.cargo\bin 15 | - rustc -vV 16 | - cargo -vV 17 | # - cargo install --git https://github.com/alexcrichton/wasm-gc 18 | 19 | build_script: 20 | - set PATH=%JDK_HOME%\bin;%PATH%;%USERPROFILE%\.cargo\bin 21 | - java -version 22 | - javac -version 23 | - gradlew displayCommitHash buildPlugin --info 24 | - gradlew verifyPlugin --info 25 | # 26 | 27 | cache: 28 | - C:\Users\appveyor\.gradle 29 | 30 | artifacts: 31 | - path: 'build\distributions\*.zip' 32 | name: intellij-pest 33 | # 34 | -------------------------------------------------------------------------------- /testData/rust/External.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | RsStructItemImpl(STRUCT_ITEM) 3 | RsOuterAttrImpl(OUTER_ATTR) 4 | PsiElement(#)('#') 5 | PsiElement([)('[') 6 | RsMetaItemImpl(META_ITEM) 7 | PsiElement(identifier)('derive') 8 | RsMetaItemArgsImpl(META_ITEM_ARGS) 9 | PsiElement(()('(') 10 | RsMetaItemImpl(META_ITEM) 11 | PsiElement(identifier)('Parser') 12 | PsiElement())(')') 13 | PsiElement(])(']') 14 | PsiWhiteSpace('\n') 15 | RsOuterAttrImpl(OUTER_ATTR) 16 | PsiElement(#)('#') 17 | PsiElement([)('[') 18 | RsMetaItemImpl(META_ITEM) 19 | PsiElement(identifier)('grammar') 20 | PsiWhiteSpace(' ') 21 | PsiElement(=)('=') 22 | PsiWhiteSpace(' ') 23 | RsLitExprImpl(LIT_EXPR) 24 | PsiElement(STRING_LITERAL)('"path/to/my_grammar.pest"') 25 | PsiElement(])(']') 26 | PsiWhiteSpace('\n') 27 | PsiElement(struct)('struct') 28 | PsiWhiteSpace(' ') 29 | PsiElement(identifier)('MyParser') 30 | PsiElement(;)(';') -------------------------------------------------------------------------------- /testData/rust/Inline.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | RsStructItemImpl(STRUCT_ITEM) 3 | RsOuterAttrImpl(OUTER_ATTR) 4 | PsiElement(#)('#') 5 | PsiElement([)('[') 6 | RsMetaItemImpl(META_ITEM) 7 | PsiElement(identifier)('derive') 8 | RsMetaItemArgsImpl(META_ITEM_ARGS) 9 | PsiElement(()('(') 10 | RsMetaItemImpl(META_ITEM) 11 | PsiElement(identifier)('Parser') 12 | PsiElement())(')') 13 | PsiElement(])(']') 14 | PsiWhiteSpace('\n') 15 | RsOuterAttrImpl(OUTER_ATTR) 16 | PsiElement(#)('#') 17 | PsiElement([)('[') 18 | RsMetaItemImpl(META_ITEM) 19 | PsiElement(identifier)('grammar_inline') 20 | PsiWhiteSpace(' ') 21 | PsiElement(=)('=') 22 | PsiWhiteSpace(' ') 23 | RsLitExprImpl(LIT_EXPR) 24 | PsiElement(STRING_LITERAL)('"\\nmy_rule = { \"\" }\n"') 25 | PsiElement(])(']') 26 | PsiWhiteSpace('\n') 27 | PsiElement(struct)('struct') 28 | PsiWhiteSpace(' ') 29 | PsiElement(identifier)('MyParser') 30 | PsiElement(;)(';') -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-completion.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInsight.completion.* 4 | import com.intellij.codeInsight.lookup.LookupElementBuilder 5 | import com.intellij.patterns.PlatformPatterns 6 | import com.intellij.util.ProcessingContext 7 | import icons.PestIcons 8 | import rs.pest.BUILTIN_RULES 9 | import rs.pest.psi.PestGrammarBody 10 | 11 | class PestBuiltinCompletionContributor : CompletionContributor() { 12 | private val builtin = BUILTIN_RULES.map { 13 | LookupElementBuilder 14 | .create(it) 15 | .withTypeText("Builtin") 16 | .withIcon(PestIcons.PEST) 17 | } 18 | 19 | init { 20 | val provider = object : CompletionProvider() { 21 | override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) { 22 | builtin.forEach(result::addElement) 23 | } 24 | } 25 | extend(CompletionType.BASIC, PlatformPatterns 26 | .psiElement() 27 | .inside(PlatformPatterns 28 | .psiElement(PestGrammarBody::class.java)), 29 | provider) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/rs/pest/psi/PestStringEscaperTest.java: -------------------------------------------------------------------------------- 1 | package rs.pest.psi; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNotEquals; 7 | 8 | public class PestStringEscaperTest { 9 | 10 | @Test 11 | public void unicode() { 12 | assertEquals(PestStringEscaper.unicode("\\u{0}", new StringBuilder(), 2), -1); 13 | assertNotEquals(PestStringEscaper.unicode("\\u{01}", new StringBuilder(), 2), -1); 14 | assertNotEquals(PestStringEscaper.unicode("\\u{022}", new StringBuilder(), 2), -1); 15 | assertNotEquals(PestStringEscaper.unicode("\\u{0223}", new StringBuilder(), 2), -1); 16 | assertNotEquals(PestStringEscaper.unicode("\\u{02234}", new StringBuilder(), 2), -1); 17 | assertNotEquals(PestStringEscaper.unicode("\\u{022345}", new StringBuilder(), 2), -1); 18 | assertEquals(PestStringEscaper.unicode("\\u{022345699}", new StringBuilder(), 2), -1); 19 | 20 | StringBuilder builder = new StringBuilder(); 21 | String inString = "\\u{1234}"; 22 | int outIndex = PestStringEscaper.unicode(inString, builder, 2); 23 | assertEquals("\u1234", builder.toString()); 24 | assertEquals(inString.length(), outIndex); 25 | } 26 | } -------------------------------------------------------------------------------- /src/rs/pest/action/ui/PestIntroduceRulePopup.java: -------------------------------------------------------------------------------- 1 | package rs.pest.action.ui; 2 | 3 | import com.intellij.openapi.editor.Editor; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.psi.PsiNamedElement; 6 | import com.intellij.refactoring.introduce.inplace.InplaceVariableIntroducer; 7 | import org.jetbrains.annotations.NotNull; 8 | import org.jetbrains.annotations.Nullable; 9 | import rs.pest.PestBundle; 10 | import rs.pest.psi.PestExpression; 11 | 12 | import javax.swing.*; 13 | 14 | @SuppressWarnings("NullableProblems") 15 | public abstract class PestIntroduceRulePopup extends InplaceVariableIntroducer { 16 | protected @NotNull JPanel mainPanel; 17 | protected @NotNull JRadioButton atomic; 18 | protected @NotNull JRadioButton compoundAtomic; 19 | protected @NotNull JRadioButton silent; 20 | protected @NotNull JRadioButton nonAtomic; 21 | protected @NotNull JRadioButton normal; 22 | 23 | public PestIntroduceRulePopup(PsiNamedElement elementToRename, Editor editor, Project project, @Nullable PestExpression expr) { 24 | super(elementToRename, editor, project, PestBundle.message("pest.actions.extract.rule.popup.title"), new PestExpression[0], expr); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rs/pest/pest-parser-def.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import com.intellij.lang.ASTNode 4 | import com.intellij.lang.ParserDefinition 5 | import com.intellij.lexer.FlexAdapter 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.psi.FileViewProvider 8 | import com.intellij.psi.PsiElement 9 | import com.intellij.psi.stubs.PsiFileStubImpl 10 | import com.intellij.psi.tree.IStubFileElementType 11 | import rs.pest.psi.PestLexer 12 | import rs.pest.psi.PestParser 13 | import rs.pest.psi.PestTokenType 14 | import rs.pest.psi.PestTypes 15 | 16 | fun lexer() = FlexAdapter(PestLexer()) 17 | 18 | class PestParserDefinition : ParserDefinition { 19 | private companion object { 20 | private val FILE = IStubFileElementType>(PestLanguage.INSTANCE) 21 | } 22 | 23 | override fun createParser(project: Project?) = PestParser() 24 | override fun createLexer(project: Project?) = lexer() 25 | override fun createElement(node: ASTNode?): PsiElement = PestTypes.Factory.createElement(node) 26 | override fun createFile(viewProvider: FileViewProvider) = PestFile(viewProvider) 27 | override fun getStringLiteralElements() = PestTokenType.STRINGS 28 | override fun getWhitespaceTokens() = PestTokenType.WHITE_SPACE 29 | override fun getCommentTokens() = PestTokenType.COMMENTS 30 | override fun getFileNodeType() = FILE 31 | } 32 | -------------------------------------------------------------------------------- /src/rs/pest/rust/rust-inject.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.rust 2 | 3 | import com.intellij.openapi.util.TextRange 4 | import com.intellij.psi.InjectedLanguagePlaces 5 | import com.intellij.psi.LanguageInjector 6 | import com.intellij.psi.PsiLanguageInjectionHost 7 | import com.intellij.psi.PsiWhiteSpace 8 | import org.rust.lang.core.psi.RsLitExpr 9 | import org.rust.lang.core.psi.RsMetaItem 10 | import org.rust.lang.core.psi.RsOuterAttr 11 | import rs.pest.PestLanguage 12 | import rs.pest.psi.childrenWithLeaves 13 | 14 | class InlineGrammarInjector : LanguageInjector { 15 | override fun getLanguagesToInject(host: PsiLanguageInjectionHost, places: InjectedLanguagePlaces) { 16 | if (host !is RsLitExpr) return 17 | if (isInlineGrammar(host)) places.addPlace(PestLanguage.INSTANCE, TextRange(1, host.textLength - 1), null, null) 18 | } 19 | 20 | /// Do we need to check the existence of `#[derive(Parser)]` as well? 21 | private fun isInlineGrammar(host: RsLitExpr): Boolean { 22 | // Should be inside of a #[] 23 | val parent = host.parent as? RsMetaItem ?: return false 24 | // Should have a #[derive(Parser)] before 25 | if (parent.parent !is RsOuterAttr) return false 26 | // Should be grammar_inline = "bla" 27 | val peers = parent.childrenWithLeaves.filter { it !is PsiWhiteSpace }.toList() 28 | if (peers.size != 3) return false 29 | if (peers[0].text != "grammar_inline") return false 30 | if (peers[1].text != "=") return false 31 | return peers[2] === host 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testData/parse/SimpleRule.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PestGrammarRuleImpl(GRAMMAR_RULE) 3 | PestValidRuleNameImpl(VALID_RULE_NAME) 4 | PsiElement(IDENTIFIER_TOKEN)('rule') 5 | PsiWhiteSpace(' ') 6 | PsiElement(ASSIGNMENT_OPERATOR)('=') 7 | PsiWhiteSpace(' ') 8 | PestGrammarBodyImpl(GRAMMAR_BODY) 9 | PsiElement(OPENING_BRACE)('{') 10 | PsiWhiteSpace(' ') 11 | PestExpressionImpl(EXPRESSION) 12 | PestIdentifierImpl(IDENTIFIER) 13 | PsiElement(IDENTIFIER_TOKEN)('rule') 14 | PsiWhiteSpace(' ') 15 | PestInfixOperatorImpl(INFIX_OPERATOR) 16 | PsiElement(CHOICE_OPERATOR)('|') 17 | PsiWhiteSpace(' ') 18 | PestStringImpl(STRING) 19 | PsiElement(STRING_TOKEN)('""') 20 | PsiWhiteSpace(' ') 21 | PestInfixOperatorImpl(INFIX_OPERATOR) 22 | PsiElement(SEQUENCE_OPERATOR)('~') 23 | PsiWhiteSpace(' ') 24 | PestRangeImpl(RANGE) 25 | PestCharacterImpl(CHARACTER) 26 | PsiElement(CHAR_TOKEN)(''a'') 27 | PsiElement(RANGE_OPERATOR)('..') 28 | PestCharacterImpl(CHARACTER) 29 | PsiElement(CHAR_TOKEN)(''z'') 30 | PsiWhiteSpace(' ') 31 | PestInfixOperatorImpl(INFIX_OPERATOR) 32 | PsiElement(CHOICE_OPERATOR)('|') 33 | PsiWhiteSpace(' ') 34 | PestStringImpl(STRING) 35 | PsiElement(STRING_TOKEN)('"bla"') 36 | PsiWhiteSpace(' ') 37 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-gutter.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInsight.daemon.LineMarkerInfo 4 | import com.intellij.codeInsight.daemon.LineMarkerProvider 5 | import com.intellij.icons.AllIcons 6 | import com.intellij.openapi.actionSystem.AnAction 7 | import com.intellij.openapi.editor.markup.GutterIconRenderer 8 | import com.intellij.psi.PsiElement 9 | import com.intellij.util.FunctionUtil 10 | import rs.pest.psi.impl.PestGrammarRuleMixin 11 | 12 | class PestRecursionLineMarkerProvider : LineMarkerProvider { 13 | override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? = null 14 | override fun collectSlowLineMarkers(elements: List, result: MutableCollection>) { 15 | elements 16 | .asSequence() 17 | .filterIsInstance() 18 | .filter(PestGrammarRuleMixin::isRecursive) 19 | .mapTo(result, ::RecMarkerInfo) 20 | } 21 | } 22 | 23 | /// This constructor is deprecated but #6 24 | @Suppress("DEPRECATION") 25 | private class RecMarkerInfo constructor(id: PsiElement) : LineMarkerInfo( 26 | id, id.textRange, AllIcons.Gutter.RecursiveMethod, 27 | FunctionUtil.constant("Recursive rule"), null, 28 | GutterIconRenderer.Alignment.RIGHT) { 29 | override fun createGutterRenderer(): LineMarkerGutterIconRenderer? = 30 | if (myIcon == null) null else object : LineMarkerGutterIconRenderer(this) { 31 | override fun getClickAction(): AnAction? = null 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /testData/parse/Paren.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PestGrammarRuleImpl(GRAMMAR_RULE) 3 | PestValidRuleNameImpl(VALID_RULE_NAME) 4 | PsiElement(IDENTIFIER_TOKEN)('a') 5 | PsiWhiteSpace(' ') 6 | PsiElement(ASSIGNMENT_OPERATOR)('=') 7 | PsiWhiteSpace(' ') 8 | PestGrammarBodyImpl(GRAMMAR_BODY) 9 | PsiElement(OPENING_BRACE)('{') 10 | PsiWhiteSpace(' ') 11 | PestExpressionImpl(EXPRESSION) 12 | PestIdentifierImpl(IDENTIFIER) 13 | PsiElement(IDENTIFIER_TOKEN)('a') 14 | PsiWhiteSpace(' ') 15 | PestInfixOperatorImpl(INFIX_OPERATOR) 16 | PsiElement(SEQUENCE_OPERATOR)('~') 17 | PsiWhiteSpace(' ') 18 | PestRuleImpl(RULE) 19 | PsiElement(OPENING_PAREN)('(') 20 | PestExpressionImpl(EXPRESSION) 21 | PestIdentifierImpl(IDENTIFIER) 22 | PsiElement(IDENTIFIER_TOKEN)('rule') 23 | PsiWhiteSpace(' ') 24 | PestInfixOperatorImpl(INFIX_OPERATOR) 25 | PsiElement(SEQUENCE_OPERATOR)('~') 26 | PsiWhiteSpace(' ') 27 | PestIdentifierImpl(IDENTIFIER) 28 | PsiElement(IDENTIFIER_TOKEN)('b') 29 | PsiElement(CLOSING_PAREN)(')') 30 | PsiWhiteSpace(' ') 31 | PestInfixOperatorImpl(INFIX_OPERATOR) 32 | PsiElement(SEQUENCE_OPERATOR)('~') 33 | PsiWhiteSpace(' ') 34 | PestIdentifierImpl(IDENTIFIER) 35 | PsiElement(IDENTIFIER_TOKEN)('c') 36 | PsiWhiteSpace(' ') 37 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /rust/README.md: -------------------------------------------------------------------------------- 1 | # pest-ide 2 | 3 | The Rust-interop bridge for the [IntelliJ IDEA plugin for Pest][jb]. 4 | Note that this README is only about the Rust bridge crate, not the IDE plugin. 5 | 6 | For the plugin please head to the [plugin page][plugin]. 7 | 8 | [![Crates.io][cr-svg]][cr-url] 9 | [![Crates.io][cv-svg]][cr-url] 10 | [![Docs.rs][dv-svg]][dv-url] 11 | [![JB][v-svg]][jb] 12 | [![Build Status][tv-svg]][tv-url] 13 | [![Build status][av-svg]][av-url] 14 | 15 | [cr-svg]: https://img.shields.io/crates/d/pest-ide.svg 16 | [cr-url]: https://crates.io/crates/pest-ide 17 | [cv-svg]: https://img.shields.io/crates/v/pest-ide.svg 18 | [d-svg]: https://img.shields.io/jetbrains/plugin/d/12046-pest.svg 19 | [v-svg]: https://img.shields.io/jetbrains/plugin/v/12046-pest.svg 20 | [jb]: https://plugins.jetbrains.com/plugin/12046-pest 21 | [tv-url]: https://travis-ci.org/pest-parser/intellij-pest 22 | [dv-svg]: https://docs.rs/pest-ide/badge.svg 23 | [dv-url]: https://docs.rs/pest-ide 24 | [tv-svg]: https://travis-ci.org/pest-parser/intellij-pest.svg?branch=master 25 | [av-url]: https://ci.appveyor.com/project/dragostis/intellij-pest-3fx8c/branch/master 26 | [av-svg]: https://img.shields.io/appveyor/ci/dragostis/intellij-pest-3fx8c/master.svg?label=appveyor 27 | [plugin]: https://github.com/pest-parser/intellij-pest 28 | 29 | There's a video about the *Live Preview* functionality, 30 | which is based on this bridge. 31 | 32 | + [YouTube][YouTube] 33 | + [Bilibili][Bilibili] 34 | 35 | [YouTube]: https://www.youtube.com/watch?v=AnUhekAENm4 36 | [Bilibili]: https://www.bilibili.com/video/av49762905/ 37 | -------------------------------------------------------------------------------- /src/rs/pest/format/model.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.format 2 | 3 | import com.intellij.formatting.* 4 | import com.intellij.lang.ASTNode 5 | import com.intellij.openapi.util.TextRange 6 | import com.intellij.psi.PsiFile 7 | import com.intellij.psi.codeStyle.CodeStyleSettings 8 | import rs.pest.PestLanguage 9 | import rs.pest.psi.PestTypes 10 | 11 | class PestFormattingModelBuilder : FormattingModelBuilder { 12 | override fun createModel(formattingContext: FormattingContext): FormattingModel { 13 | return FormattingModelProvider 14 | .createFormattingModelForPsiFile( 15 | formattingContext.containingFile, 16 | PestSimpleBlock( 17 | createSpaceBuilder(formattingContext.codeStyleSettings), 18 | formattingContext.node, 19 | Wrap.createWrap(WrapType.NONE, false), 20 | Alignment.createAlignment(true) 21 | ), 22 | formattingContext.codeStyleSettings 23 | ) 24 | } 25 | 26 | private fun createSpaceBuilder(settings: CodeStyleSettings) = SpacingBuilder(settings, PestLanguage.INSTANCE) 27 | .around(PestTypes.CHOICE_OPERATOR).spaces(1) 28 | .around(PestTypes.SEQUENCE_OPERATOR).spaces(1) 29 | .around(PestTypes.ASSIGNMENT_OPERATOR).spaces(1) 30 | .before(PestTypes.OPENING_PAREN).spaces(1) 31 | .before(PestTypes.OPENING_BRACE).spaces(1) 32 | .after(PestTypes.OPTIONAL_OPERATOR).spaces(1) 33 | .after(PestTypes.REPEAT_OPERATOR).spaces(1) 34 | .after(PestTypes.REPEAT_ONCE_OPERATOR).spaces(1) 35 | .after(PestTypes.CLOSING_PAREN).spaces(1) 36 | .after(PestTypes.CLOSING_BRACE).none() 37 | 38 | override fun getRangeAffectingIndent(file: PsiFile, offset: Int, elementAtOffset: ASTNode): TextRange? = null 39 | } 40 | -------------------------------------------------------------------------------- /src/rs/pest/psi/utils.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.psi 2 | 3 | import com.intellij.lang.ASTNode 4 | import com.intellij.openapi.util.TextRange 5 | import com.intellij.psi.PsiDocumentManager 6 | import com.intellij.psi.PsiElement 7 | import com.intellij.psi.PsiWhiteSpace 8 | import com.intellij.psi.tree.IElementType 9 | 10 | val PsiElement.childrenWithLeaves: Sequence 11 | get() = generateSequence(this.firstChild) { it.nextSibling } 12 | 13 | val ASTNode.childrenWithLeaves: Sequence 14 | get() = generateSequence(this.firstChildNode) { it.treeNext } 15 | 16 | val PsiElement.startOffset: Int 17 | get() = textRange.startOffset 18 | 19 | val PsiElement.elementType: IElementType? 20 | get() = node?.elementType 21 | 22 | val PsiElement.endOffset: Int 23 | get() = textRange.endOffset 24 | 25 | val PsiElement.endOffsetInParent: Int 26 | get() = startOffsetInParent + textLength 27 | 28 | fun PsiElement.rangeWithPrevSpace(prev: PsiElement?) = when (prev) { 29 | is PsiWhiteSpace -> textRange.union(prev.textRange) 30 | else -> textRange 31 | } 32 | 33 | val PsiElement.rangeWithPrevSpace: TextRange 34 | get() = rangeWithPrevSpace(prevSibling) 35 | 36 | private fun PsiElement.getLineCount(): Int { 37 | val doc = containingFile?.let { file -> PsiDocumentManager.getInstance(project).getDocument(file) } 38 | if (doc != null) { 39 | val spaceRange = textRange ?: TextRange.EMPTY_RANGE 40 | 41 | if (spaceRange.endOffset <= doc.textLength) { 42 | val startLine = doc.getLineNumber(spaceRange.startOffset) 43 | val endLine = doc.getLineNumber(spaceRange.endOffset) 44 | 45 | return endLine - startLine 46 | } 47 | } 48 | 49 | return (text ?: "").count { it == '\n' } + 1 50 | } 51 | 52 | fun PsiWhiteSpace.isMultiLine(): Boolean = getLineCount() > 1 53 | -------------------------------------------------------------------------------- /res/rs/pest/error/report-bundle.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | # 3 | # Copyright (c) 2017 Patrick Scheibe 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | # THE SOFTWARE. 21 | # 22 | 23 | report.error.to.plugin.vendor=Report error on GitHub 24 | report.error.progress.dialog.text=Submitting error report... 25 | report.error.connection.failure=Could not communicate with GitHub 26 | 27 | git.issue.title=[auto-generated:{0}] {1} 28 | git.issue.label=auto-generated 29 | git.issue.text=Created issue {1}. Thanks for your feedback, please comment \ 30 | (to further discuss with us) if possible! 31 | git.issue.duplicate.text=A similar issues was already reported (#{1}). \ 32 | Thanks for your feedback! 33 | -------------------------------------------------------------------------------- /src/rs/pest/action/ui/RuleSelector.form: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /testData/parse/Builtins.txt: -------------------------------------------------------------------------------- 1 | FILE 2 | PsiComment(line comment)('//') 3 | PsiWhiteSpace('\n') 4 | PsiComment(line comment)('// Created by intellij-pest on 2019-03-22') 5 | PsiWhiteSpace('\n') 6 | PsiComment(line comment)('// Builtins') 7 | PsiWhiteSpace('\n') 8 | PsiComment(line comment)('// Author: ice1000') 9 | PsiWhiteSpace('\n') 10 | PsiComment(line comment)('//') 11 | PsiWhiteSpace('\n\n') 12 | PestGrammarRuleImpl(GRAMMAR_RULE) 13 | PestValidRuleNameImpl(VALID_RULE_NAME) 14 | PsiElement(IDENTIFIER_TOKEN)('builtins') 15 | PsiWhiteSpace(' ') 16 | PsiElement(ASSIGNMENT_OPERATOR)('=') 17 | PsiWhiteSpace(' ') 18 | PestGrammarBodyImpl(GRAMMAR_BODY) 19 | PsiElement(OPENING_BRACE)('{') 20 | PsiWhiteSpace(' ') 21 | PestStringImpl(STRING) 22 | PsiElement(STRING_TOKEN)('"Hello World!"') 23 | PsiWhiteSpace(' ') 24 | PsiElement(CLOSING_BRACE)('}') 25 | PsiWhiteSpace('\n') 26 | PestGrammarRuleImpl(GRAMMAR_RULE) 27 | PestCustomizableRuleNameImpl(CUSTOMIZABLE_RULE_NAME) 28 | PsiElement(COMMENT_TOKEN)('COMMENT') 29 | PsiWhiteSpace(' ') 30 | PsiElement(ASSIGNMENT_OPERATOR)('=') 31 | PsiWhiteSpace(' ') 32 | PestGrammarBodyImpl(GRAMMAR_BODY) 33 | PsiElement(OPENING_BRACE)('{') 34 | PsiWhiteSpace(' ') 35 | PestBuiltinImpl(BUILTIN) 36 | PsiElement(ID_CONTINUE_TOKEN)('ID_CONTINUE') 37 | PsiWhiteSpace(' ') 38 | PsiElement(CLOSING_BRACE)('}') 39 | PsiWhiteSpace('\n') 40 | PestGrammarRuleImpl(GRAMMAR_RULE) 41 | PestCustomizableRuleNameImpl(CUSTOMIZABLE_RULE_NAME) 42 | PsiElement(WHITESPACE_TOKEN)('WHITESPACE') 43 | PsiWhiteSpace(' ') 44 | PsiElement(ASSIGNMENT_OPERATOR)('=') 45 | PsiWhiteSpace(' ') 46 | PestGrammarBodyImpl(GRAMMAR_BODY) 47 | PsiElement(OPENING_BRACE)('{') 48 | PsiWhiteSpace(' ') 49 | PestBuiltinImpl(BUILTIN) 50 | PsiElement(WHITE_SPACE_TOKEN)('WHITE_SPACE') 51 | PsiWhiteSpace(' ') 52 | PsiElement(CLOSING_BRACE)('}') -------------------------------------------------------------------------------- /src/rs/pest/action/create-file.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.action 2 | 3 | import com.intellij.ide.actions.CreateFileFromTemplateAction 4 | import com.intellij.ide.actions.CreateFileFromTemplateDialog 5 | import com.intellij.ide.fileTemplates.FileTemplate 6 | import com.intellij.ide.fileTemplates.FileTemplateManager 7 | import com.intellij.ide.fileTemplates.actions.AttributesDefaults 8 | import com.intellij.ide.fileTemplates.ui.CreateFromTemplateDialog 9 | import com.intellij.openapi.project.DumbAware 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.openapi.util.io.FileUtilRt 12 | import com.intellij.psi.PsiDirectory 13 | import icons.PestIcons 14 | import rs.pest.PestBundle 15 | import java.util.* 16 | import java.util.Locale 17 | 18 | class NewPestFile : CreateFileFromTemplateAction( 19 | PestBundle.message("pest.actions.new-file.name"), 20 | PestBundle.message("pest.actions.new-file.description"), 21 | PestIcons.PEST_FILE), DumbAware { 22 | companion object { 23 | fun createProperties(project: Project, className: String): Properties { 24 | val properties = FileTemplateManager.getInstance(project).defaultProperties 25 | properties += "NAME" to className 26 | properties += "NAME_SNAKE" to className.lowercase(Locale.getDefault()).replace(Regex("[ \r\t-()!@#~]+"), "_") 27 | return properties 28 | } 29 | } 30 | 31 | override fun getActionName(directory: PsiDirectory?, s: String, s2: String?) = 32 | PestBundle.message("pest.actions.new-file.name") 33 | 34 | override fun buildDialog(project: Project, directory: PsiDirectory, builder: CreateFileFromTemplateDialog.Builder) { 35 | builder 36 | .setTitle(PestBundle.message("pest.actions.new-file.title")) 37 | .addKind("File", PestIcons.PEST_FILE, "Pest File") 38 | } 39 | 40 | override fun createFileFromTemplate(name: String, template: FileTemplate, dir: PsiDirectory) = try { 41 | val className = FileUtilRt.getNameWithoutExtension(name) 42 | val project = dir.project 43 | val properties = createProperties(project, className) 44 | CreateFromTemplateDialog(project, dir, template, AttributesDefaults(className).withFixedName(true), properties) 45 | .create() 46 | .containingFile 47 | } catch (e: Exception) { 48 | LOG.error("Error while creating new file", e) 49 | null 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/rs/pest/pest-infos.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import com.intellij.AbstractBundle 4 | import com.intellij.extapi.psi.PsiFileBase 5 | import com.intellij.openapi.fileTypes.LanguageFileType 6 | import com.intellij.openapi.util.TextRange 7 | import com.intellij.psi.FileViewProvider 8 | import com.intellij.psi.PsiElement 9 | import icons.PestIcons 10 | import org.jetbrains.annotations.NonNls 11 | import org.jetbrains.annotations.PropertyKey 12 | import rs.pest.livePreview.Lib 13 | import rs.pest.livePreview.LivePreviewFile 14 | import rs.pest.psi.impl.PestGrammarRuleMixin 15 | import java.util.* 16 | 17 | object PestFileType : LanguageFileType(PestLanguage.INSTANCE) { 18 | override fun getDefaultExtension() = PEST_EXTENSION 19 | override fun getName() = PestBundle.message("pest.name") 20 | override fun getIcon() = PestIcons.PEST_FILE 21 | override fun getDescription() = PestBundle.message("pest.name.description") 22 | } 23 | 24 | class PestFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, PestLanguage.INSTANCE) { 25 | override fun getFileType() = PestFileType 26 | override fun subtreeChanged() { 27 | super.subtreeChanged() 28 | rulesCache = null 29 | } 30 | 31 | /** This information is from the Psi system. */ 32 | private var rulesCache: List? = null 33 | var livePreviewFile: MutableList = mutableListOf() 34 | /** This information is from Pest VM. */ 35 | var errors: Sequence> = emptySequence() 36 | /** This information is from Pest VM. */ 37 | var availableRules: Sequence = emptySequence() 38 | val vm = Lib(1926417 + 1919810) 39 | fun rebootVM() = vm.reboot() 40 | fun reloadVM(code: String = text) = vm.loadVM(code) 41 | var isDocumentListenerAdded = false 42 | fun rules() = rulesCache ?: calcRules().also { rulesCache = it } 43 | fun livePreviewFile() = livePreviewFile.also { it.retainAll(PsiElement::isValid) } 44 | private fun calcRules() = children.filterIsInstance() 45 | } 46 | 47 | object PestBundle { 48 | @NonNls 49 | private const val BUNDLE = "rs.pest.pest-bundle" 50 | private val bundle: ResourceBundle by lazy { ResourceBundle.getBundle(BUNDLE) } 51 | 52 | @JvmStatic 53 | fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any) = 54 | AbstractBundle.message(bundle, key, *params) 55 | } 56 | -------------------------------------------------------------------------------- /res/META-INF/description.html: -------------------------------------------------------------------------------- 1 | Pest grammar language support. 2 |
3 | 4 |

5 | You're encouraged to contribute to the plugin in any form if you've found any issues or missing functionality that 6 | you'd want to see. 7 |

8 | There are tutorial videos on both 9 | YouTube and 10 | Bilibili. 11 |

12 | All your issues/pull requests will be replied very quickly. 13 |


14 | 15 | Features as a standalone plugin:
16 |
    17 |
  • This is a dynamic plugin!
  • 18 |
  • File icon, different for dark and bright themes
  • 19 |
  • Rich completion for rules
  • 20 |
  • Live Preview -- syntax highlight with pest on-the-fly!
  • 21 |
  • Live Preview to HTML -- export your live-previewed file as HTML!
  • 22 |
  • Separated highlighting for different rule types
  • 23 |
  • Keyword highlight built-in rules
  • 24 |
  • Rename for rules (and validate your rename!)
  • 25 |
  • Backspace deletes corresponding parenthesis/bracket/brace as well
  • 26 |
  • Click to go to definition for rules
  • 27 |
  • GitHub error reporter
  • 28 |
  • Structure view
  • 29 |
  • Rule folding
  • 30 |
  • Find usages
  • 31 |
  • String literal injection
  • 32 |
  • Quote handler (automatic insert paired quote)
  • 33 |
  • Spell checker (for comments/rule names, strings are suppressed)
  • 34 |
  • Recursive rule line marker
  • 35 |
  • Duplicated rule checker
  • 36 |
  • Rule inline (this is very fancy!)
  • 37 |
  • Rule extraction (this is currently poorly implemented)
  • 38 |
  • Live template completion for COMMENT and WHITESPACE
  • 39 |
  • Completion for built-in rules
  • 40 |

41 | 42 | Features when co-installed with Rust plugin:
43 |
    44 |
  • Automatically highlight pest code in #[grammar_inline = "..."]
  • 45 |
46 | 47 | Hope one day this plugin can be integrated with the Rust plugin and provide completion/resolving for something like 48 | Rule::rule_name. 49 |
50 | 51 |

Maintainers:

52 |
    53 |
  • @ice1000
  • 54 |
55 |

Contributors:

56 |
    57 |
  • @MalteSchledjewski
  • 58 |
59 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-structure.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.ide.structureView.StructureViewModel 4 | import com.intellij.ide.structureView.StructureViewModelBase 5 | import com.intellij.ide.structureView.StructureViewTreeElement 6 | import com.intellij.ide.structureView.TreeBasedStructureViewBuilder 7 | import com.intellij.ide.util.treeView.smartTree.SortableTreeElement 8 | import com.intellij.lang.PsiStructureViewFactory 9 | import com.intellij.navigation.ItemPresentation 10 | import com.intellij.openapi.editor.Editor 11 | import com.intellij.pom.Navigatable 12 | import com.intellij.psi.NavigatablePsiElement 13 | import com.intellij.psi.PsiFile 14 | import com.intellij.psi.PsiNamedElement 15 | import rs.pest.PestFile 16 | import rs.pest.psi.impl.PestGrammarRuleMixin 17 | 18 | fun cutText(it: String, textMax: Int) = if (it.length <= textMax) it else "${it.take(textMax)}…" 19 | 20 | class PestStructureViewModel(root: PsiFile, editor: Editor?) : 21 | StructureViewModelBase(root, editor, PestStructureViewElement(root)), 22 | StructureViewModel.ElementInfoProvider { 23 | init { 24 | withSuitableClasses(PestFile::class.java, PestGrammarRuleMixin::class.java) 25 | } 26 | 27 | override fun shouldEnterElement(o: Any?) = true 28 | override fun isAlwaysShowsPlus(element: StructureViewTreeElement) = false 29 | override fun isAlwaysLeaf(element: StructureViewTreeElement) = element is PestGrammarRuleMixin 30 | } 31 | 32 | class PestStructureViewElement(private val root: NavigatablePsiElement) : 33 | StructureViewTreeElement, ItemPresentation, SortableTreeElement, Navigatable by root { 34 | override fun getLocationString() = "" 35 | override fun getIcon(open: Boolean) = root.getIcon(0) 36 | override fun getPresentableText() = when (root) { 37 | is PestGrammarRuleMixin -> root.name 38 | is PestFile -> cutText(root.name, 18) 39 | else -> "Unknown" 40 | } 41 | 42 | override fun getPresentation() = this 43 | override fun getValue() = root 44 | override fun getAlphaSortKey() = (root as? PsiNamedElement)?.name.orEmpty() 45 | override fun getChildren() = root 46 | .children 47 | .filterIsInstance() 48 | .map { PestStructureViewElement(it) } 49 | .toTypedArray() 50 | } 51 | 52 | class PestStructureViewFactory : PsiStructureViewFactory { 53 | override fun getStructureViewBuilder(file: PsiFile) = object : TreeBasedStructureViewBuilder() { 54 | override fun isRootNodeShown() = true 55 | override fun createStructureViewModel(editor: Editor?) = PestStructureViewModel(file, editor) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/rs/pest/rust-interaction-test.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import org.junit.Test 4 | import rs.pest.livePreview.Lib 5 | import rs.pest.livePreview.Rendering 6 | import rs.pest.vm.PestUtil 7 | import kotlin.random.Random 8 | import kotlin.test.assertEquals 9 | import kotlin.test.assertFalse 10 | import kotlin.test.assertTrue 11 | 12 | class InteractionTest { 13 | @Test 14 | fun `connectivity -- a + b problem`() { 15 | val random = Random(System.currentTimeMillis()) 16 | val instance = PestUtil(1919810) 17 | repeat(10) { 18 | val a = random.nextInt() 19 | val b = random.nextInt() 20 | assertEquals(a + b, instance.connectivity_check_add(a, b)) 21 | } 22 | } 23 | } 24 | 25 | class IntegrationTest { 26 | @Test 27 | fun `load Pest VM`() { 28 | val lib = Lib(1919810) 29 | val (works, output) = lib.loadVM("""a = { "Hello" }""") 30 | assertEquals(listOf("a"), output.toList()) 31 | assertTrue(works) 32 | } 33 | 34 | @Test 35 | fun `load Pest VM with multiple rules`() { 36 | val lib = Lib(1919810) 37 | val (works, output) = lib.loadVM(""" 38 | a = { "Hello" } 39 | b = { a } 40 | """) 41 | assertEquals(listOf("a", "b"), output.toList()) 42 | assertTrue(works) 43 | } 44 | 45 | @Test 46 | fun `load Pest VM with invalid rule name`() { 47 | val lib = Lib(1919810) 48 | val (parses, output) = lib.loadVM("""type = { "Hello" }""") 49 | assertFalse(parses) 50 | assertEquals(listOf("1^1^1^5^type is a rust keyword"), output.toList()) 51 | } 52 | 53 | @Test 54 | fun `load Pest VM with syntax error`() { 55 | val lib = Lib(1919810) 56 | val (parses, output) = lib.loadVM("""bla = { "Hello }""") 57 | assertFalse(parses) 58 | assertEquals(listOf("""1^17^1^17^expected `\"`"""), output.toList()) 59 | } 60 | 61 | @Test 62 | fun `render simple code in Pest VM`() { 63 | val lib = Lib(1919810) 64 | val (parses, output) = lib.loadVM("""bla = { "Dio" }""") 65 | assertTrue(parses) 66 | assertEquals(listOf("bla"), output.toList()) 67 | val renderCode = lib.renderCode("bla", "Dio") as Rendering.Ok 68 | assertEquals(listOf("0^3^bla"), renderCode.lexical.toList()) 69 | } 70 | 71 | @Test 72 | fun `render syntactically incorrect code in Pest VM`() { 73 | val lib = Lib(1919810) 74 | val (parses, output) = lib.loadVM("""bla = { "Hello" }""") 75 | assertTrue(parses) 76 | assertEquals(listOf("bla"), output.toList()) 77 | val renderCode = lib.renderCode("bla", "The World!") as Rendering.Err 78 | assertEquals(""" --> 1:1 79 | | 80 | 1 | The World! 81 | | ^--- 82 | | 83 | = expected bla""", renderCode.msg) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intellij-pest 2 | 3 | [![JB][d-svg]][jb] 4 | [![JB][v-svg]][jb] 5 | [![Build Status][tv-svg]][tv-url] 6 | [![Build status][av-svg]][av-url] 7 | 8 | [d-svg]: https://img.shields.io/jetbrains/plugin/d/12046-pest.svg 9 | [v-svg]: https://img.shields.io/jetbrains/plugin/v/12046-pest.svg 10 | [jb]: https://plugins.jetbrains.com/plugin/12046-pest 11 | [tv-url]: https://travis-ci.org/pest-parser/intellij-pest 12 | [tv-svg]: https://travis-ci.org/pest-parser/intellij-pest.svg?branch=master 13 | [av-url]: https://ci.appveyor.com/project/dragostis/intellij-pest-3fx8c/branch/master 14 | [av-svg]: https://img.shields.io/appveyor/ci/dragostis/intellij-pest-3fx8c/master.svg?label=appveyor 15 | [av-zip]: https://ci.appveyor.com/project/dragostis/intellij-pest-3fx8c/branch/master/artifacts 16 | 17 | The IntelliJ IDEA plugin for [Pest](https://pest.rs). 18 | 19 | ## Features 20 | 21 | See [JetBrains Plugin Marketplace][jb] page. 22 |
23 | This plugin features in a bundled pest-vm which can do real-time syntax highlighting for you. 24 | 25 | ## Maintainer 26 | 27 | + [@ice1000](https://github.com/ice1000) 28 | 29 | ## Videos 30 | 31 | There's a video about the *Live Preview* functionality. 32 | 33 | + [YouTube][YouTube] 34 | + [Bilibili][Bilibili] 35 | 36 | [YouTube]: https://www.youtube.com/watch?v=AnUhekAENm4 37 | [Bilibili]: https://www.bilibili.com/video/av49762905/ 38 | 39 | ## Screenshots 40 | 41 | ### Rust code injection 42 | ![rust-injected](https://user-images.githubusercontent.com/16398479/53776511-44c84b00-3ec4-11e9-9771-83106b6ccd57.png) 43 | 44 | ### Completion 45 | ![screen](https://user-images.githubusercontent.com/16398479/53726936-0dfb2200-3e3d-11e9-9ea3-d1bf5511e8cb.gif) 46 | 47 | ### Rename 48 | ![rename](https://user-images.githubusercontent.com/16398479/53851472-d00d1380-3f8c-11e9-9b50-03c813125e5d.gif) 49 | 50 | ### Inline 51 | ![inline](https://user-images.githubusercontent.com/16398479/53846719-fc6c6400-3f7b-11e9-9506-9a3d0c50e319.gif) 52 | 53 | ### Extract rule 54 | ![extract](https://user-images.githubusercontent.com/16398479/56088933-52c0a280-5e58-11e9-9d93-fd8d318879d4.gif) 55 | 56 | ### Completion under white theme 57 | ![completion](https://user-images.githubusercontent.com/16398479/53726938-0dfb2200-3e3d-11e9-9c50-8f3139b30c0d.jpg) 58 | 59 | ### Miscellaneous highlighting 60 | ![unresolved.png](https://user-images.githubusercontent.com/16398479/53846891-a9df7780-3f7c-11e9-9823-bbc4a8655ef7.png) 61 | ![recursion.png](https://user-images.githubusercontent.com/16398479/53846994-0fcbff00-3f7d-11e9-933c-d7fc0fb0f007.png) 62 | 63 | Unstable per-commit build can be downloaded from [AppVeyor page][av-zip] 64 | -------------------------------------------------------------------------------- /res/icons/pest_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 17 | 24 | 28 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /res/META-INF/pluginIcon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 17 | 24 | 28 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /res/icons/pest.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 17 | 24 | 28 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /res/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 17 | 24 | 28 | 32 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | cmake-build-release/ 27 | 28 | # Mongo Explorer plugin: 29 | .idea/**/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Cursive Clojure plugin 46 | .idea/replstate.xml 47 | 48 | # Crashlytics plugin (for Android Studio and IntelliJ) 49 | com_crashlytics_export_strings.xml 50 | crashlytics.properties 51 | crashlytics-build.properties 52 | fabric.properties 53 | ### Gradle template 54 | .gradle 55 | /build/ 56 | 57 | # Ignore Gradle GUI config 58 | gradle-app.setting 59 | 60 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 61 | !gradle-wrapper.jar 62 | 63 | # Cache of project 64 | .gradletasknamecache 65 | 66 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 67 | # gradle/wrapper/gradle-wrapper.properties 68 | ### Java template 69 | # Compiled class file 70 | *.class 71 | 72 | # Log file 73 | *.log 74 | 75 | # BlueJ files 76 | *.ctxt 77 | 78 | # Mobile Tools for Java (J2ME) 79 | .mtj.tmp/ 80 | 81 | # Package Files # 82 | *.jar 83 | *.war 84 | *.ear 85 | *.zip 86 | *.tar.gz 87 | *.rar 88 | 89 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 90 | hs_err_pid* 91 | ### Kotlin template 92 | # Compiled class file 93 | 94 | # Log file 95 | 96 | # BlueJ files 97 | 98 | # Mobile Tools for Java (J2ME) 99 | 100 | # Package Files # 101 | 102 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 103 | 104 | ## ice1000's ignored files 105 | 106 | *.iml 107 | .idea 108 | gen 109 | idea-flex.skeleton 110 | resources 111 | idea-sandbox-copy 112 | !lib/org.eclipse.egit.github.core-2.1.5.jar 113 | # gradle 114 | exportToHTML 115 | .piggy 116 | pinpoint_piggy 117 | grammar/*.scm 118 | asmble 119 | 120 | .intellijPlatform 121 | -------------------------------------------------------------------------------- /src/rs/pest/action/ui/ui-impl.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.action.ui 2 | 3 | import com.intellij.codeInsight.lookup.LookupManager 4 | import com.intellij.codeInsight.lookup.impl.LookupImpl 5 | import com.intellij.ide.browsers.BrowserLauncher 6 | import com.intellij.openapi.command.WriteCommandAction 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.psi.PsiDocumentManager 10 | import rs.pest.PEST_IDE_CRATE_LINK 11 | import rs.pest.PEST_WEBSITE 12 | import rs.pest.livePreview.Lib 13 | import rs.pest.psi.PestExpression 14 | import rs.pest.psi.PestGrammarRule 15 | import rs.pest.psi.startOffset 16 | import javax.swing.ButtonGroup 17 | 18 | @Suppress("unused") 19 | class PestIntroduceRulePopupImpl( 20 | newRuleStartOffset: Int, 21 | elementToRename: PestGrammarRule, 22 | editor: Editor, 23 | project: Project, 24 | expr: PestExpression 25 | ) : PestIntroduceRulePopup(elementToRename, editor, project, expr) { 26 | init { 27 | mainPanel.border = null 28 | val buttons = listOf(atomic, compoundAtomic, nonAtomic, normal, silent) 29 | with(ButtonGroup()) { buttons.forEach(::add) } 30 | buttons.forEach { button -> 31 | button.addActionListener { 32 | val runnable = act@{ 33 | val document = myEditor.document 34 | val grammarBody = elementToRename.grammarBody ?: return@act 35 | val local = elementToRename.startOffset == 0 36 | var offset = if (local) newRuleStartOffset + grammarBody.startOffset 37 | else grammarBody.startOffset - 1 38 | if (document.immutableCharSequence[offset] in "@!_$") 39 | document.deleteString(offset, offset + 1) 40 | else offset += 1 41 | when (button) { 42 | normal -> Unit 43 | atomic -> document.insertString(offset, "@") 44 | nonAtomic -> document.insertString(offset, "!") 45 | compoundAtomic -> document.insertString(offset, "$") 46 | silent -> document.insertString(offset, "_") 47 | } 48 | PsiDocumentManager.getInstance(myProject).commitDocument(document) 49 | } 50 | WriteCommandAction.runWriteCommandAction(project) { 51 | val lookup = LookupManager.getActiveLookup(myEditor) as? LookupImpl 52 | if (lookup != null) lookup.performGuardedChange(runnable) 53 | else runnable() 54 | } 55 | } 56 | } 57 | } 58 | 59 | override fun getComponent() = mainPanel 60 | } 61 | 62 | class PestIdeBridgeInfoImpl : PestIdeBridgeInfo() { 63 | val component get() = mainPanel 64 | 65 | companion object { 66 | val info by lazy { Lib(114514 * 50).crateInfo() } 67 | } 68 | 69 | init { 70 | websiteLink.text = PEST_WEBSITE 71 | websiteLink.setListener({ _, _ -> BrowserLauncher.instance.open(PEST_WEBSITE) }, null) 72 | crateLink.text = PEST_IDE_CRATE_LINK 73 | crateLink.setListener({ _, _ -> BrowserLauncher.instance.open(PEST_IDE_CRATE_LINK) }, null) 74 | versionLabel.text = info.version 75 | authorLabel.text = info.author 76 | descriptionLabel.text = info.description 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/rs/pest/psi/types.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.psi 2 | 3 | import com.intellij.openapi.editor.colors.TextAttributesKey 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.psi.PsiElement 6 | import com.intellij.psi.PsiFileFactory 7 | import com.intellij.psi.TokenType 8 | import com.intellij.psi.tree.IElementType 9 | import com.intellij.psi.tree.TokenSet 10 | import rs.pest.PestBundle 11 | import rs.pest.PestHighlighter 12 | import rs.pest.PestLanguage 13 | import rs.pest.psi.impl.PestGrammarRuleMixin 14 | 15 | class PestElementType(debugName: String) : IElementType(debugName, PestLanguage.INSTANCE) 16 | 17 | class PestTokenType(debugName: String) : IElementType(debugName, PestLanguage.INSTANCE) { 18 | companion object Builtin { 19 | @JvmField val LINE_COMMENT = PestTokenType("line comment") 20 | @JvmField val LINE_DOC_COMMENT = PestTokenType("doc comment") 21 | @JvmField val LINE_REGEX_COMMENT = PestTokenType("regex comment") 22 | @JvmField val BLOCK_COMMENT = PestTokenType("block comment") 23 | @JvmField val STRING_INCOMPLETE = PestTokenType("incomplete string") 24 | @JvmField val CHAR_INCOMPLETE = PestTokenType("incomplete char") 25 | @JvmField val COMMENTS = TokenSet.create(LINE_COMMENT, LINE_DOC_COMMENT, LINE_REGEX_COMMENT, BLOCK_COMMENT) 26 | @JvmField val STRINGS = TokenSet.create(PestTypes.STRING_TOKEN, PestTypes.CHAR_TOKEN) 27 | @JvmField val WHITE_SPACE = TokenSet.create(TokenType.WHITE_SPACE) 28 | @JvmField val INCOMPLETE_STRINGS = TokenSet.create(STRING_INCOMPLETE, CHAR_INCOMPLETE) 29 | @JvmField val ANY_STRINGS = TokenSet.orSet(STRINGS, INCOMPLETE_STRINGS) 30 | @JvmField val IDENTIFIERS = TokenSet.create(PestTypes.IDENTIFIER) 31 | 32 | fun fromText(text: String, project: Project): PsiElement? = PsiFileFactory.getInstance(project).createFileFromText(PestLanguage.INSTANCE, text).firstChild 33 | fun createRule(text: String, project: Project) = fromText(text, project) as? PestGrammarRuleMixin 34 | fun createBody(text: String, project: Project) = createRule("r=$text", project)?.grammarBody 35 | fun createExpression(text: String, project: Project) = createBody("{$text}", project)?.expression 36 | fun createRuleName(text: String, project: Project) = createRule("$text = {\"d\"}", project)?.nameIdentifier 37 | } 38 | } 39 | 40 | enum class PestRuleType(val description: String, val highlight: TextAttributesKey) { 41 | Simple("Simple", PestHighlighter.SIMPLE), 42 | Silent("Silent", PestHighlighter.SILENT), 43 | Atomic("Atomic", PestHighlighter.ATOMIC), 44 | CompoundAtomic("Compound atomic", PestHighlighter.COMPOUND_ATOMIC), 45 | NonAtomic("Non-atomic", PestHighlighter.NON_ATOMIC); 46 | 47 | fun help() = when (this) { 48 | Simple -> PestBundle.message("pest.rule.help.simple") 49 | Silent -> PestBundle.message("pest.rule.help.silent") 50 | Atomic -> PestBundle.message("pest.rule.help.atomic") 51 | CompoundAtomic -> PestBundle.message("pest.rule.help.compound") 52 | NonAtomic -> PestBundle.message("pest.rule.help.non-atomic") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/rs/pest/livePreview/html.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.livePreview 2 | 3 | import com.intellij.openapi.application.ReadAction 4 | import com.intellij.openapi.progress.ProcessCanceledException 5 | import com.intellij.openapi.progress.ProgressIndicator 6 | import com.intellij.openapi.progress.ProgressIndicatorProvider 7 | import com.intellij.openapi.project.guessProjectDir 8 | import com.intellij.openapi.vfs.VfsUtil 9 | import kotlinx.html.* 10 | import kotlinx.html.stream.appendHTML 11 | import java.awt.Color 12 | import java.io.File 13 | 14 | fun defaultPreviewToHtml(file: LivePreviewFile, indicator: ProgressIndicator) { 15 | val fileName = "${file.pestFile?.name}-${file.ruleName}.html" 16 | val folder = file.pestFile?.virtualFile?.parent ?: file.project.guessProjectDir() ?: return 17 | val ioFile = File("${folder.canonicalPath}").resolve(fileName) 18 | ioFile.deleteOnExit() 19 | ioFile.writer().use { 20 | toHtml(file, it, indicator) 21 | it.flush() 22 | } 23 | VfsUtil.findFileByIoFile(ioFile, true) 24 | } 25 | 26 | private fun toHtml(file: LivePreviewFile, html: Appendable, indicator: ProgressIndicator) { 27 | indicator.text = "Initializing highlighting info" 28 | indicator.fraction = 0.0 29 | html.appendHTML().html { 30 | head { charset("UTF-8") } 31 | comment("Generated with love by IntelliJ-Pest") 32 | ReadAction.run { 33 | val chars = file.textToCharArray() 34 | val highlights = arrayOfNulls>(chars.size) 35 | highlight(file, { range, err -> 36 | for (i in range.startOffset until range.endOffset) highlights[i] = null to err.orEmpty() 37 | }) { range, info, attributes -> 38 | for (i in range.startOffset until range.endOffset) 39 | highlights[i] = attributes.foregroundColor to info 40 | } 41 | indicator.text = "Writing html" 42 | indicator.fraction = 0.2 43 | body { pre { render(highlights, chars, indicator) } } 44 | } 45 | } 46 | } 47 | 48 | private fun PRE.render(highlights: Array?>, chars: CharArray, indicator: ProgressIndicator) { 49 | ProgressIndicatorProvider.checkCanceled() 50 | for ((i, highlight) in highlights.withIndex()) { 51 | indicator.fraction = 0.2 + (i.toDouble() / chars.size) * 0.8 52 | if (highlight == null) +chars[i].toString() 53 | else { 54 | val color = highlight.first 55 | if (color == null) a { 56 | attributes["title"] = highlight.second 57 | attributes["style"] = "text-decoration: underline; text-decoration-color: red" 58 | +chars[i].toString() 59 | } else font(color) { 60 | attributes["title"] = highlight.second 61 | +chars[i].toString() 62 | } 63 | } 64 | } 65 | } 66 | 67 | private class FONT(consumer: TagConsumer<*>, color: Color) : HTMLTag("font", 68 | consumer, 69 | mapOf("color" to "#${Integer.toHexString(color.rgb).drop(2)}"), 70 | inlineTag = true, 71 | emptyTag = false 72 | ), HtmlInlineTag 73 | 74 | private fun PRE.font(color: Color, block: FONT.() -> Unit = {}) { 75 | FONT(consumer, color).visit(block) 76 | } 77 | -------------------------------------------------------------------------------- /src/rs/pest/action/ui/PestIntroduceRulePopup.form: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared 2 | # with the Build workflow. Running the publishPlugin task requires the PUBLISH_TOKEN secret provided. 3 | 4 | name: Release 5 | on: 6 | release: 7 | types: [prereleased, released] 8 | 9 | jobs: 10 | 11 | # Prepare and publish the plugin to the Marketplace repository 12 | release: 13 | name: Publish Plugin 14 | runs-on: ubuntu-latest 15 | steps: 16 | 17 | # Check out current repository 18 | - name: Fetch Sources 19 | uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.release.tag_name }} 22 | 23 | # Setup Java 22 environment for the next steps 24 | - name: Setup Java 25 | uses: actions/setup-java@v4 26 | with: 27 | distribution: zulu 28 | java-version: 22 29 | cache: gradle 30 | 31 | # Set environment variables 32 | - name: Export Properties 33 | id: properties 34 | shell: bash 35 | run: | 36 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 37 | ${{ github.event.release.body }} 38 | EOM 39 | )" 40 | 41 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 42 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 43 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 44 | 45 | echo "::set-output name=changelog::$CHANGELOG" 46 | 47 | # Update Unreleased section with the current release note 48 | - name: Patch Changelog 49 | if: ${{ steps.properties.outputs.changelog != '' }} 50 | env: 51 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 52 | run: | 53 | ./gradlew patchChangelog --release-note="$CHANGELOG" 54 | 55 | # Publish the plugin to the Marketplace 56 | - name: Publish Plugin 57 | env: 58 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 59 | run: ./gradlew publishPlugin 60 | 61 | # Upload artifact as a release asset 62 | - name: Upload Release Asset 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 66 | 67 | # Create pull request 68 | - name: Create Pull Request 69 | if: ${{ steps.properties.outputs.changelog != '' }} 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | run: | 73 | VERSION="${{ github.event.release.tag_name }}" 74 | BRANCH="changelog-update-$VERSION" 75 | 76 | git config user.email "action@github.com" 77 | git config user.name "GitHub Action" 78 | 79 | git checkout -b $BRANCH 80 | git commit -am "Changelog update - $VERSION" 81 | git push --set-upstream origin $BRANCH 82 | 83 | gh pr create \ 84 | --title "Changelog update - \`$VERSION\`" \ 85 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 86 | --base main \ 87 | --head $BRANCH 88 | -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-annotator.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInspection.ProblemHighlightType 4 | import com.intellij.lang.annotation.AnnotationHolder 5 | import com.intellij.lang.annotation.Annotator 6 | import com.intellij.lang.annotation.HighlightSeverity 7 | import com.intellij.psi.PsiElement 8 | import com.intellij.psi.util.PsiTreeUtil 9 | import rs.pest.PestBundle 10 | import rs.pest.PestFile 11 | import rs.pest.PestHighlighter 12 | import rs.pest.psi.PestCharacter 13 | import rs.pest.psi.impl.PestFixedBuiltinRuleNameMixin 14 | import rs.pest.psi.impl.PestGrammarRuleMixin 15 | import rs.pest.psi.impl.PestIdentifierMixin 16 | import rs.pest.psi.impl.PestRuleNameMixin 17 | 18 | class PestAnnotator : Annotator { 19 | override fun annotate(element: PsiElement, holder: AnnotationHolder) { 20 | when (element) { 21 | is PestFile -> file(element, holder) 22 | is PestIdentifierMixin -> identifier(element, holder) 23 | is PestCharacter -> char(element, holder) 24 | is PestFixedBuiltinRuleNameMixin -> fixedBuiltInRuleName(element, holder) 25 | is PestRuleNameMixin -> validRuleName(element, holder) 26 | } 27 | } 28 | 29 | private fun file(element: PestFile, holder: AnnotationHolder) { 30 | element.errors.forEach { (range, msg) -> 31 | holder.newAnnotation(HighlightSeverity.ERROR, msg) 32 | .range(range) 33 | .create() 34 | } 35 | } 36 | 37 | private fun char(element: PestCharacter, holder: AnnotationHolder) { 38 | if (element.textLength <= 2) holder 39 | .newAnnotation(HighlightSeverity.ERROR, PestBundle.message("pest.annotator.empty-char")) 40 | .range(element) 41 | .textAttributes(PestHighlighter.UNRESOLVED) 42 | .highlightType(ProblemHighlightType.ERROR) 43 | .create() 44 | } 45 | 46 | private fun fixedBuiltInRuleName(element: PestFixedBuiltinRuleNameMixin, holder: AnnotationHolder) { 47 | holder.newAnnotation(HighlightSeverity.ERROR, PestBundle.message("pest.annotator.overwrite")) 48 | .range(element) 49 | .highlightType(ProblemHighlightType.ERROR) 50 | .create() 51 | } 52 | 53 | private fun validRuleName(element: PestRuleNameMixin, holder: AnnotationHolder) { 54 | val rule = element.parent as PestGrammarRuleMixin 55 | holder.newAnnotation(HighlightSeverity.INFORMATION, rule.type.help()) 56 | .textAttributes(rule.type.highlight) 57 | .range(element) 58 | .create() 59 | if (PsiTreeUtil.hasErrorElements(element.parent)) { 60 | holder.newAnnotation(HighlightSeverity.ERROR, PestBundle.message("pest.annotator.rule-contains-error", element.text)) 61 | .highlightType(ProblemHighlightType.GENERIC_ERROR) 62 | .range(element) 63 | .create() 64 | } 65 | } 66 | 67 | private fun identifier(element: PestIdentifierMixin, holder: AnnotationHolder) { 68 | val rule = element.reference.resolve() as? PestGrammarRuleMixin ?: run { 69 | holder.newAnnotation(HighlightSeverity.ERROR, PestBundle.message("pest.annotator.unresolved")) 70 | .range(element) 71 | .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL) 72 | .textAttributes(PestHighlighter.UNRESOLVED) 73 | .create() 74 | return 75 | } 76 | holder.newSilentAnnotation(HighlightSeverity.INFORMATION) 77 | .textAttributes(rule.type.highlight) 78 | .range(element) 79 | .create() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/rs/pest/livePreview/live-preview.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.livePreview 2 | 3 | import com.intellij.extapi.psi.ASTWrapperPsiElement 4 | import com.intellij.extapi.psi.PsiFileBase 5 | import com.intellij.lang.ASTNode 6 | import com.intellij.lang.LightPsiParser 7 | import com.intellij.lang.PsiBuilder 8 | import com.intellij.lang.PsiParser 9 | import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx 10 | import com.intellij.openapi.fileTypes.LanguageFileType 11 | import com.intellij.openapi.fileTypes.PlainTextParserDefinition 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.psi.FileViewProvider 14 | import com.intellij.psi.impl.PsiManagerEx 15 | import com.intellij.psi.tree.IElementType 16 | import com.intellij.psi.tree.IFileElementType 17 | import com.intellij.testFramework.LightVirtualFile 18 | import icons.PestIcons 19 | import rs.pest.PestFile 20 | import javax.swing.SwingConstants 21 | 22 | fun livePreview(file: PestFile, selected: String) { 23 | val project = file.project 24 | val virtualFile = LightVirtualFile("${file.name}.$selected.preview", LivePreviewFileType, "") 25 | val psiFile = PsiManagerEx.getInstanceEx(project).findFile(virtualFile) as? LivePreviewFile ?: return 26 | psiFile.pestFile = file 27 | psiFile.ruleName = selected 28 | file.livePreviewFile.add(psiFile) 29 | val editorManager = FileEditorManagerEx.getInstanceEx(project) 30 | editorManager.currentWindow?.split(SwingConstants.HORIZONTAL, false, virtualFile, true) 31 | editorManager.openFile(virtualFile, true) 32 | } 33 | 34 | object LivePreviewFileType : LanguageFileType(LivePreviewLanguage.INSTANCE) { 35 | override fun getDefaultExtension() = "preview" 36 | override fun getName() = LivePreviewLanguage.INSTANCE.displayName 37 | override fun getIcon() = PestIcons.PEST_FILE 38 | override fun getDescription() = LivePreviewLanguage.INSTANCE.displayName 39 | } 40 | 41 | class LivePreviewFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, LivePreviewLanguage.INSTANCE) { 42 | override fun getFileType() = LivePreviewFileType 43 | var pestFile: PestFile? = null 44 | var ruleName: String? = null 45 | } 46 | 47 | class LivePreviewParser : PsiParser, LightPsiParser { 48 | override fun parseLight(root: IElementType?, builder: PsiBuilder?) { 49 | parse(root ?: return, builder ?: return) 50 | } 51 | 52 | override fun parse(root: IElementType, builder: PsiBuilder): ASTNode { 53 | val mark = builder.mark() 54 | run { 55 | @Suppress("NAME_SHADOWING") 56 | val mark = builder.mark() 57 | builder.advanceLexer() 58 | mark.done(LivePreviewElement.TYPE) 59 | } 60 | mark.done(root) 61 | return builder.treeBuilt 62 | } 63 | } 64 | 65 | class LivePreviewElement(node: ASTNode) : ASTWrapperPsiElement(node) { 66 | companion object Tokens { 67 | @JvmField 68 | val TYPE = IElementType("LivePreview Content", LivePreviewLanguage.INSTANCE) 69 | } 70 | } 71 | 72 | class LivePreviewParserDefinition : PlainTextParserDefinition() { 73 | override fun getFileNodeType() = FILE 74 | override fun createFile(viewProvider: FileViewProvider) = LivePreviewFile(viewProvider) 75 | override fun createParser(project: Project?) = LivePreviewParser() 76 | override fun createElement(node: ASTNode) = LivePreviewElement(node) 77 | 78 | companion object { 79 | @JvmField 80 | val FILE = IFileElementType(LivePreviewLanguage.INSTANCE) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /res/rs/pest/pest-bundle.properties: -------------------------------------------------------------------------------- 1 | pest.highlighter.settings.block-comment=Comments//Block comment 2 | pest.highlighter.settings.char=Character 3 | pest.highlighter.settings.comment=Comments//Line comment 4 | pest.highlighter.settings.number=Number 5 | pest.highlighter.settings.string=String 6 | pest.name=Pest 7 | pest.name.description=Pest grammar file 8 | pest.rule.help.simple=Normal rules accept whitespaces and produce a pair 9 | pest.rule.help.atomic=Atomic rules do not accept whitespaces but produce a pair 10 | pest.rule.help.silent=Silent rules do not create token pairs during parsing, nor are they error-reported. 11 | pest.rule.help.compound=Compound-atomic are identical to atomic rules with the exception that rules called by them are \ 12 | not forbidden from generating token pairs. 13 | pest.rule.name=Grammar Rule 14 | pest.rule.help.non-atomic=Non-atomic are identical to normal rules with the exception that they stop the cascading \ 15 | effect of atomic and compound-atomic rules. 16 | pest.highlighter.settings.unresolved=Rule names//Unresolved 17 | pest.highlighter.settings.builtin=Rule names//Builtin 18 | pest.highlighter.settings.simple=Rule names//Simple 19 | pest.highlighter.settings.silent=Rule names//Silent 20 | pest.highlighter.settings.atomic=Rule names//Atomic 21 | pest.highlighter.settings.compound-atomic=Rule names//Compound atomic 22 | pest.highlighter.settings.non-atomic=Rule names//Non atomic 23 | pest.annotator.overwrite=Overwriting a built-in rule 24 | pest.annotator.unresolved=Unresolved reference 25 | pest.annotator.empty-char=Character literals cannot be empty 26 | pest.annotator.rule-contains-error=Rule {0} contains a syntax error 27 | pest.annotator.live-preview.error.title=Syntax error from Pest 28 | pest.inspection.duplicate-rule=Rule {0} is defined more than once 29 | pest.actions.new-file.name=Pest Grammar File 30 | pest.actions.new-file.title=New Pest Grammar File 31 | pest.actions.new-file.description=Create new pest grammar file 32 | pest.actions.inline.error.title=Inline Rule 33 | pest.actions.inline.has-error.info=Rule has errors, cannot inline 34 | pest.actions.inline.recursive.info=Cannot inline recursive rules 35 | pest.actions.inline.dialog.this=Inline this reference and keep the definition 36 | pest.actions.inline.dialog.all=Inline all references and remove the definition 37 | pest.actions.inline.dialog.title=Inline 38 | pest.actions.inline.command.name=Inline rule {0} 39 | pest.actions.inline.view.title=Grammar Rule 40 | pest.actions.extract.rule.target-chooser.title=Expressions 41 | pest.actions.general.title.error=Error 42 | pest.actions.extract.rule.types.atomic=&Atomic 43 | pest.actions.extract.rule.types.compound-atomic=&Compound atomic 44 | pest.actions.extract.rule.types.silent=&Silent 45 | pest.actions.extract.rule.types.non-atomic=&Non-atomic 46 | pest.actions.extract.rule.types.normal=N&ormal 47 | pest.actions.extract.rule.popup.title=Introduce Rule 48 | pest.actions.live-preview.popup.label=&Select the root node: 49 | pest.actions.live-preview.popup.title=Rule Selection 50 | pest.actions.live-preview.popup.ok=Start &Live Preview 51 | pest.actions.info.title=Pest Information 52 | pest.tools.info.bridge-title=Pest IDE Bridge information 53 | pest.tools.info.version=Version: 54 | pest.tools.info.website=Pest website: 55 | pest.tools.info.author=Author: 56 | pest.tools.info.description=Description: 57 | pest.tools.info.crate=Crate Info: 58 | -------------------------------------------------------------------------------- /src/rs/pest/pest-constants.kt: -------------------------------------------------------------------------------- 1 | package rs.pest 2 | 3 | import org.jetbrains.annotations.Nls 4 | import org.jetbrains.annotations.NonNls 5 | 6 | @NonNls const val PEST_DEFAULT_CONTEXT_ID = "PEST_DEFAULT_CONTEXT" 7 | @NonNls const val PEST_LOCAL_CONTEXT_ID = "PEST_LOCAL_CONTEXT" 8 | @Nls const val PEST_LOCAL_CONTEXT_NAME = "Expression" 9 | @NonNls const val PEST_LANGUAGE_NAME = "Pest" 10 | @NonNls const val PEST_EXTENSION = "pest" 11 | @NonNls const val PEST_BLOCK_COMMENT_BEGIN = "/*" 12 | @NonNls const val PEST_BLOCK_COMMENT_END = "*/" 13 | @NonNls const val PEST_LINE_COMMENT = "// " 14 | @NonNls const val PEST_RUN_CONFIG_ID = "PEST_RUN_CONFIG_ID" 15 | @NonNls const val PEST_PLUGIN_ID = "rs.pest" 16 | 17 | @NonNls const val PEST_WEBSITE = "https://pest.rs/" 18 | @NonNls const val PEST_IDE_CRATE_LINK = "https://crates.io/crates/pest-ide" 19 | @NonNls const val PEST_FOLDING_PLACEHOLDER = "{...}" 20 | 21 | @NonNls const val LP_LANGUAGE_NAME = "PEST_LP" 22 | 23 | @JvmField val BUILTIN_RULES = listOf( 24 | "LETTER", 25 | "CASED_LETTER", 26 | "UPPERCASE_LETTER", 27 | "LOWERCASE_LETTER", 28 | "TITLECASE_LETTER", 29 | "MODIFIER_LETTER", 30 | "OTHER_LETTER", 31 | "MARK", 32 | "NONSPACING_MARK", 33 | "SPACING_MARK", 34 | "ENCLOSING_MARK", 35 | "NUMBER", 36 | "DECIMAL_NUMBER", 37 | "LETTER_NUMBER", 38 | "OTHER_NUMBER", 39 | "PUNCTUATION", 40 | "CONNECTOR_PUNCTUATION", 41 | "DASH_PUNCTUATION", 42 | "OPEN_PUNCTUATION", 43 | "CLOSE_PUNCTUATION", 44 | "INITIAL_PUNCTUATION", 45 | "FINAL_PUNCTUATION", 46 | "OTHER_PUNCTUATION", 47 | "SYMBOL", 48 | "MATH_SYMBOL", 49 | "CURRENCY_SYMBOL", 50 | "MODIFIER_SYMBOL", 51 | "OTHER_SYMBOL", 52 | "SEPARATOR", 53 | "SPACE_SEPARATOR", 54 | "LINE_SEPARATOR", 55 | "PARAGRAPH_SEPARATOR", 56 | "OTHER", 57 | "CONTROL", 58 | "FORMAT", 59 | "SURROGATE", 60 | "PRIVATE_USE", 61 | "UNASSIGNED", 62 | "ALPHABETIC", 63 | "BIDI_CONTROL", 64 | "CASE_IGNORABLE", 65 | "CASED", 66 | "CHANGES_WHEN_CASEFOLDED", 67 | "CHANGES_WHEN_CASEMAPPED", 68 | "CHANGES_WHEN_LOWERCASED", 69 | "CHANGES_WHEN_TITLECASED", 70 | "CHANGES_WHEN_UPPERCASED", 71 | "DASH", 72 | "DEFAULT_IGNORABLE_CODE_POINT", 73 | "DEPRECATED", 74 | "DIACRITIC", 75 | "EXTENDER", 76 | "GRAPHEME_BASE", 77 | "GRAPHEME_EXTEND", 78 | "GRAPHEME_LINK", 79 | "HEX_DIGIT", 80 | "HYPHEN", 81 | "IDS_BINARY_OPERATOR", 82 | "IDS_TRINARY_OPERATOR", 83 | "ID_CONTINUE", 84 | "ID_START", 85 | "IDEOGRAPHIC", 86 | "JOIN_CONTROL", 87 | "LOGICAL_ORDER_EXCEPTION", 88 | "LOWERCASE", 89 | "MATH", 90 | "NONCHARACTER_CODE_POINT", 91 | "OTHER_ALPHABETIC", 92 | "OTHER_DEFAULT_IGNORABLE_CODE_POINT", 93 | "OTHER_GRAPHEME_EXTEND", 94 | "OTHER_ID_CONTINUE", 95 | "OTHER_ID_START", 96 | "OTHER_LOWERCASE", 97 | "OTHER_MATH", 98 | "OTHER_UPPERCASE", 99 | "PATTERN_SYNTAX", 100 | "PATTERN_WHITE_SPACE", 101 | "PREPENDED_CONCATENATION_MARK", 102 | "QUOTATION_MARK", 103 | "RADICAL", 104 | "REGIONAL_INDICATOR", 105 | "SENTENCE_TERMINAL", 106 | "SOFT_DOTTED", 107 | "TERMINAL_PUNCTUATION", 108 | "UNIFIED_IDEOGRAPH", 109 | "UPPERCASE", 110 | "VARIATION_SELECTOR", 111 | "WHITE_SPACE", 112 | "XID_CONTINUE", 113 | "XID_START", 114 | "PUSH", 115 | "PEEK", 116 | "PEEK_ALL", 117 | "POP", 118 | "POP_ALL", 119 | "ANY", 120 | "EOI", 121 | "SOI", 122 | "DROP", 123 | "ASCII", 124 | "NEWLINE", 125 | "ASCII_DIGIT", 126 | "ASCII_ALPHA", 127 | "ASCII_ALPHANUMERIC", 128 | "ASCII_NONZERO_DIGIT", 129 | "ASCII_BIN_DIGIT", 130 | "ASCII_OCT_DIGIT", 131 | "ASCII_HEX_DIGIT", 132 | "ASCII_ALPHA_UPPER", 133 | "ASCII_ALPHA_LOWER") 134 | -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-inspection.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer 4 | import com.intellij.codeInspection.InspectionManager 5 | import com.intellij.codeInspection.LocalInspectionTool 6 | import com.intellij.codeInspection.ProblemDescriptor 7 | import com.intellij.codeInspection.ProblemsHolder 8 | import com.intellij.openapi.editor.Document 9 | import com.intellij.openapi.editor.event.DocumentEvent 10 | import com.intellij.openapi.editor.event.DocumentListener 11 | import com.intellij.openapi.util.TextRange 12 | import com.intellij.psi.PsiDocumentManager 13 | import com.intellij.psi.PsiFile 14 | import com.intellij.psi.util.PsiTreeUtil 15 | import com.intellij.util.containers.ContainerUtil 16 | import org.intellij.lang.annotations.Language 17 | import rs.pest.PestBundle 18 | import rs.pest.PestFile 19 | import rs.pest.psi.impl.PestGrammarRuleMixin 20 | 21 | class DuplicateRuleInspection : LocalInspectionTool() { 22 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array { 23 | if (file !is PestFile) return emptyArray() 24 | val problemsHolder = ProblemsHolder(manager, file, isOnTheFly) 25 | val ruleSet = hashMapOf() 26 | val problematicRules = hashSetOf() 27 | file.rules().forEach { 28 | val name = it.name 29 | val tried = ruleSet[name] 30 | if (tried != null) { 31 | problematicRules += it 32 | problematicRules += tried 33 | } else ruleSet[name] = it 34 | } 35 | problematicRules.forEach { 36 | problemsHolder.registerProblem(it, PestBundle.message("pest.inspection.duplicate-rule", it.name)) 37 | } 38 | return problemsHolder.resultsArray 39 | } 40 | } 41 | 42 | @Language("RegExp") 43 | private val errorMsgRegex = Regex("\\A(\\d+)\\^(\\d+)\\^(\\d+)\\^(\\d+)\\^(.*)$") 44 | 45 | private fun vmListener(element: PestFile) = object : DocumentListener { 46 | override fun documentChanged(event: DocumentEvent) = reloadVM(event.document, element) 47 | } 48 | 49 | fun reloadVM(dom: Document, element: PestFile) { 50 | if (PsiTreeUtil.hasErrorElements(element)) return 51 | val (works, messages) = try { 52 | element.reloadVM(dom.text) 53 | } catch (e: Exception) { 54 | element.rebootVM() 55 | element.reloadVM(dom.text) 56 | } 57 | if (works) with(element) { 58 | errors = emptySequence() 59 | availableRules = messages 60 | livePreviewFile().forEach(DaemonCodeAnalyzer.getInstance(element.project)::restart) 61 | } else with(element) { 62 | errors = messages.mapNotNull { errorMsgRegex.matchEntire(it)?.groupValues }.mapNotNull { 63 | val startLine = it[1].toInt() - 1 64 | val startCol = it[2].toInt() - 1 65 | val endLine = it[3].toInt() - 1 66 | val endCol = it[4].toInt() - 1 67 | val range = try { 68 | TextRange(dom.getLineStartOffset(startLine) + startCol, dom.getLineStartOffset(endLine) + endCol) 69 | } catch (e: IndexOutOfBoundsException) { 70 | return@mapNotNull null 71 | } 72 | Pair(range, it[5]) 73 | } 74 | availableRules = emptySequence() 75 | } 76 | } 77 | 78 | class PestVmInspection : LocalInspectionTool() { 79 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array { 80 | if (file !is PestFile) return emptyArray() 81 | val project = file.project 82 | if (!file.isDocumentListenerAdded) { 83 | PsiDocumentManager.getInstance(project).getDocument(file)?.apply { 84 | reloadVM(this, file) 85 | file.isDocumentListenerAdded = true 86 | addDocumentListener(vmListener(file)) 87 | } 88 | } 89 | return emptyArray() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/rs/pest/psi/impl/psi-impl.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.psi.impl 2 | 3 | import com.intellij.psi.PsiElement 4 | import com.intellij.psi.PsiFile 5 | import com.intellij.psi.PsiWhiteSpace 6 | import com.intellij.psi.SyntaxTraverser 7 | import com.intellij.psi.util.PsiTreeUtil 8 | import rs.pest.psi.* 9 | import java.util.* 10 | 11 | 12 | /** 13 | * @param self The declaration itself 14 | */ 15 | inline fun collectFrom(startPoint: PsiElement, name: String, self: PsiElement? = null) = SyntaxTraverser 16 | .psiTraverser(startPoint) 17 | .filterIsInstance() 18 | .filter { it.text == name && it != self } 19 | .mapNotNull(PsiElement::getReference) 20 | .toMutableList() 21 | 22 | fun PsiElement.bodyText(maxSizeExpected: Int) = buildString { 23 | append(' ') 24 | var child = firstChild 25 | while (child != null && child != this@bodyText) { 26 | if (child is PsiWhiteSpace) append(' ') 27 | else { 28 | while (child.firstChild != null && length + child.textLength > maxSizeExpected) child = child.firstChild 29 | append(child.text) 30 | } 31 | if (maxSizeExpected < 0 || length >= maxSizeExpected) break 32 | do { 33 | child = child.nextSibling ?: child.parent 34 | } while (child == null) 35 | } 36 | } 37 | 38 | inline fun findParentExpression(file: PsiFile, startOffset: Int, endOffset: Int): T? { 39 | @Suppress("NAME_SHADOWING") 40 | var endOffset = endOffset 41 | if (endOffset > startOffset) endOffset-- 42 | val startElement = file.findElementAt(startOffset) 43 | val endElement = file.findElementAt(endOffset) 44 | if (startElement == null || endElement == null) return null 45 | val commonParent = PsiTreeUtil.findCommonParent(startElement, endElement) 46 | return PsiTreeUtil.getParentOfType(commonParent, T::class.java, false) 47 | } 48 | 49 | class SquashedPsi(val it: PsiElement) { 50 | override fun hashCode() = it.hashCode() 51 | override fun equals(other: Any?) = 52 | if (other !is SquashedPsi) false else compareExpr(it, other.it) 53 | } 54 | 55 | /** 56 | * @param std The element that we should be similar to 57 | * @param us Us 58 | */ 59 | fun extractSimilar(std: PsiElement, us: PsiElement) = 60 | if (std.javaClass == us.javaClass && us.javaClass == PestExpressionImpl::class.java) { 61 | val lc = std.children 62 | val rc = us.children 63 | val index = Collections.indexOfSubList(rc.map(::SquashedPsi), lc.map(::SquashedPsi)) 64 | if (index >= 0) Array(lc.size) { rc[it + index] } 65 | else null 66 | } else if (compareExpr(std, us)) { 67 | arrayOf(us) 68 | } else null 69 | 70 | fun compareExpr(l: PsiElement, r: PsiElement): Boolean { 71 | /// Because we don't want to extract ourselves as yet another usage :) 72 | if (l == r) return false 73 | if (!l.isValid || !r.isValid) return false 74 | return when { 75 | l is PestCustomizableRuleName && r is PestCustomizableRuleName 76 | || l is PestBuiltin && r is PestBuiltin 77 | || l is PestString && r is PestString 78 | || l is PestInfixOperator && r is PestInfixOperator 79 | || l is PestPrefixOperator && r is PestPrefixOperator 80 | || l is PestIdentifier && r is PestIdentifier 81 | || l is PestPush && r is PestPush 82 | || l is PestPeek && r is PestPeek 83 | || l is PestRange && r is PestRange 84 | || l is PestPeekSlice && r is PestPeekSlice 85 | || l is PestCharacter && r is PestCharacter -> l.textMatches(r) 86 | l.javaClass == r.javaClass && r.javaClass == PestExpressionImpl::class.java 87 | || l is PestPostfixOperator && r is PestPostfixOperator -> { 88 | val lc = l.children 89 | val rc = r.children 90 | lc.size == rc.size 91 | && lc.isNotEmpty() 92 | && (lc zip rc).all { (x, y) -> compareExpr(x, y) } 93 | } 94 | else -> false 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/rs/pest/livePreview/ffi.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.livePreview 2 | 3 | import rs.pest.vm.PestUtil 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.io.InputStreamReader 7 | import java.nio.ByteBuffer 8 | import java.nio.charset.StandardCharsets 9 | 10 | sealed class Rendering { 11 | data class Err(val msg: String) : Rendering() 12 | data class Ok(val lexical: Sequence) : Rendering() 13 | } 14 | 15 | data class CrateInfo( 16 | val version: String, 17 | val author: String, 18 | val description: String 19 | ) 20 | 21 | class Lib(private var native: PestUtil) { 22 | constructor(size: Int) : this(PestUtil(size)) 23 | 24 | /** 25 | * @return (true, rule names) or (false, error messages) 26 | */ 27 | fun loadVM(pestCode: String): Pair> { 28 | val pestCodePtr = ptrFromString(pestCode) 29 | val returned = native.load_vm(pestCodePtr.offset, pestCodePtr.size) 30 | val str = nullTermedStringFromOffset(returned) 31 | val isError = str.startsWith("Err") 32 | return !isError to str 33 | .removePrefix("Err") 34 | .removeSurrounding(prefix = "[", suffix = "]") 35 | .splitToSequence(',') 36 | .map { it.trim() } 37 | .map { it.removeSurrounding(delimiter = "\"") } 38 | } 39 | 40 | fun reboot(newMemory: ByteBuffer = ByteBuffer.allocate(native.memory.limit() * 3 / 2)) { 41 | native = PestUtil(newMemory) 42 | } 43 | 44 | /** 45 | * @return Syntax information or error message (it's pretty. Need special printing) 46 | */ 47 | fun renderCode(ruleName: String, userCode: String): Rendering { 48 | val ruleNamePtr = ptrFromString(ruleName) 49 | val userCodePtr = ptrFromString(userCode) 50 | val returned = native.render_code( 51 | ruleNamePtr.offset, ruleNamePtr.size, 52 | userCodePtr.offset, userCodePtr.size) 53 | val str = nullTermedStringFromOffset(returned) 54 | return if (str.startsWith("Err")) Rendering.Err(str.removePrefix("Err")) 55 | else Rendering.Ok(str 56 | .removeSurrounding(prefix = "[", suffix = "]") 57 | .splitToSequence(',') 58 | .map { it.trim() } 59 | .map { it.removeSurrounding(delimiter = "\"") }) 60 | } 61 | 62 | fun crateInfo(): CrateInfo { 63 | val info = nullTermedStringFromOffset(native.crate_info()) 64 | val (version, author, description) = info.split('\n') 65 | return CrateInfo(version, author, description) 66 | } 67 | 68 | private fun ptrFromString(str: String): Ptr { 69 | val bytes = str.toByteArray(StandardCharsets.UTF_8) 70 | val ptr = Ptr(bytes.size) 71 | ptr.put(bytes) 72 | return ptr 73 | } 74 | 75 | private fun nullTermedStringFromOffset(offset: Int): String { 76 | val memory = native.memory 77 | memory.position(offset) 78 | // We're going to turn the mem into an input stream. This is the 79 | // reasonable way to stream a UTF8 read using standard Java libs 80 | // that I could find. 81 | val r = InputStreamReader(object : InputStream() { 82 | @Throws(IOException::class) 83 | override fun read() = if (!memory.hasRemaining()) -1 else memory.get().toInt() and 0xFF 84 | }, StandardCharsets.UTF_8) 85 | val builder = StringBuilder() 86 | while (true) { 87 | val c = r.read() 88 | if (c <= 0) break 89 | builder.append(c.toChar()) 90 | } 91 | 92 | native.dealloc(offset, memory.position() - offset) 93 | return builder.toString() 94 | } 95 | 96 | internal inner class Ptr(val offset: Int, val size: Int) { 97 | constructor(size: Int) : this(native.alloc(size), size) 98 | 99 | fun put(bytes: ByteArray) { 100 | // Yeah, yeah, not thread safe 101 | val memory = native.memory 102 | memory.position(offset) 103 | memory.put(bytes) 104 | } 105 | 106 | @Throws(Throwable::class) 107 | protected fun finalize() { 108 | native.dealloc(offset, size) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/rs/pest/action/tools.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.action 2 | 3 | import com.intellij.notification.Notification 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.notification.Notifications 6 | import com.intellij.openapi.actionSystem.AnAction 7 | import com.intellij.openapi.actionSystem.AnActionEvent 8 | import com.intellij.openapi.actionSystem.CommonDataKeys 9 | import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx 10 | import com.intellij.openapi.progress.ProgressIndicator 11 | import com.intellij.openapi.progress.ProgressManager 12 | import com.intellij.openapi.progress.Task 13 | import com.intellij.openapi.project.DumbAware 14 | import com.intellij.openapi.ui.DialogBuilder 15 | import com.intellij.openapi.ui.popup.Balloon 16 | import com.intellij.openapi.ui.popup.JBPopupFactory 17 | import com.intellij.openapi.util.text.StringUtil 18 | import com.intellij.psi.util.PsiTreeUtil 19 | import rs.pest.PestBundle 20 | import rs.pest.PestFile 21 | import rs.pest.action.ui.PestIdeBridgeInfoImpl 22 | import rs.pest.action.ui.RuleSelector 23 | import rs.pest.livePreview.LivePreviewFile 24 | import rs.pest.livePreview.defaultPreviewToHtml 25 | import rs.pest.livePreview.livePreview 26 | 27 | class PestViewInfoAction : AnAction(), DumbAware { 28 | override fun actionPerformed(e: AnActionEvent) { 29 | val component = PestIdeBridgeInfoImpl().component 30 | DialogBuilder() 31 | .title(PestBundle.message("pest.actions.info.title")) 32 | .centerPanel(component) 33 | .show() 34 | } 35 | } 36 | 37 | class PestLivePreviewToHtmlAction : AnAction() { 38 | override fun update(e: AnActionEvent) { 39 | super.update(e) 40 | e.presentation.isEnabledAndVisible = CommonDataKeys.PSI_FILE.getData(e.dataContext) is LivePreviewFile 41 | } 42 | 43 | override fun actionPerformed(e: AnActionEvent) { 44 | val file = CommonDataKeys.PSI_FILE.getData(e.dataContext) as? LivePreviewFile ?: return 45 | ProgressManager.getInstance().run(object : Task.Backgroundable(file.project, "HTML generation", true, 46 | ALWAYS_BACKGROUND 47 | ) { 48 | override fun run(indicator: ProgressIndicator) { 49 | val startTime = System.currentTimeMillis() 50 | defaultPreviewToHtml(file, indicator) 51 | val s = "HTML file generated in ${StringUtil.formatDuration(System.currentTimeMillis() - startTime)}" 52 | Notifications.Bus.notify(Notification("HTML Export", "", s, NotificationType.INFORMATION)) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | class PestLivePreviewAction : AnAction() { 59 | override fun update(e: AnActionEvent) { 60 | super.update(e) 61 | val psiFile = CommonDataKeys.PSI_FILE.getData(e.dataContext) as? PestFile 62 | if (psiFile == null) { 63 | e.presentation.isEnabledAndVisible = false 64 | return 65 | } 66 | e.presentation.isEnabledAndVisible = 67 | CommonDataKeys.EDITOR.getData(e.dataContext) != null 68 | && !PsiTreeUtil.hasErrorElements(psiFile) 69 | && psiFile.errors.none() 70 | && psiFile.availableRules.any() 71 | } 72 | 73 | override fun actionPerformed(e: AnActionEvent) { 74 | val file = CommonDataKeys.PSI_FILE.getData(e.dataContext) as? PestFile ?: return 75 | val parentComponent = FileEditorManagerEx.getInstanceEx(file.project) 76 | .getEditors(file.virtualFile) 77 | .firstOrNull() 78 | ?.component 79 | ?: return 80 | lateinit var balloon: Balloon 81 | val popup = RuleSelector().apply { 82 | file.availableRules.map { 83 | object { 84 | override fun toString() = it 85 | } 86 | }.forEach(ruleCombo::addItem) 87 | ruleCombo.selectedIndex = 0 88 | okButton.addActionListener { 89 | balloon.hide(true) 90 | val selectedItem: Any? = ruleCombo.selectedItem 91 | livePreview(file, selectedItem.toString()) 92 | } 93 | } 94 | balloon = JBPopupFactory.getInstance() 95 | .createDialogBalloonBuilder(popup.mainPanel, PestBundle.message("pest.actions.live-preview.popup.title")) 96 | .setHideOnClickOutside(true) 97 | .createBalloon() 98 | balloon.showInCenterOf(parentComponent) 99 | } 100 | } -------------------------------------------------------------------------------- /res/icons/pest_file_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | 20 | 24 | 28 | 32 | 33 | 40 | 44 | 48 | 52 | 53 | 54 | 56 | 57 | 60 | 63 | 66 | 70 | 74 | 77 | 81 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /res/icons/pest_file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 13 | 20 | 24 | 28 | 32 | 33 | 40 | 44 | 48 | 52 | 53 | 54 | 56 | 57 | 60 | 63 | 66 | 70 | 74 | 77 | 81 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /res/META-INF/change-notes.html: -------------------------------------------------------------------------------- 1 | 0.3.2
2 |
    3 |
  • Make the plugin dynamically loadable
  • 4 |
  • Update minimum supported platform version to 201
  • 5 |
6 | 0.3.1
7 |
    8 |
  • Upgrade many dependencies, including pest, 9 | kotlin, kotlinx.html, gradle, grammar-kit, etc. 10 |
  • 11 |
  • Compatible with latest Rust plugin
  • 12 |
13 | 0.3.0
14 |
    15 |
  • Support exporting live-preview files into HTML 16 | (right-click in live-preview!). 17 | The inner-most rule information will be preserved, 18 | syntax error information will be exported as well. 19 |
  • 20 |
21 | 0.2.7
22 |
    23 |
  • Catch an exception (#27)
  • 24 |
  • Bump pest-ide dependencies
  • 25 |
  • Start using a custom version of asmble
  • 26 |
27 | 0.2.6
28 |
    29 |
  • Regex-based rule highlight (#22)
  • 30 |
  • Something that hopefully fixes the index out of bound exception (#20, 31 | #24) 32 |
  • 33 |
  • Fix comment lexer (#25)
  • 34 |
35 | 0.2.5
36 |
    37 |
  • Rebuild native bridge with rust nightly published yesterday
  • 38 |
  • Do not ignore case in lexer
  • 39 |
  • Hide live preview action in non-pest files
  • 40 |
41 | 0.2.4
42 |
    43 |
  • Fix NoClassDefError when Rust plugin is not installed (#18)
  • 44 |
  • Access native bridge information
  • 45 |
  • Live Preview is no longer DumbAware
  • 46 |
47 | 0.2.3
48 |
    49 |
  • Correctly bind doc comments to grammar rules
  • 50 |
  • Correctly syntax-highlight doc comments
  • 51 |
  • Re-highlight live preview when pest code is changed
  • 52 |
  • Support RGB-based colors (starting with #) in live-preview
  • 53 |
  • Handle syntax errors
  • 54 |
55 | 0.2.2
56 |
    57 |
  • Bug fix on introduce rule (#17)
  • 58 |
  • Bundle a Pest VM in the plugin
  • 59 |
  • Support "Live Preview" by Ctrl+Alt+P (Preview! 60 | Checkout this video instruction showing a quite work-in-progress on-the-fly syntax highlighter: 61 | YouTube link)
  • 62 |
  • Support showing error messages from Pest VM
  • 63 |
64 | 0.2.1
65 |
    66 |
  • Fix some compatibility problems, bump minimum supported platform version to 173
  • 67 |
  • Fix a critical bug in 0.2.0 (so 0.2.0 update is deleted)
  • 68 |
69 | 0.2.0
70 |
    71 |
  • Introduce rule is now very very usable
    72 | It now detects existing occurrences and provides options for the generated rule type!
  • 73 |
  • Change silent rule color under dark themes
  • 74 |
  • Migrate to Kotlin 1.3.30, Grammar-Kit 2019.1, IntelliJ Gradle Plugin 0.4.7
  • 75 |
76 | 0.1.7
77 |
    78 |
  • Support using ovewritable names
  • 79 |
80 | 0.1.6
81 |
    82 |
  • Fix built-in rules (#14)
  • 83 |
84 | 0.1.5
85 |
    86 |
  • Fix char lexer (#13)
  • 87 |
  • Fix psi reference range
  • 88 |
89 | 0.1.4
90 |
    91 |
  • Fix rename problem (#9)
  • 92 |
  • Support find usages (#12)
  • 93 |
94 | 0.1.3
95 |
    96 |
  • Fix PEEK parsing (#7, #8), contributed by 97 | @MalteSchledjewski 98 |
  • 99 |
  • Clear reference cache correctly (#10, #9)
  • 100 |
  • Old platform version (like CLion 183) compatibility (#6)
  • 101 |
  • Add tools menu option: browse website
  • 102 |
103 | 0.1.2
104 |
    105 |
  • Add more grammar pins to improve editing experience
  • 106 |
  • Rule extraction (this is currently poorly implemented)
  • 107 |
  • Fix exception (#2)
  • 108 |
109 | 0.1.1
110 |
    111 |
  • Add plugin icon
  • 112 |
  • Fix lexer (#4)
  • 113 |
114 | 0.1.0
115 |
    116 |
  • Improve completion for rules
  • 117 |
  • Add code folding
  • 118 |
  • Add structure view
  • 119 |
  • Support string literal injection
  • 120 |
  • Support create file action
  • 121 |
  • Add Quote handler (automatic insert paired quote)
  • 122 |
  • Spell checker (for comments/rule names, strings are suppressed)
  • 123 |
  • Automatically highlight pest code in #[grammar_inline = "..."]
  • 124 |
  • Recursive rule line marker
  • 125 |
  • Duplicated rule checker
  • 126 |
  • Live template completion for COMMENT and WHITESPACE
  • 127 |
  • Support rule inline (this is very fancy!)
  • 128 |
  • Fix rename unstable issue
  • 129 |
  • Add completion for built-in rules
  • 130 |
131 | 0.0.1
132 |
    133 |
  • File icon, different for dark and bright themes
  • 134 |
  • Completion for rules' names
  • 135 |
  • Keyword highlight built-in rules
  • 136 |
  • Rename for rules (and validate your rename!)
  • 137 |
  • Backspace deletes corresponding parenthesis/bracket/brace as well
  • 138 |
  • Click to go to definition for rules
  • 139 |
  • GitHub error reporter
  • 140 |
-------------------------------------------------------------------------------- /src/rs/pest/action/inline.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.action 2 | 3 | import com.intellij.lang.Language 4 | import com.intellij.lang.refactoring.InlineActionHandler 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.openapi.editor.Editor 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.psi.ElementDescriptionUtil 9 | import com.intellij.psi.PsiElement 10 | import com.intellij.psi.PsiReference 11 | import com.intellij.psi.util.PsiTreeUtil 12 | import com.intellij.refactoring.BaseRefactoringProcessor 13 | import com.intellij.refactoring.RefactoringBundle 14 | import com.intellij.refactoring.inline.InlineOptionsDialog 15 | import com.intellij.refactoring.util.CommonRefactoringUtil 16 | import com.intellij.usageView.UsageInfo 17 | import com.intellij.usageView.UsageViewBundle 18 | import com.intellij.usageView.UsageViewDescriptor 19 | import com.intellij.usageView.UsageViewNodeTextLocation 20 | import rs.pest.PestBundle 21 | import rs.pest.PestLanguage 22 | import rs.pest.psi.* 23 | import rs.pest.psi.impl.PestGrammarRuleMixin 24 | import rs.pest.psi.impl.bodyText 25 | 26 | class PestInlineRuleActionHandler : InlineActionHandler() { 27 | override fun isEnabledForLanguage(l: Language?) = l == PestLanguage.INSTANCE 28 | override fun canInlineElement(element: PsiElement?) = element is PestGrammarRuleMixin && !element.isRecursive 29 | override fun inlineElement(project: Project, editor: Editor?, element: PsiElement?) { 30 | val rule = element as? PestGrammarRuleMixin ?: return 31 | if (PsiTreeUtil.hasErrorElements(rule)) { 32 | CommonRefactoringUtil.showErrorHint(project, editor, PestBundle.message("pest.actions.inline.has-error.info"), PestBundle.message("pest.actions.inline.error.title"), null) 33 | return 34 | } 35 | if (rule.isRecursive) { 36 | CommonRefactoringUtil.showErrorHint(project, editor, PestBundle.message("pest.actions.inline.recursive.info"), PestBundle.message("pest.actions.inline.error.title"), null) 37 | return 38 | } 39 | if (!CommonRefactoringUtil.checkReadOnlyStatus(project, rule)) return 40 | val reference = editor?.let { rule.containingFile.findElementAt(it.caretModel.offset) } 41 | PestInlineDialog(project, element, reference).show() 42 | } 43 | } 44 | 45 | class PestInlineViewDescriptor(private val element: PestGrammarRuleMixin) : UsageViewDescriptor { 46 | override fun getElements() = arrayOf(element) 47 | override fun getProcessedElementsHeader() = PestBundle.message("pest.actions.inline.view.title") 48 | override fun getCodeReferencesText(usagesCount: Int, filesCount: Int): String = 49 | RefactoringBundle.message("invocations.to.be.inlined", UsageViewBundle.getReferencesString(usagesCount, filesCount)) 50 | 51 | override fun getCommentReferencesText(usagesCount: Int, filesCount: Int): String = 52 | RefactoringBundle.message("comments.elements.header", UsageViewBundle.getOccurencesString(usagesCount, filesCount)) 53 | } 54 | 55 | class PestInlineDialog(project: Project, val element: PestGrammarRuleMixin, private val reference: PsiElement?) 56 | : InlineOptionsDialog(project, true, element) { 57 | init { 58 | myInvokedOnReference = reference != null 59 | init() 60 | } 61 | 62 | override fun isInlineThis() = false 63 | override fun getNameLabelText() = ElementDescriptionUtil.getElementDescription(element, UsageViewNodeTextLocation.INSTANCE) 64 | override fun getInlineThisText() = PestBundle.message("pest.actions.inline.dialog.this") 65 | override fun getInlineAllText() = PestBundle.message("pest.actions.inline.dialog.all") 66 | override fun doAction() = invokeRefactoring(PestInlineProcessor(project, element, reference, isInlineThisOnly)) 67 | } 68 | 69 | class PestInlineProcessor( 70 | project: Project, 71 | val rule: PestGrammarRuleMixin, 72 | private val reference: PsiElement?, 73 | private val thisOnly: Boolean 74 | ) : BaseRefactoringProcessor(project) { 75 | private val name = rule.name 76 | override fun getCommandName() = PestBundle.message("pest.actions.inline.command.name", name) 77 | override fun createUsageViewDescriptor(usages: Array) = PestInlineViewDescriptor(rule) 78 | override fun findUsages() = rule 79 | .refreshReferenceCache() 80 | .asSequence() 81 | .map(PsiReference::getElement) 82 | .filter(PsiElement::isValid) 83 | .filterNot { PsiTreeUtil.isAncestor(rule, it, false) } 84 | .map(::UsageInfo) 85 | .toList() 86 | .toTypedArray() 87 | 88 | override fun performRefactoring(usages: Array) { 89 | val grammarBody = rule.grammarBody ?: return 90 | val newText = when (val expression = grammarBody.expression ?: return) { 91 | is PestString -> expression.text 92 | is PestCharacter -> expression.text 93 | is PestIdentifier -> expression.text 94 | is PestTerm -> expression.text 95 | is PestRange -> expression.text 96 | is PestBuiltin -> expression.text 97 | else -> "(${expression.bodyText(grammarBody.textLength).trim()})" 98 | } 99 | ApplicationManager.getApplication().runWriteAction { 100 | val newElement = PestTokenType.createExpression(newText, myProject)!! 101 | if (thisOnly) { 102 | reference?.replace(newElement) 103 | } else { 104 | usages.forEach { it.element?.replace(newElement) } 105 | rule.delete() 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/rs/pest/action/ui/PestIdeBridgeInfo.form: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for testing and preparing the plugin release in following steps: 2 | # - validate Gradle Wrapper, 3 | # - run 'test' and 'verifyPlugin' tasks, 4 | # - run Qodana inspections, 5 | # - run 'buildPlugin' task and prepare artifact for the further tests, 6 | # - run 'runPluginVerifier' task, 7 | # - create a draft release. 8 | # 9 | # Workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | ## JBIJPPTPL 14 | 15 | name: Build 16 | on: 17 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g. for dependabot pull requests) 18 | push: 19 | branches: [main, trying, staging] 20 | # Trigger the workflow on any pull request 21 | pull_request: 22 | 23 | jobs: 24 | 25 | # Run Gradle Wrapper Validation Action to verify the wrapper's checksum 26 | # Run verifyPlugin, IntelliJ Plugin Verifier, and test Gradle tasks 27 | # Build plugin and provide the artifact for the next workflow jobs 28 | build: 29 | name: Build 30 | runs-on: ubuntu-latest 31 | outputs: 32 | version: ${{ steps.properties.outputs.version }} 33 | changelog: ${{ steps.properties.outputs.changelog }} 34 | steps: 35 | 36 | # Check out current repository 37 | - name: Fetch Sources 38 | uses: actions/checkout@v4 39 | 40 | # Setup Java 22 environment for the next steps 41 | - name: Setup Java 42 | uses: actions/setup-java@v4 43 | with: 44 | distribution: zulu 45 | java-version: 22 46 | cache: gradle 47 | 48 | # Set environment variables 49 | - name: Export Properties 50 | id: properties 51 | shell: bash 52 | run: | 53 | PROPERTIES="$(./gradlew properties --console=plain -q)" 54 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 55 | NAME="$(echo "$PROPERTIES" | grep "^pluginName:" | cut -f2- -d ' ')" 56 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 57 | CHANGELOG="${CHANGELOG//'%'/'%25'}" 58 | CHANGELOG="${CHANGELOG//$'\n'/'%0A'}" 59 | CHANGELOG="${CHANGELOG//$'\r'/'%0D'}" 60 | 61 | echo "version=$VERSION" >> $GITHUB_OUTPUT 62 | echo "name=$NAME" >> $GITHUB_OUTPUT 63 | echo "changelog=$CHANGELOG" >> $GITHUB_OUTPUT 64 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 65 | 66 | ./gradlew printProductsReleases # prepare list of IDEs for Plugin Verifier 67 | 68 | # Would be nice if we can make a PR of some kind from this change 69 | - name: Update Aya Parser 70 | run: ./gradlew updateAyaParser 71 | 72 | - name: Run Tests 73 | run: ./gradlew test 74 | 75 | # Collect Tests Result of failed tests 76 | - name: Collect Tests Result 77 | if: ${{ failure() }} 78 | uses: actions/upload-artifact@v4 79 | with: 80 | name: tests-result 81 | path: ${{ github.workspace }}/build/reports/tests 82 | 83 | # Build plugin 84 | - name: Build Plugin 85 | run: ./gradlew buildPlugin 86 | 87 | # Prepare plugin archive content for creating artifact 88 | - name: Prepare Plugin Artifact 89 | id: artifact 90 | shell: bash 91 | run: | 92 | cd ${{ github.workspace }}/build/distributions 93 | FILENAME=`ls *.zip` 94 | unzip "$FILENAME" -d content 95 | 96 | echo "::set-output name=filename::$FILENAME" 97 | 98 | # Store already-built plugin as an artifact for downloading 99 | - name: Upload artifact 100 | uses: actions/upload-artifact@v4 101 | with: 102 | name: ${{ steps.artifact.outputs.filename }} 103 | path: ./build/distributions/content/*/* 104 | 105 | # Prepare a draft release for GitHub Releases page for the manual verification 106 | # If accepted and published, release workflow would be triggered 107 | releaseDraft: 108 | name: Release Draft 109 | if: github.base_ref == null && github.ref == 'refs/heads/main' 110 | needs: build 111 | runs-on: ubuntu-latest 112 | steps: 113 | 114 | # Check out current repository 115 | - name: Fetch Sources 116 | uses: actions/checkout@v4 117 | 118 | # Remove old release drafts by using the curl request for the available releases with draft flag 119 | - name: Remove Old Release Drafts 120 | env: 121 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 122 | run: | 123 | gh api repos/{owner}/{repo}/releases \ 124 | --jq '.[] | select(.draft == true) | .id' \ 125 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 126 | 127 | # Create new release draft - which is not publicly visible and requires manual acceptance 128 | - name: Create Release Draft 129 | env: 130 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 131 | run: | 132 | gh release create v${{ needs.build.outputs.version }} \ 133 | --draft \ 134 | --title "v${{ needs.build.outputs.version }}" \ 135 | --notes "$(cat << 'EOM' 136 | ${{ needs.build.outputs.changelog }} 137 | EOM 138 | )" 139 | -------------------------------------------------------------------------------- /res/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | Pest 3 | ice1000 4 | 5 | 6 | com.intellij.modules.lang 7 | org.rust.lang 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 | 50 | 57 | 60 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 77 | 78 | 79 | 85 | 90 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/rs/pest/livePreview/live-highlight.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.livePreview 2 | 3 | import com.intellij.lang.annotation.AnnotationHolder 4 | import com.intellij.lang.annotation.Annotator 5 | import com.intellij.lang.annotation.HighlightSeverity 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.editor.EditorFactory 8 | import com.intellij.openapi.editor.markup.TextAttributes 9 | import com.intellij.openapi.ui.popup.Balloon 10 | import com.intellij.openapi.ui.popup.JBPopupFactory 11 | import com.intellij.openapi.util.TextRange 12 | import com.intellij.psi.PsiComment 13 | import com.intellij.psi.PsiDocumentManager 14 | import com.intellij.psi.PsiElement 15 | import com.intellij.ui.JBColor 16 | import com.intellij.ui.components.JBTextArea 17 | import org.intellij.lang.annotations.Language 18 | import rs.pest.PestBundle 19 | import rs.pest.psi.PestTokenType 20 | import rs.pest.psi.childrenWithLeaves 21 | import rs.pest.psi.elementType 22 | import java.awt.Color 23 | import javax.swing.JPanel 24 | 25 | fun rgbToAttributes(rgb: String) = stringToColor(rgb) 26 | ?.let { TextAttributes().apply { foregroundColor = it } } 27 | 28 | private fun stringToColor(rgb: String) = when { 29 | rgb.startsWith("#") -> rgb.drop(1).toIntOrNull(16)?.let(::Color) 30 | else -> Color::class.java.fields 31 | .firstOrNull { it.name.equals(rgb, ignoreCase = true) } 32 | ?.let { it.get(null) as? Color } 33 | } 34 | 35 | fun textAttrFromDoc(docComment: PsiComment) = 36 | docComment.text.removePrefix("///").trim().let(::rgbToAttributes) 37 | 38 | @Language("RegExp") 39 | @JvmField 40 | val lexicalRegex = Regex("\\A(\\d+)\\^(\\d+)\\^(.*)$") 41 | 42 | @Language("RegExp") 43 | @JvmField 44 | val errLineRegex = Regex("\\A\\s+-->\\s+(\\d+):(\\d+)$") 45 | 46 | @Language("RegExp") 47 | @JvmField 48 | val errInfoRegex = Regex("\\A\\s+=\\s+(\\p{all}*)$") 49 | 50 | class LivePreviewAnnotator : Annotator { 51 | override fun annotate(element: PsiElement, holder: AnnotationHolder) { 52 | if (element !is LivePreviewFile) return 53 | highlight(element, { range, info -> 54 | holder.newAnnotation(HighlightSeverity.ERROR, info.orEmpty()) 55 | .range(range) 56 | .create() 57 | }) { range, rule, attributes -> 58 | holder.newAnnotation(HighlightSeverity.INFORMATION, rule) 59 | .enforcedTextAttributes(attributes) 60 | .range(range) 61 | .create() 62 | } 63 | } 64 | } 65 | 66 | inline fun highlight( 67 | element: LivePreviewFile, 68 | err: (TextRange, String?) -> Unit, 69 | okk: (TextRange, String, TextAttributes) -> Unit 70 | ) { 71 | val project = element.project 72 | val dom = PsiDocumentManager.getInstance(project).getDocument(element) 73 | ?: return 74 | val pestFile = element.pestFile ?: return 75 | val ruleName = element.ruleName ?: return 76 | val rules = pestFile.rules().map { it.name to it }.toMap() 77 | val regexes = pestFile.childrenWithLeaves 78 | .filter { it.elementType == PestTokenType.LINE_REGEX_COMMENT } 79 | .mapNotNull { 80 | val expr = it.text.removePrefix("//!") 81 | val (color, regex) = expr.split(":", limit = 2) 82 | .takeIf { it.size == 2 } ?: return@mapNotNull null 83 | val c = rgbToAttributes(color) ?: return@mapNotNull null 84 | val r = try { 85 | Regex(regex.trim()) 86 | } catch (_: Exception) { 87 | return@mapNotNull null 88 | } 89 | c to r 90 | } 91 | .toList() 92 | if (rules.isEmpty()) return 93 | if (pestFile.errors.any()) return 94 | if (pestFile.availableRules.none()) return 95 | val vm = pestFile.vm 96 | when (val res = try { 97 | vm.renderCode(ruleName, element.text) 98 | } catch (e: Exception) { 99 | vm.reboot() 100 | vm.loadVM(pestFile.text) 101 | vm.renderCode(ruleName, element.text) 102 | }) { 103 | is Rendering.Err -> run { 104 | val errorLines = res.msg.lines() 105 | val firstLine = errorLines.firstOrNull() ?: return@run null 106 | val lastLine = errorLines.lastOrNull() ?: return@run null 107 | val length = dom.textLength 108 | if (length == 0) return@run null 109 | val (_, lineS, colS) = errLineRegex.matchEntire(firstLine)?.groupValues 110 | ?: return@run null 111 | val line = lineS.toIntOrNull() ?: return@run null 112 | val col = colS.toIntOrNull() ?: return@run null 113 | val lineStart = dom.getLineStartOffset(line - 1) 114 | val offset = lineStart + col - 1 115 | val range = if (offset >= dom.textLength) TextRange(length - 1, length) 116 | else TextRange(offset, offset + 1) 117 | val errorMsg = errInfoRegex.matchEntire(lastLine)?.run { groupValues[1] } 118 | err(range, errorMsg) 119 | } ?: ApplicationManager.getApplication().invokeLater { 120 | val panel = JPanel().apply { add(JBTextArea().apply { text = res.msg }) } 121 | val editor = EditorFactory.getInstance() 122 | .getEditors(dom, project) 123 | .firstOrNull() 124 | ?: return@invokeLater 125 | val factory = JBPopupFactory.getInstance() 126 | factory 127 | .createBalloonBuilder(panel) 128 | .setTitle(PestBundle.message("pest.annotator.live-preview.error.title")) 129 | .setFillColor(JBColor.RED) 130 | .createBalloon() 131 | .show(factory.guessBestPopupLocation(editor), Balloon.Position.below) 132 | } 133 | is Rendering.Ok -> res.lexical.mapNotNull(lexicalRegex::matchEntire).forEach { 134 | val (_, start, end, rule) = it.groupValues 135 | val psiRule = rules[rule] ?: return@forEach 136 | val attributes = psiRule.docComment?.let(::textAttrFromDoc) 137 | ?: regexes.asSequence().firstOrNull { (_, regex) -> 138 | regex.matchEntire(psiRule.name) != null 139 | }?.first ?: return@forEach 140 | val range = TextRange(start.toInt(), end.toInt()) 141 | okk(range, rule, attributes) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/rs/pest/psi/PestStringEscaper.java: -------------------------------------------------------------------------------- 1 | package rs.pest.psi; 2 | 3 | import com.intellij.openapi.util.Ref; 4 | import com.intellij.openapi.util.TextRange; 5 | import com.intellij.psi.LiteralTextEscaper; 6 | import com.intellij.psi.PsiLanguageInjectionHost; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | public class PestStringEscaper extends LiteralTextEscaper { 10 | private int[] outSourceOffsets; 11 | 12 | public PestStringEscaper(@NotNull T host) { 13 | super(host); 14 | } 15 | 16 | @Override 17 | public boolean decode(@NotNull final TextRange rangeInsideHost, @NotNull StringBuilder outChars) { 18 | String subText = rangeInsideHost.substring(myHost.getText()); 19 | 20 | Ref sourceOffsetsRef = new Ref<>(); 21 | boolean result = parseStringCharacters(subText, outChars, sourceOffsetsRef, !isOneLine()); 22 | outSourceOffsets = sourceOffsetsRef.get(); 23 | return result; 24 | } 25 | 26 | @Override 27 | public int getOffsetInHost(int offsetInDecoded, @NotNull final TextRange rangeInsideHost) { 28 | int result = offsetInDecoded < outSourceOffsets.length ? outSourceOffsets[offsetInDecoded] : -1; 29 | if (result == -1) return -1; 30 | return (result <= rangeInsideHost.getLength() ? result : rangeInsideHost.getLength()) + 31 | rangeInsideHost.getStartOffset(); 32 | } 33 | 34 | @Override 35 | public boolean isOneLine() { 36 | return true; 37 | } 38 | 39 | public static boolean parseStringCharacters( 40 | @NotNull String chars, 41 | StringBuilder outChars, 42 | @NotNull Ref sourceOffsetsRef, 43 | boolean escapeBacktick) { 44 | int[] sourceOffsets = new int[chars.length() + 1]; 45 | sourceOffsetsRef.set(sourceOffsets); 46 | 47 | if (chars.indexOf('\\') < 0) { 48 | outChars.append(chars); 49 | for (int i = 0; i < sourceOffsets.length; i++) { 50 | sourceOffsets[i] = i; 51 | } 52 | return true; 53 | } 54 | 55 | int index = 0; 56 | final int outOffset = outChars.length(); 57 | while (index < chars.length()) { 58 | char c = chars.charAt(index++); 59 | 60 | sourceOffsets[outChars.length() - outOffset] = index - 1; 61 | sourceOffsets[outChars.length() + 1 - outOffset] = index; 62 | 63 | if (c != '\\') { 64 | outChars.append(c); 65 | continue; 66 | } 67 | if (index == chars.length()) return false; 68 | c = chars.charAt(index++); 69 | if (escapeBacktick && c == '`') { 70 | outChars.append(c); 71 | } else { 72 | switch (c) { 73 | case 'b': 74 | outChars.append('\b'); 75 | break; 76 | 77 | case 't': 78 | outChars.append('\t'); 79 | break; 80 | 81 | case 'n': 82 | case '\n': 83 | outChars.append('\n'); 84 | break; 85 | 86 | case 'f': 87 | outChars.append('\f'); 88 | break; 89 | 90 | case 'r': 91 | outChars.append('\r'); 92 | break; 93 | 94 | case '"': 95 | outChars.append('"'); 96 | break; 97 | 98 | case '/': 99 | outChars.append('/'); 100 | break; 101 | 102 | case '\'': 103 | outChars.append('\''); 104 | break; 105 | 106 | case '\\': 107 | outChars.append('\\'); 108 | break; 109 | 110 | case '0': 111 | case '1': 112 | case '2': 113 | case '3': 114 | case '4': 115 | case '5': 116 | case '6': 117 | case '7': 118 | index = number(chars, outChars, index, c); 119 | break; 120 | case 'x': 121 | if (index + 2 <= chars.length()) { 122 | try { 123 | int v = Integer.parseInt(chars.substring(index, index + 2), 16); 124 | outChars.append((char) v); 125 | index += 2; 126 | } catch (Exception e) { 127 | return false; 128 | } 129 | } else { 130 | return false; 131 | } 132 | break; 133 | case 'u': 134 | index = unicode(chars, outChars, index); 135 | if (index == -1) return false; 136 | else break; 137 | 138 | default: 139 | outChars.append(c); 140 | break; 141 | } 142 | } 143 | 144 | sourceOffsets[outChars.length() - outOffset] = index; 145 | } 146 | return true; 147 | } 148 | 149 | private static int number(@NotNull String chars, StringBuilder outChars, int index, char c) { 150 | char startC = c; 151 | int v = (int) c - '0'; 152 | if (index < chars.length()) { 153 | c = chars.charAt(index++); 154 | if ('0' <= c && c <= '7') { 155 | v <<= 3; 156 | v += c - '0'; 157 | if (startC <= '3' && index < chars.length()) { 158 | c = chars.charAt(index++); 159 | if ('0' <= c && c <= '7') { 160 | v <<= 3; 161 | v += c - '0'; 162 | } else { 163 | index--; 164 | } 165 | } 166 | } else { 167 | index--; 168 | } 169 | } 170 | outChars.append((char) v); 171 | return index; 172 | } 173 | 174 | public static int unicode(@NotNull String chars, StringBuilder outChars, int index) { 175 | if (index + 4 > chars.length()) return -1; 176 | if (chars.charAt(index) != '{') return -1; 177 | int range; 178 | if (chars.charAt(index + 3) == '}') range = 2; 179 | else if (index + 4 < chars.length() && chars.charAt(index + 4) == '}') range = 3; 180 | else if (index + 5 < chars.length() && chars.charAt(index + 5) == '}') range = 4; 181 | else if (index + 6 < chars.length() && chars.charAt(index + 6) == '}') range = 5; 182 | else if (index + 7 < chars.length() && chars.charAt(index + 7) == '}') range = 6; 183 | else return -1; 184 | try { 185 | int v = Integer.parseInt(chars.substring(index + 1, index + 1 + range), 16); 186 | char c = chars.charAt(index + 1); 187 | if (c == '+' || c == '-') return -1; 188 | outChars.append((char) v); 189 | return index + range + 2; 190 | } catch (Exception e) { 191 | return -1; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/rs/pest/psi/impl/psi-mixins.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.psi.impl 2 | 3 | import com.intellij.codeInsight.lookup.LookupElementBuilder 4 | import com.intellij.extapi.psi.ASTWrapperPsiElement 5 | import com.intellij.lang.ASTNode 6 | import com.intellij.openapi.util.TextRange 7 | import com.intellij.psi.* 8 | import com.intellij.psi.search.LocalSearchScope 9 | import com.intellij.util.IncorrectOperationException 10 | import icons.PestIcons 11 | import rs.pest.PestFile 12 | import rs.pest.psi.* 13 | 14 | abstract class PestElement(node: ASTNode) : ASTWrapperPsiElement(node) { 15 | val containingPestFile get() = containingFile as? PestFile 16 | protected fun allGrammarRules(): Collection = containingPestFile?.rules().orEmpty() 17 | override fun getUseScope(): LocalSearchScope { 18 | val containingFile = containingFile 19 | return LocalSearchScope(containingFile, containingFile.name) 20 | } 21 | } 22 | 23 | abstract class PestGrammarRuleMixin(node: ASTNode) : PestElement(node), PsiNameIdentifierOwner, PestGrammarRule { 24 | private var recCache: Boolean? = null 25 | val isRecursive 26 | get() = recCache ?: run { 27 | val grammarBody = grammarBody?.expression ?: return@run false 28 | val name = name 29 | SyntaxTraverser 30 | .psiTraverser(grammarBody) 31 | .filterTypes { it == PestTypes.IDENTIFIER } 32 | .any { it.text == name } 33 | }.also { recCache = it } 34 | 35 | fun refreshReferenceCache() = refreshReferenceCache(name, nameIdentifier) 36 | private fun refreshReferenceCache(myName: String, self: PsiElement) = collectFrom(containingFile, myName, self) 37 | override fun subtreeChanged() { 38 | typeCache = null 39 | recCache = null 40 | } 41 | 42 | override fun getNameIdentifier(): PsiElement = firstChild 43 | override fun getIcon(flags: Int) = PestIcons.PEST 44 | override fun getName(): String = nameIdentifier.text 45 | @Throws(IncorrectOperationException::class) 46 | override fun setName(newName: String): PsiElement { 47 | firstChild.replace(PestTokenType.createRuleName(newName, project) 48 | ?: throw IncorrectOperationException("Invalid name $newName")) 49 | return this 50 | } 51 | 52 | val docComment: PsiComment? 53 | get() { 54 | var prevSibling = prevSibling 55 | while (prevSibling != null) { 56 | if (prevSibling is PsiComment && prevSibling.elementType == PestTokenType.LINE_DOC_COMMENT) return prevSibling 57 | else if (prevSibling is PestGrammarRule) return null 58 | else prevSibling = prevSibling.prevSibling 59 | } 60 | return null 61 | } 62 | 63 | fun preview(maxSizeExpected: Int) = grammarBody?.expression?.bodyText(maxSizeExpected) 64 | private var typeCache: PestRuleType? = null 65 | val type: PestRuleType 66 | get() = typeCache ?: when (modifier?.firstChild?.node?.elementType) { 67 | PestTypes.SILENT_MODIFIER -> PestRuleType.Silent 68 | PestTypes.ATOMIC_MODIFIER -> PestRuleType.Atomic 69 | PestTypes.NON_ATOMIC_MODIFIER -> PestRuleType.NonAtomic 70 | PestTypes.COMPOUND_ATOMIC_MODIFIER -> PestRuleType.CompoundAtomic 71 | else -> PestRuleType.Simple 72 | }.also { typeCache = it } 73 | } 74 | 75 | abstract class PestResolvableMixin(node: ASTNode) : PestExpressionImpl(node), PsiPolyVariantReference { 76 | override fun isSoft() = true 77 | override fun getRangeInElement() = TextRange(0, textLength) 78 | 79 | override fun getReference() = this 80 | override fun getReferences() = arrayOf(reference) 81 | override fun isReferenceTo(reference: PsiElement) = reference == resolve() 82 | override fun getCanonicalText(): String = text 83 | override fun resolve(): PsiElement? = multiResolve(false).firstOrNull()?.run { element } 84 | override fun multiResolve(incomplete: Boolean): Array = allGrammarRules().filter { it.name == text }.map(::PsiElementResolveResult).toTypedArray() 85 | override fun getElement() = this 86 | override fun bindToElement(element: PsiElement): PsiElement = throw IncorrectOperationException("Unsupported") 87 | override fun getVariants() = allGrammarRules().map { 88 | LookupElementBuilder 89 | .create(it) 90 | .withTailText(it.preview(35), true) 91 | .withIcon(it.getIcon(0)) 92 | .withTypeText(it.type.description) 93 | }.toTypedArray() 94 | } 95 | 96 | 97 | abstract class PestIdentifierMixin(node: ASTNode) : PestResolvableMixin(node) { 98 | @Throws(IncorrectOperationException::class) 99 | override fun handleElementRename(newName: String): PsiElement = replace( 100 | PestTokenType.createExpression(newName, project) 101 | ?: throw IncorrectOperationException("Invalid name: $newName")) 102 | } 103 | 104 | abstract class PestBuiltinMixin(node: ASTNode) : PestResolvableMixin(node) { 105 | @Throws(IncorrectOperationException::class) 106 | override fun handleElementRename(newName: String): PsiElement = replace( 107 | PestTokenType.createExpression(newName, project) 108 | ?: throw IncorrectOperationException("Invalid name: $newName")) 109 | } 110 | 111 | abstract class PestStringMixin(node: ASTNode) : PestExpressionImpl(node), PsiLanguageInjectionHost { 112 | override fun isValidHost() = true 113 | override fun updateText(text: String) = PestTokenType.createExpression(text, project)?.let(::replace) as? PestStringMixin 114 | override fun createLiteralTextEscaper(): LiteralTextEscaper = PestStringEscaper(this) 115 | } 116 | 117 | abstract class PestFixedBuiltinRuleNameMixin(node: ASTNode) : PestElement(node), PestFixedBuiltinRuleName 118 | abstract class PestCustomizableRuleNameMixin(node: ASTNode) : PestResolvableMixin(node), PestCustomizableRuleName { 119 | @Throws(IncorrectOperationException::class) 120 | override fun handleElementRename(newName: String): PsiElement = throw IncorrectOperationException("Cannot rename") 121 | } 122 | 123 | abstract class PestRuleNameMixin(node: ASTNode) : PestElement(node), PestValidRuleName 124 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /grammar/pest.bnf: -------------------------------------------------------------------------------- 1 | //noinspection BnfResolveForFile 2 | { 3 | generate=[java="8" names="long"] 4 | 5 | parserClass="rs.pest.psi.PestParser" 6 | 7 | extends="rs.pest.psi.impl.PestElement" 8 | 9 | psiClassPrefix="Pest" 10 | psiImplClassSuffix="Impl" 11 | psiPackage="rs.pest.psi" 12 | psiImplPackage="rs.pest.psi.impl" 13 | 14 | elementTypeHolderClass="rs.pest.psi.PestTypes" 15 | elementTypeClass="rs.pest.psi.PestElementType" 16 | tokenTypeClass="rs.pest.psi.PestTokenType" 17 | } 18 | 19 | grammar_rules ::= grammar_rule+ { recoverWhile=grammar_rules_recover } 20 | 21 | grammar_body ::= OPENING_BRACE expression CLOSING_BRACE { 22 | pin=1 23 | recoverWhile=grammar_body_recover 24 | } 25 | 26 | private grammar_body_recover ::= !rule_name 27 | 28 | grammar_rule ::= rule_name ASSIGNMENT_OPERATOR modifier? grammar_body { 29 | implements=["com.intellij.psi.PsiNameIdentifierOwner"] 30 | mixin="rs.pest.psi.impl.PestGrammarRuleMixin" 31 | pin=2 32 | recoverWhile=grammar_rule_recover 33 | } 34 | 35 | private rule_name ::= 36 | valid_rule_name 37 | | customizable_rule_name 38 | | fixed_builtin_rule_name 39 | 40 | fixed_builtin_rule_name ::= commands | builtin { 41 | mixin="rs.pest.psi.impl.PestFixedBuiltinRuleNameMixin" 42 | } 43 | 44 | customizable_rule_name ::= WHITESPACE_TOKEN | COMMENT_TOKEN { 45 | mixin="rs.pest.psi.impl.PestCustomizableRuleNameMixin" 46 | } 47 | 48 | valid_rule_name ::= IDENTIFIER_TOKEN { 49 | mixin="rs.pest.psi.impl.PestRuleNameMixin" 50 | } 51 | 52 | private grammar_rule_recover ::= !(rule_name ) 53 | private grammar_rules_recover ::= !(rule_name ASSIGNMENT_OPERATOR) 54 | 55 | modifier ::= 56 | SILENT_MODIFIER | 57 | ATOMIC_MODIFIER | 58 | COMPOUND_ATOMIC_MODIFIER | 59 | NON_ATOMIC_MODIFIER 60 | 61 | expression ::= term infix_part* 62 | term ::= prefix_operator* rule postfix_operator* { pin=1 extends=expression } 63 | // Can't name `node`, conflict with PsiElement's class 64 | rule ::= paren_part | atom { extends=expression } 65 | 66 | private paren_part ::= OPENING_PAREN expression CLOSING_PAREN { pin=1 } 67 | private infix_part ::= infix_operator term { pin=1 } 68 | 69 | private atom ::= 70 | push | 71 | peek | 72 | identifier | 73 | customizable_rule_name | 74 | string | 75 | range | 76 | builtin | 77 | commands 78 | 79 | prefix_operator ::= POSITIVE_PREDICATE_OPERATOR | NON_ATOMIC_MODIFIER 80 | infix_operator ::= SEQUENCE_OPERATOR | CHOICE_OPERATOR 81 | postfix_operator ::= 82 | OPTIONAL_OPERATOR | 83 | REPEAT_OPERATOR | 84 | REPEAT_ONCE_OPERATOR | 85 | repeat_exact | 86 | repeat_min | 87 | repeat_max | 88 | repeat_min_max 89 | 90 | private repeat_exact ::= OPENING_BRACE NUMBER CLOSING_BRACE 91 | private repeat_min ::= OPENING_BRACE NUMBER COMMA CLOSING_BRACE 92 | private repeat_max ::= OPENING_BRACE COMMA NUMBER CLOSING_BRACE 93 | private repeat_min_max ::= OPENING_BRACE NUMBER COMMA NUMBER CLOSING_BRACE 94 | 95 | integer ::= MINUS? NUMBER 96 | identifier ::= IDENTIFIER_TOKEN { 97 | mixin="rs.pest.psi.impl.PestIdentifierMixin" 98 | extends=expression 99 | } 100 | 101 | push ::= PUSH_TOKEN OPENING_PAREN expression CLOSING_PAREN { 102 | extends=expression 103 | pin=1 104 | name="PUSH" 105 | } 106 | 107 | peek ::= PEEK_TOKEN peek_slice? { 108 | extends=expression 109 | pin=1 110 | name="PEEK" 111 | } 112 | 113 | peek_slice ::= OPENING_BRACK integer? RANGE_OPERATOR integer? CLOSING_BRACK { 114 | pin=1 115 | name="PEEK.." 116 | } 117 | 118 | commands ::= 119 | PEEK_ALL_TOKEN 120 | | POP_TOKEN 121 | | POP_ALL_TOKEN { extends=expression } 122 | builtin ::= 123 | LETTER_TOKEN 124 | | CASED_LETTER_TOKEN 125 | | UPPERCASE_LETTER_TOKEN 126 | | LOWERCASE_LETTER_TOKEN 127 | | TITLECASE_LETTER_TOKEN 128 | | MODIFIER_LETTER_TOKEN 129 | | OTHER_LETTER_TOKEN 130 | | MARK_TOKEN 131 | | NONSPACING_MARK_TOKEN 132 | | SPACING_MARK_TOKEN 133 | | ENCLOSING_MARK_TOKEN 134 | | NUMBER_TOKEN 135 | | DECIMAL_NUMBER_TOKEN 136 | | LETTER_NUMBER_TOKEN 137 | | OTHER_NUMBER_TOKEN 138 | | PUNCTUATION_TOKEN 139 | | CONNECTOR_PUNCTUATION_TOKEN 140 | | DASH_PUNCTUATION_TOKEN 141 | | OPEN_PUNCTUATION_TOKEN 142 | | CLOSE_PUNCTUATION_TOKEN 143 | | INITIAL_PUNCTUATION_TOKEN 144 | | FINAL_PUNCTUATION_TOKEN 145 | | OTHER_PUNCTUATION_TOKEN 146 | | SYMBOL_TOKEN 147 | | MATH_SYMBOL_TOKEN 148 | | CURRENCY_SYMBOL_TOKEN 149 | | MODIFIER_SYMBOL_TOKEN 150 | | OTHER_SYMBOL_TOKEN 151 | | SEPARATOR_TOKEN 152 | | SPACE_SEPARATOR_TOKEN 153 | | LINE_SEPARATOR_TOKEN 154 | | PARAGRAPH_SEPARATOR_TOKEN 155 | | OTHER_TOKEN 156 | | CONTROL_TOKEN 157 | | FORMAT_TOKEN 158 | | SURROGATE_TOKEN 159 | | PRIVATE_USE_TOKEN 160 | | UNASSIGNED_TOKEN 161 | | ALPHABETIC_TOKEN 162 | | BIDI_CONTROL_TOKEN 163 | | CASE_IGNORABLE_TOKEN 164 | | CASED_TOKEN 165 | | CHANGES_WHEN_CASEFOLDED_TOKEN 166 | | CHANGES_WHEN_CASEMAPPED_TOKEN 167 | | CHANGES_WHEN_LOWERCASED_TOKEN 168 | | CHANGES_WHEN_TITLECASED_TOKEN 169 | | CHANGES_WHEN_UPPERCASED_TOKEN 170 | | DASH_TOKEN 171 | | DEFAULT_IGNORABLE_CODE_POINT_TOKEN 172 | | DEPRECATED_TOKEN 173 | | DIACRITIC_TOKEN 174 | | EXTENDER_TOKEN 175 | | GRAPHEME_BASE_TOKEN 176 | | GRAPHEME_EXTEND_TOKEN 177 | | GRAPHEME_LINK_TOKEN 178 | | HEX_DIGIT_TOKEN 179 | | HYPHEN_TOKEN 180 | | IDS_BINARY_OPERATOR_TOKEN 181 | | IDS_TRINARY_OPERATOR_TOKEN 182 | | ID_CONTINUE_TOKEN 183 | | ID_START_TOKEN 184 | | IDEOGRAPHIC_TOKEN 185 | | JOIN_CONTROL_TOKEN 186 | | LOGICAL_ORDER_EXCEPTION_TOKEN 187 | | LOWERCASE_TOKEN 188 | | MATH_TOKEN 189 | | NONCHARACTER_CODE_POINT_TOKEN 190 | | OTHER_ALPHABETIC_TOKEN 191 | | OTHER_DEFAULT_IGNORABLE_CODE_POINT_TOKEN 192 | | OTHER_GRAPHEME_EXTEND_TOKEN 193 | | OTHER_ID_CONTINUE_TOKEN 194 | | OTHER_ID_START_TOKEN 195 | | OTHER_LOWERCASE_TOKEN 196 | | OTHER_MATH_TOKEN 197 | | OTHER_UPPERCASE_TOKEN 198 | | PATTERN_SYNTAX_TOKEN 199 | | PATTERN_WHITE_SPACE_TOKEN 200 | | PREPENDED_CONCATENATION_MARK_TOKEN 201 | | QUOTATION_MARK_TOKEN 202 | | RADICAL_TOKEN 203 | | REGIONAL_INDICATOR_TOKEN 204 | | SENTENCE_TERMINAL_TOKEN 205 | | SOFT_DOTTED_TOKEN 206 | | TERMINAL_PUNCTUATION_TOKEN 207 | | UNIFIED_IDEOGRAPH_TOKEN 208 | | UPPERCASE_TOKEN 209 | | VARIATION_SELECTOR_TOKEN 210 | | WHITE_SPACE_TOKEN 211 | | XID_CONTINUE_TOKEN 212 | | XID_START_TOKEN 213 | | PUSH_TOKEN 214 | | PEEK_TOKEN 215 | | PEEK_ALL_TOKEN 216 | | POP_TOKEN 217 | | POP_ALL_TOKEN 218 | | ANY_TOKEN 219 | | EOI_TOKEN 220 | | SOI_TOKEN 221 | | DROP_TOKEN 222 | | ASCII_TOKEN 223 | | NEWLINE_TOKEN 224 | | ASCII_DIGIT_TOKEN 225 | | ASCII_ALPHA_TOKEN 226 | | ASCII_ALPHANUMERIC_TOKEN 227 | | ASCII_NONZERO_DIGIT_TOKEN 228 | | ASCII_BIN_DIGIT_TOKEN 229 | | ASCII_OCT_DIGIT_TOKEN 230 | | ASCII_HEX_DIGIT_TOKEN 231 | | ASCII_ALPHA_UPPER_TOKEN 232 | | ASCII_ALPHA_LOWER_TOKEN { 233 | extends=expression 234 | mixin="rs.pest.psi.impl.PestBuiltinMixin" 235 | } 236 | 237 | character ::= CHAR_TOKEN { extends=expression } 238 | string ::= INSENSITIVE_OPERATOR? STRING_TOKEN { 239 | extends=expression 240 | implements=["com.intellij.psi.PsiLanguageInjectionHost"] 241 | mixin="rs.pest.psi.impl.PestStringMixin" 242 | } 243 | 244 | range ::= character RANGE_OPERATOR character { 245 | name=".." 246 | extends=expression 247 | pin=2 248 | } 249 | 250 | -------------------------------------------------------------------------------- /src/rs/pest/editing/pest-editing.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.editing 2 | 3 | import com.intellij.codeInsight.CodeInsightSettings 4 | import com.intellij.codeInsight.editorActions.BackspaceHandlerDelegate 5 | import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler 6 | import com.intellij.lang.ASTNode 7 | import com.intellij.lang.BracePair 8 | import com.intellij.lang.Commenter 9 | import com.intellij.lang.PairedBraceMatcher 10 | import com.intellij.lang.cacheBuilder.DefaultWordsScanner 11 | import com.intellij.lang.findUsages.FindUsagesProvider 12 | import com.intellij.lang.folding.CustomFoldingBuilder 13 | import com.intellij.lang.folding.FoldingDescriptor 14 | import com.intellij.lang.refactoring.NamesValidator 15 | import com.intellij.lang.refactoring.RefactoringSupportProvider 16 | import com.intellij.lexer.Lexer 17 | import com.intellij.openapi.editor.Document 18 | import com.intellij.openapi.editor.Editor 19 | import com.intellij.openapi.editor.ex.EditorEx 20 | import com.intellij.openapi.editor.highlighter.HighlighterIterator 21 | import com.intellij.openapi.project.Project 22 | import com.intellij.openapi.util.TextRange 23 | import com.intellij.psi.PsiElement 24 | import com.intellij.psi.PsiFile 25 | import com.intellij.psi.PsiNameIdentifierOwner 26 | import com.intellij.psi.PsiNamedElement 27 | import com.intellij.psi.impl.cache.impl.BaseFilterLexer 28 | import com.intellij.psi.impl.cache.impl.OccurrenceConsumer 29 | import com.intellij.psi.impl.cache.impl.id.LexerBasedIdIndexer 30 | import com.intellij.psi.impl.cache.impl.todo.LexerBasedTodoIndexer 31 | import com.intellij.psi.impl.search.IndexPatternBuilder 32 | import com.intellij.psi.search.UsageSearchContext 33 | import com.intellij.psi.tree.IElementType 34 | import com.intellij.psi.tree.TokenSet 35 | import com.intellij.spellchecker.tokenizer.PsiIdentifierOwnerTokenizer 36 | import com.intellij.spellchecker.tokenizer.SpellcheckingStrategy 37 | import com.intellij.spellchecker.tokenizer.Tokenizer 38 | import rs.pest.* 39 | import rs.pest.psi.* 40 | 41 | class PestCommenter : Commenter { 42 | override fun getCommentedBlockCommentPrefix() = blockCommentPrefix 43 | override fun getCommentedBlockCommentSuffix() = blockCommentSuffix 44 | override fun getBlockCommentPrefix() = PEST_BLOCK_COMMENT_BEGIN 45 | override fun getBlockCommentSuffix() = PEST_BLOCK_COMMENT_END 46 | override fun getLineCommentPrefix() = PEST_LINE_COMMENT 47 | } 48 | 49 | class PestBraceMatcher : PairedBraceMatcher { 50 | private companion object Pairs { 51 | private val PAIRS = arrayOf( 52 | BracePair(PestTypes.OPENING_BRACK, PestTypes.CLOSING_BRACK, false), 53 | BracePair(PestTypes.OPENING_PAREN, PestTypes.CLOSING_PAREN, false), 54 | BracePair(PestTypes.OPENING_BRACE, PestTypes.CLOSING_BRACE, false)) 55 | } 56 | 57 | override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int) = openingBraceOffset 58 | override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?) = true 59 | override fun getPairs() = PAIRS 60 | } 61 | 62 | 63 | class PestTodoIndexer : LexerBasedTodoIndexer() { 64 | override fun createLexer(consumer: OccurrenceConsumer): Lexer = PestIdIndexer.createIndexingLexer(consumer) 65 | } 66 | 67 | class PestIdIndexer : LexerBasedIdIndexer() { 68 | override fun createLexer(consumer: OccurrenceConsumer) = createIndexingLexer(consumer) 69 | 70 | companion object { 71 | fun createIndexingLexer(consumer: OccurrenceConsumer) = PestFilterLexer(lexer(), consumer) 72 | } 73 | } 74 | 75 | class PestFilterLexer(originalLexer: Lexer, table: OccurrenceConsumer) : BaseFilterLexer(originalLexer, table) { 76 | override fun advance() { 77 | scanWordsInToken(UsageSearchContext.IN_COMMENTS.toInt(), false, false) 78 | advanceTodoItemCountsInToken() 79 | myDelegate.advance() 80 | } 81 | } 82 | 83 | class PestTodoIndexPatternBuilder : IndexPatternBuilder { 84 | override fun getIndexingLexer(file: PsiFile): Lexer? = if (file is PestFile) lexer() else null 85 | override fun getCommentTokenSet(file: PsiFile): TokenSet? = if (file is PestFile) PestTokenType.COMMENTS else null 86 | override fun getCommentStartDelta(tokenType: IElementType?) = 0 87 | override fun getCommentEndDelta(tokenType: IElementType?) = 0 88 | } 89 | 90 | class PestFindUsagesProvider : FindUsagesProvider { 91 | override fun canFindUsagesFor(element: PsiElement) = element is PsiNameIdentifierOwner 92 | override fun getHelpId(psiElement: PsiElement): String? = null 93 | override fun getType(element: PsiElement) = if (element is PestGrammarRule) PestBundle.message("pest.rule.name") else "" 94 | override fun getDescriptiveName(element: PsiElement) = (element as? PsiNamedElement)?.name ?: "" 95 | override fun getNodeText(element: PsiElement, useFullName: Boolean) = getDescriptiveName(element) 96 | override fun getWordsScanner() = DefaultWordsScanner(lexer(), PestTokenType.IDENTIFIERS, PestTokenType.COMMENTS, PestTokenType.STRINGS) 97 | } 98 | 99 | class PestRuleNameValidator : NamesValidator { 100 | override fun isKeyword(name: String, project: Project?) = "?!@#$%^&*()[]{}<>,./|\\~`'\" \r\n\t".any { it in name } || name in BUILTIN_RULES 101 | override fun isIdentifier(name: String, project: Project?) = !isKeyword(name, project) 102 | } 103 | 104 | class PestRefactoringSupportProvider : RefactoringSupportProvider() { 105 | override fun isMemberInplaceRenameAvailable(element: PsiElement, context: PsiElement?) = true 106 | } 107 | 108 | class PestPairBackspaceHandler : BackspaceHandlerDelegate() { 109 | override fun charDeleted(c: Char, file: PsiFile, editor: Editor) = false 110 | override fun beforeCharDeleted(c: Char, file: PsiFile, editor: Editor) { 111 | if (c !in "\"`'(" || file !is PestFile || !CodeInsightSettings.getInstance().AUTOINSERT_PAIR_BRACKET) return 112 | val offset = editor.caretModel.offset 113 | val highlighter = (editor as EditorEx).highlighter 114 | val iterator = highlighter.createIterator(offset) 115 | if (iterator.tokenType != PestTypes.CLOSING_BRACK 116 | && iterator.tokenType != PestTypes.CLOSING_BRACE 117 | && iterator.tokenType != PestTypes.CLOSING_PAREN) return 118 | iterator.retreat() 119 | if (iterator.tokenType != PestTypes.OPENING_BRACK 120 | && iterator.tokenType != PestTypes.OPENING_BRACE 121 | && iterator.tokenType != PestTypes.OPENING_PAREN) return 122 | 123 | if (offset + 1 > file.textLength) editor.document.deleteString(offset, offset) 124 | else editor.document.deleteString(offset, offset + 1) 125 | } 126 | } 127 | 128 | class PestFoldingBuilder : CustomFoldingBuilder() { 129 | override fun isRegionCollapsedByDefault(node: ASTNode) = node.textLength > 80 130 | override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String = "..." 131 | private fun foldingDescriptor(elem: PsiElement) = FoldingDescriptor(elem.node, elem.textRange, null, PEST_FOLDING_PLACEHOLDER) 132 | override fun buildLanguageFoldRegions(descriptors: MutableList, root: PsiElement, document: Document, quick: Boolean) { 133 | if (root !is PestFile) return 134 | root.rules().mapNotNullTo(descriptors) { it.grammarBody?.let(::foldingDescriptor) } 135 | } 136 | } 137 | 138 | class PestQuoteHandler : SimpleTokenSetQuoteHandler(PestTokenType.ANY_STRINGS) { 139 | override fun hasNonClosedLiteral(editor: Editor, iterator: HighlighterIterator, offset: Int) = iterator.tokenType in PestTokenType.ANY_STRINGS 140 | } 141 | 142 | class PestSpellCheckingStrategy : SpellcheckingStrategy() { 143 | override fun getTokenizer(element: PsiElement): Tokenizer<*> = when (element) { 144 | is PestIdentifier -> { 145 | val parent = element.parent 146 | if (parent is PestGrammarRule && parent.firstChild === element) PsiIdentifierOwnerTokenizer() 147 | else EMPTY_TOKENIZER 148 | } 149 | is PestString, is PestCharacter -> EMPTY_TOKENIZER 150 | else -> super.getTokenizer(element) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /grammar/pest.flex: -------------------------------------------------------------------------------- 1 | package rs.pest.psi; 2 | 3 | import com.intellij.lexer.FlexLexer; 4 | import com.intellij.psi.tree.IElementType; 5 | import static rs.pest.psi.PestTokenType.*; 6 | import static rs.pest.psi.PestTypes.*; 7 | 8 | import static com.intellij.psi.TokenType.BAD_CHARACTER; 9 | import static com.intellij.psi.TokenType.WHITE_SPACE; 10 | 11 | %% 12 | 13 | %{ 14 | public PestLexer() { this((java.io.Reader)null); } 15 | 16 | private int commentStart = 0; 17 | private int commentDepth = 0; 18 | %} 19 | 20 | %public 21 | %class PestLexer 22 | %implements FlexLexer 23 | %function advance 24 | %type IElementType 25 | %unicode 26 | %eof{ return; 27 | %eof} 28 | 29 | %state INSIDE_COMMENT 30 | 31 | WHITE_SPACE=[\ \t\f\r\n] 32 | INTEGER=[0-9]+ 33 | IDENTIFIER=[a-zA-Z_][a-zA-Z_0-9]* 34 | STRING_UNICODE=\\((u\{{HEXDIGIT}{2,6}\})|(x{HEXDIGIT}{2})) 35 | STRING_INCOMPLETE=\"([^\"\\]|(\\[^])|{STRING_UNICODE})* 36 | CHAR_INCOMPLETE='([^\"\\]|(\\[^])|{STRING_UNICODE})? 37 | STRING_LITERAL={STRING_INCOMPLETE}\" 38 | CHAR_LITERAL={CHAR_INCOMPLETE}' 39 | HEXDIGIT=[a-fA-F0-9] 40 | 41 | %% 42 | 43 | { 44 | "/*" { ++commentDepth; } 45 | \*+\/ { 46 | if (--commentDepth <= 0) { 47 | yybegin(YYINITIAL); 48 | zzStartRead = commentStart; 49 | return BLOCK_COMMENT; 50 | } 51 | } 52 | <> { 53 | yybegin(YYINITIAL); 54 | zzStartRead = commentStart; 55 | return BLOCK_COMMENT; 56 | } 57 | \**[^/*]+ { } 58 | \/+[^/*]+ { } 59 | } 60 | 61 | "/*" { yybegin(INSIDE_COMMENT); commentDepth = 1; commentStart = getTokenStart(); } 62 | -?{INTEGER} { return NUMBER; } 63 | - { return MINUS; } 64 | PUSH { return PUSH_TOKEN; } 65 | PEEK { return PEEK_TOKEN; } 66 | PEEK_ALL { return PEEK_ALL_TOKEN; } 67 | POP { return POP_TOKEN; } 68 | POP_ALL { return POP_ALL_TOKEN; } 69 | ANY { return ANY_TOKEN; } 70 | EOI { return EOI_TOKEN; } 71 | SOI { return SOI_TOKEN; } 72 | DROP { return DROP_TOKEN; } 73 | ASCII { return ASCII_TOKEN; } 74 | NEWLINE { return NEWLINE_TOKEN; } 75 | COMMENT { return COMMENT_TOKEN; } 76 | WHITESPACE { return WHITESPACE_TOKEN; } 77 | LETTER { return LETTER_TOKEN; } 78 | CASED_LETTER { return CASED_LETTER_TOKEN; } 79 | UPPERCASE_LETTER { return UPPERCASE_LETTER_TOKEN; } 80 | LOWERCASE_LETTER { return LOWERCASE_LETTER_TOKEN; } 81 | TITLECASE_LETTER { return TITLECASE_LETTER_TOKEN; } 82 | MODIFIER_LETTER { return MODIFIER_LETTER_TOKEN; } 83 | OTHER_LETTER { return OTHER_LETTER_TOKEN; } 84 | MARK { return MARK_TOKEN; } 85 | NONSPACING_MARK { return NONSPACING_MARK_TOKEN; } 86 | SPACING_MARK { return SPACING_MARK_TOKEN; } 87 | ENCLOSING_MARK { return ENCLOSING_MARK_TOKEN; } 88 | NUMBER { return NUMBER_TOKEN; } 89 | DECIMAL_NUMBER { return DECIMAL_NUMBER_TOKEN; } 90 | LETTER_NUMBER { return LETTER_NUMBER_TOKEN; } 91 | OTHER_NUMBER { return OTHER_NUMBER_TOKEN; } 92 | PUNCTUATION { return PUNCTUATION_TOKEN; } 93 | CONNECTOR_PUNCTUATION { return CONNECTOR_PUNCTUATION_TOKEN; } 94 | DASH_PUNCTUATION { return DASH_PUNCTUATION_TOKEN; } 95 | OPEN_PUNCTUATION { return OPEN_PUNCTUATION_TOKEN; } 96 | CLOSE_PUNCTUATION { return CLOSE_PUNCTUATION_TOKEN; } 97 | INITIAL_PUNCTUATION { return INITIAL_PUNCTUATION_TOKEN; } 98 | FINAL_PUNCTUATION { return FINAL_PUNCTUATION_TOKEN; } 99 | OTHER_PUNCTUATION { return OTHER_PUNCTUATION_TOKEN; } 100 | SYMBOL { return SYMBOL_TOKEN; } 101 | MATH_SYMBOL { return MATH_SYMBOL_TOKEN; } 102 | CURRENCY_SYMBOL { return CURRENCY_SYMBOL_TOKEN; } 103 | MODIFIER_SYMBOL { return MODIFIER_SYMBOL_TOKEN; } 104 | OTHER_SYMBOL { return OTHER_SYMBOL_TOKEN; } 105 | SEPARATOR { return SEPARATOR_TOKEN; } 106 | SPACE_SEPARATOR { return SPACE_SEPARATOR_TOKEN; } 107 | LINE_SEPARATOR { return LINE_SEPARATOR_TOKEN; } 108 | PARAGRAPH_SEPARATOR { return PARAGRAPH_SEPARATOR_TOKEN; } 109 | OTHER { return OTHER_TOKEN; } 110 | CONTROL { return CONTROL_TOKEN; } 111 | FORMAT { return FORMAT_TOKEN; } 112 | SURROGATE { return SURROGATE_TOKEN; } 113 | PRIVATE_USE { return PRIVATE_USE_TOKEN; } 114 | UNASSIGNED { return UNASSIGNED_TOKEN; } 115 | ALPHABETIC { return ALPHABETIC_TOKEN; } 116 | BIDI_CONTROL { return BIDI_CONTROL_TOKEN; } 117 | CASE_IGNORABLE { return CASE_IGNORABLE_TOKEN; } 118 | CASED { return CASED_TOKEN; } 119 | CHANGES_WHEN_CASEFOLDED { return CHANGES_WHEN_CASEFOLDED_TOKEN; } 120 | CHANGES_WHEN_CASEMAPPED { return CHANGES_WHEN_CASEMAPPED_TOKEN; } 121 | CHANGES_WHEN_LOWERCASED { return CHANGES_WHEN_LOWERCASED_TOKEN; } 122 | CHANGES_WHEN_TITLECASED { return CHANGES_WHEN_TITLECASED_TOKEN; } 123 | CHANGES_WHEN_UPPERCASED { return CHANGES_WHEN_UPPERCASED_TOKEN; } 124 | DASH { return DASH_TOKEN; } 125 | DEFAULT_IGNORABLE_CODE_POINT { return DEFAULT_IGNORABLE_CODE_POINT_TOKEN; } 126 | DEPRECATED { return DEPRECATED_TOKEN; } 127 | DIACRITIC { return DIACRITIC_TOKEN; } 128 | EXTENDER { return EXTENDER_TOKEN; } 129 | GRAPHEME_BASE { return GRAPHEME_BASE_TOKEN; } 130 | GRAPHEME_EXTEND { return GRAPHEME_EXTEND_TOKEN; } 131 | GRAPHEME_LINK { return GRAPHEME_LINK_TOKEN; } 132 | HEX_DIGIT { return HEX_DIGIT_TOKEN; } 133 | HYPHEN { return HYPHEN_TOKEN; } 134 | IDS_BINARY_OPERATOR { return IDS_BINARY_OPERATOR_TOKEN; } 135 | IDS_TRINARY_OPERATOR { return IDS_TRINARY_OPERATOR_TOKEN; } 136 | ID_CONTINUE { return ID_CONTINUE_TOKEN; } 137 | ID_START { return ID_START_TOKEN; } 138 | IDEOGRAPHIC { return IDEOGRAPHIC_TOKEN; } 139 | JOIN_CONTROL { return JOIN_CONTROL_TOKEN; } 140 | LOGICAL_ORDER_EXCEPTION { return LOGICAL_ORDER_EXCEPTION_TOKEN; } 141 | LOWERCASE { return LOWERCASE_TOKEN; } 142 | MATH { return MATH_TOKEN; } 143 | NONCHARACTER_CODE_POINT { return NONCHARACTER_CODE_POINT_TOKEN; } 144 | OTHER_ALPHABETIC { return OTHER_ALPHABETIC_TOKEN; } 145 | OTHER_DEFAULT_IGNORABLE_CODE_POINT { return OTHER_DEFAULT_IGNORABLE_CODE_POINT_TOKEN; } 146 | OTHER_GRAPHEME_EXTEND { return OTHER_GRAPHEME_EXTEND_TOKEN; } 147 | OTHER_ID_CONTINUE { return OTHER_ID_CONTINUE_TOKEN; } 148 | OTHER_ID_START { return OTHER_ID_START_TOKEN; } 149 | OTHER_LOWERCASE { return OTHER_LOWERCASE_TOKEN; } 150 | OTHER_MATH { return OTHER_MATH_TOKEN; } 151 | OTHER_UPPERCASE { return OTHER_UPPERCASE_TOKEN; } 152 | PATTERN_SYNTAX { return PATTERN_SYNTAX_TOKEN; } 153 | PATTERN_WHITE_SPACE { return PATTERN_WHITE_SPACE_TOKEN; } 154 | PREPENDED_CONCATENATION_MARK { return PREPENDED_CONCATENATION_MARK_TOKEN; } 155 | QUOTATION_MARK { return QUOTATION_MARK_TOKEN; } 156 | RADICAL { return RADICAL_TOKEN; } 157 | REGIONAL_INDICATOR { return REGIONAL_INDICATOR_TOKEN; } 158 | SENTENCE_TERMINAL { return SENTENCE_TERMINAL_TOKEN; } 159 | SOFT_DOTTED { return SOFT_DOTTED_TOKEN; } 160 | TERMINAL_PUNCTUATION { return TERMINAL_PUNCTUATION_TOKEN; } 161 | UNIFIED_IDEOGRAPH { return UNIFIED_IDEOGRAPH_TOKEN; } 162 | UPPERCASE { return UPPERCASE_TOKEN; } 163 | VARIATION_SELECTOR { return VARIATION_SELECTOR_TOKEN; } 164 | WHITE_SPACE { return WHITE_SPACE_TOKEN; } 165 | XID_CONTINUE { return XID_CONTINUE_TOKEN; } 166 | XID_START { return XID_START_TOKEN; } 167 | ASCII_DIGIT { return ASCII_DIGIT_TOKEN; } 168 | ASCII_ALPHA { return ASCII_ALPHA_TOKEN; } 169 | ASCII_ALPHANUMERIC { return ASCII_ALPHANUMERIC_TOKEN; } 170 | ASCII_NONZERO_DIGIT { return ASCII_NONZERO_DIGIT_TOKEN; } 171 | ASCII_BIN_DIGIT { return ASCII_BIN_DIGIT_TOKEN; } 172 | ASCII_OCT_DIGIT { return ASCII_OCT_DIGIT_TOKEN; } 173 | ASCII_HEX_DIGIT { return ASCII_HEX_DIGIT_TOKEN; } 174 | ASCII_ALPHA_UPPER { return ASCII_ALPHA_UPPER_TOKEN; } 175 | ASCII_ALPHA_LOWER { return ASCII_ALPHA_LOWER_TOKEN; } 176 | "_" { return SILENT_MODIFIER; } 177 | {IDENTIFIER} { return IDENTIFIER_TOKEN; } 178 | "//!"[^\r\n]* { return LINE_REGEX_COMMENT; } 179 | "///"[^\r\n]* { return LINE_DOC_COMMENT; } 180 | "//"[^\r\n]* { return LINE_COMMENT; } 181 | "=" { return ASSIGNMENT_OPERATOR; } 182 | "{" { return OPENING_BRACE; } 183 | "}" { return CLOSING_BRACE; } 184 | "(" { return OPENING_PAREN; } 185 | ")" { return CLOSING_PAREN; } 186 | "[" { return OPENING_BRACK; } 187 | "]" { return CLOSING_BRACK; } 188 | "@" { return ATOMIC_MODIFIER; } 189 | "$" { return COMPOUND_ATOMIC_MODIFIER; } 190 | "!" { return NON_ATOMIC_MODIFIER; } 191 | "&" { return POSITIVE_PREDICATE_OPERATOR; } 192 | "~" { return SEQUENCE_OPERATOR; } 193 | "|" { return CHOICE_OPERATOR; } 194 | "?" { return OPTIONAL_OPERATOR; } 195 | "*" { return REPEAT_OPERATOR; } 196 | "+" { return REPEAT_ONCE_OPERATOR; } 197 | "^" { return INSENSITIVE_OPERATOR; } 198 | ".." { return RANGE_OPERATOR; } 199 | "," { return COMMA; } 200 | {STRING_LITERAL} { return STRING_TOKEN; } 201 | {CHAR_LITERAL} { return CHAR_TOKEN; } 202 | {STRING_INCOMPLETE} { return STRING_INCOMPLETE; } 203 | {CHAR_INCOMPLETE} { return CHAR_INCOMPLETE; } 204 | {WHITE_SPACE}+ { return WHITE_SPACE; } 205 | [^] { return BAD_CHARACTER; } 206 | -------------------------------------------------------------------------------- /src/rs/pest/action/introduce.kt: -------------------------------------------------------------------------------- 1 | package rs.pest.action 2 | 3 | import com.intellij.lang.Language 4 | import com.intellij.lang.refactoring.RefactoringSupportProvider 5 | import com.intellij.openapi.actionSystem.DataContext 6 | import com.intellij.openapi.command.WriteCommandAction 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.util.Pass 10 | import com.intellij.openapi.util.TextRange 11 | import com.intellij.psi.* 12 | import com.intellij.psi.util.PsiTreeUtil 13 | import com.intellij.refactoring.IntroduceTargetChooser 14 | import com.intellij.refactoring.RefactoringActionHandler 15 | import com.intellij.refactoring.RefactoringBundle 16 | import com.intellij.refactoring.actions.BasePlatformRefactoringAction 17 | import com.intellij.refactoring.introduce.inplace.OccurrencesChooser 18 | import com.intellij.refactoring.util.CommonRefactoringUtil 19 | import rs.pest.PestBundle 20 | import rs.pest.PestFile 21 | import rs.pest.PestLanguage 22 | import rs.pest.action.ui.PestIntroduceRulePopupImpl 23 | import rs.pest.psi.* 24 | import rs.pest.psi.impl.PestGrammarRuleMixin 25 | import rs.pest.psi.impl.bodyText 26 | import rs.pest.psi.impl.extractSimilar 27 | import rs.pest.psi.impl.findParentExpression 28 | import java.util.* 29 | 30 | 31 | class PestIntroduceRuleAction : BasePlatformRefactoringAction() { 32 | init { 33 | setInjectedContext(true) 34 | } 35 | 36 | override fun isAvailableInEditorOnly() = true 37 | override fun isAvailableForFile(file: PsiFile?) = file is PestFile 38 | override fun isAvailableForLanguage(language: Language?) = language === PestLanguage.INSTANCE 39 | override fun isEnabledOnElements(elements: Occurrences) = false 40 | override fun getRefactoringHandler(provider: RefactoringSupportProvider) = PestIntroduceRuleActionHandler() 41 | } 42 | 43 | typealias Occurrences = Array 44 | 45 | class PestIntroduceRuleActionHandler : RefactoringActionHandler { 46 | override fun invoke(project: Project, editor: Editor, file: PsiFile?, dataContext: DataContext?) { 47 | if (file !is PestFile) return 48 | val selectionModel = editor.selectionModel 49 | val starts = selectionModel.blockSelectionStarts 50 | val ends = selectionModel.blockSelectionEnds 51 | if (starts.isEmpty() || ends.isEmpty()) return 52 | 53 | val startOffset = starts.first() 54 | val endOffset = ends.last() 55 | val currentRule = PsiTreeUtil.getParentOfType(file.findElementAt(startOffset), PestGrammarRuleMixin::class.java) 56 | var parentExpression = if (currentRule != null) findParentExpression(file, startOffset, endOffset) else null 57 | if (currentRule == null || parentExpression == null) { 58 | @Suppress("InvalidBundleOrProperty") 59 | CommonRefactoringUtil.showErrorHint(project, editor, 60 | RefactoringBundle.message("refactoring.introduce.context.error"), 61 | PestBundle.message("pest.actions.general.title.error"), null) 62 | return 63 | } 64 | if (!selectionModel.hasSelection()) { 65 | val expressions = ArrayList() 66 | while (parentExpression != null) { 67 | expressions.add(parentExpression) 68 | parentExpression = PsiTreeUtil.getParentOfType(parentExpression, PestExpression::class.java) 69 | } 70 | if (expressions.size == 1) { 71 | invokeIntroduce(project, editor, file, currentRule, expressions) 72 | } else { 73 | IntroduceTargetChooser.showChooser( 74 | editor, expressions, 75 | object : Pass() { 76 | override fun pass(expression: PestExpression) { 77 | invokeIntroduce(project, editor, file, currentRule, Collections.singletonList(expression)) 78 | } 79 | }, { it.bodyText(it.textLength) }, PestBundle.message("pest.actions.extract.rule.target-chooser.title") 80 | ) 81 | } 82 | } else { 83 | val selectedExpression = findSelectedExpressionsInRange(parentExpression, TextRange(startOffset, endOffset)) 84 | if (selectedExpression.isEmpty()) { 85 | CommonRefactoringUtil.showErrorHint(project, editor, 86 | @Suppress("InvalidBundleOrProperty") 87 | RefactoringBundle.message("refactoring.introduce.selection.error"), 88 | PestBundle.message("pest.actions.general.title.error"), null) 89 | return 90 | } 91 | invokeIntroduce(project, editor, file, currentRule, selectedExpression) 92 | } 93 | } 94 | 95 | private fun findSelectedExpressionsInRange(parentExpression: PestExpression, range: TextRange): List { 96 | if (parentExpression.textRange == range) return listOf(parentExpression) 97 | val list = ArrayList() 98 | var c: PsiElement? = parentExpression.firstChild 99 | while (c != null) { 100 | if (c is PsiWhiteSpace) { 101 | c = c.nextSibling 102 | continue 103 | } 104 | if (c.textRange.intersectsStrict(range)) { 105 | if (c is PestExpression) list.add(c) 106 | else if (c === parentExpression.firstChild || c === parentExpression.lastChild) 107 | return Collections.singletonList(parentExpression) 108 | } 109 | c = c.nextSibling 110 | } 111 | return list 112 | } 113 | 114 | private fun invokeIntroduce(project: Project, editor: Editor, file: PestFile, currentRule: PestGrammarRuleMixin, expressions: List) { 115 | val first = expressions.first() 116 | val last = expressions.last() 117 | val currentRuleName = currentRule.name 118 | var i = 0 119 | var name: String 120 | while (true) { 121 | name = "$currentRuleName$i" 122 | if (file.rules().any { it.name == name }) i++ 123 | else break 124 | } 125 | val range = TextRange(first.startOffset, last.endOffset) 126 | val rule = PestTokenType.createRule("$name = { ${range.shiftLeft(currentRule.startOffset).substring(currentRule.text).trim()} }", project)!! 127 | val expr = rule.grammarBody!!.expression!! 128 | val occurrence = mutableMapOf>() 129 | occurrence[OccurrencesChooser.ReplaceChoice.NO] = listOf(expressions.toTypedArray()) 130 | val allList = SyntaxTraverser.psiTraverser() 131 | .withRoot(file) 132 | .filterIsInstance() 133 | .mapNotNull { extractSimilar(expr, it) } 134 | .toList() 135 | if (allList.size > 1) 136 | occurrence[OccurrencesChooser.ReplaceChoice.ALL] = allList 137 | object : OccurrencesChooser(editor) { 138 | override fun getOccurrenceRange(occurrence: Occurrences) = 139 | TextRange(occurrence.first().startOffset, occurrence.last().endOffset) 140 | }.showChooser(object : Pass() { 141 | override fun pass(choice: OccurrencesChooser.ReplaceChoice?) = WriteCommandAction.runWriteCommandAction(project) { 142 | @Suppress("NAME_SHADOWING") 143 | val rule = file.addAfter(rule, currentRule.nextSibling) as PestGrammarRule 144 | val document = editor.document 145 | PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document) 146 | val all = OccurrencesChooser.ReplaceChoice.ALL 147 | if (choice === all) occurrence[all]?.let { exprToReplace -> 148 | replaceUsages(exprToReplace) 149 | } else occurrence[OccurrencesChooser.ReplaceChoice.NO]?.let { exprToReplace -> 150 | replaceUsages(exprToReplace) 151 | } 152 | val newExpr = PestTokenType.createExpression(name, project)!! 153 | if (expressions.size == 1) { 154 | expressions.first().replace(newExpr) 155 | } else { 156 | val firstExpr = expressions.first() 157 | val parent = firstExpr.parent 158 | parent.addBefore(newExpr, firstExpr) 159 | parent.deleteChildRange(firstExpr, expressions.last()) 160 | } 161 | val newRuleStartOffset = currentRule.endOffset + 1 162 | val popup = PestIntroduceRulePopupImpl(newRuleStartOffset, rule, editor, project, expr) 163 | editor.caretModel.moveToOffset(newRuleStartOffset) 164 | PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document) 165 | var lastOffset = newRuleStartOffset + rule.textLength 166 | val fullLength = document.textLength 167 | if (lastOffset > fullLength) lastOffset = fullLength 168 | document.insertString(lastOffset, "\n") 169 | PsiDocumentManager.getInstance(project).commitDocument(document) 170 | popup.performInplaceRefactoring(null) 171 | } 172 | 173 | private fun replaceUsages(expressions: List) { 174 | val newExpr = PestTokenType.createExpression(name, project)!! 175 | if (expressions.size == 1) { 176 | expressions.first().first().replace(newExpr) 177 | } else expressions.forEach { 178 | val firstExpr = it.first() 179 | val parent = firstExpr.parent 180 | parent.addBefore(newExpr, firstExpr) 181 | parent.deleteChildRange(firstExpr, it.last()) 182 | } 183 | } 184 | }, occurrence) 185 | } 186 | 187 | // Unsupported 188 | override fun invoke(project: Project, elements: Occurrences, dataContext: DataContext?) = Unit 189 | } 190 | -------------------------------------------------------------------------------- /rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | # Pest IDE 3 | 4 | This is a bridge library for the [IntelliJ IDEA plugin for Pest][jb]. 5 | 6 | [jb]: https://plugins.jetbrains.com/plugin/12046-pest 7 | [asmble]:https://github.com/cretz/asmble 8 | 9 | It's supposed to be compiled only with the wasm32 backend of nightly rustc 10 | (at least at this moment). 11 | 12 | After compiling as wasm, it's translated to JVM bytecode with [asmble][asmble] and then 13 | loaded in the plugin. 14 | Thus no JNI. 15 | */ 16 | 17 | use std::alloc::System; 18 | use std::ffi::CString; 19 | use std::{mem, str}; 20 | 21 | use pest::error::{Error, ErrorVariant, InputLocation}; 22 | use pest::iterators::Pair; 23 | use pest_meta::parser::{self, Rule}; 24 | use pest_meta::{optimizer, validator}; 25 | use pest_vm::Vm; 26 | 27 | use self::misc::JavaStr; 28 | 29 | /// Allocation library for Java use. 30 | /// 31 | /// On Java side, it can create a Rust string based on the codes in this module. 32 | pub mod str4j; 33 | 34 | /// Everything that are not related to pest. 35 | pub mod misc; 36 | 37 | #[global_allocator] 38 | static GLOBAL_ALLOCATOR: System = System; 39 | static mut VM: Option = None; 40 | 41 | /// From position to line-column pair. 42 | fn line_col(pos: usize, input: &str) -> (usize, usize) { 43 | let mut pos = pos; 44 | // Position's pos is always a UTF-8 border. 45 | let slice = &input[..pos]; 46 | let mut chars = slice.chars().peekable(); 47 | 48 | let mut line_col = (1, 1); 49 | 50 | while pos != 0 { 51 | match chars.next() { 52 | Some('\r') => { 53 | if let Some(&'\n') = chars.peek() { 54 | chars.next(); 55 | 56 | if pos == 1 { 57 | pos -= 1; 58 | } else { 59 | pos -= 2; 60 | } 61 | 62 | line_col = (line_col.0 + 1, 1); 63 | } else { 64 | pos -= 1; 65 | line_col = (line_col.0, line_col.1 + 1); 66 | } 67 | } 68 | Some('\n') => { 69 | pos -= 1; 70 | line_col = (line_col.0 + 1, 1); 71 | } 72 | Some(c) => { 73 | pos -= c.len_utf8(); 74 | line_col = (line_col.0, line_col.1 + 1); 75 | } 76 | None => unreachable!(), 77 | } 78 | } 79 | 80 | line_col 81 | } 82 | 83 | /// Convert the error to a readable format. 84 | fn convert_error(error: Error, grammar: &str) -> String { 85 | let message = match error.variant { 86 | ErrorVariant::CustomError { message } => message, 87 | _ => unreachable!(), 88 | }; 89 | let ((start_line, start_col), (end_line, end_col)) = match error.location { 90 | InputLocation::Pos(pos) => (line_col(pos, grammar), line_col(pos, grammar)), 91 | InputLocation::Span((start, end)) => (line_col(start, grammar), line_col(end, grammar)), 92 | }; 93 | format!( 94 | "{:?}^{:?}^{:?}^{:?}^{}", 95 | start_line, start_col, end_line, end_col, message 96 | ) 97 | } 98 | 99 | #[unsafe(no_mangle)] 100 | /// Load the Pest VM as a global variable. 101 | pub extern "C" fn load_vm(pest_code: JavaStr, pest_code_len: i32) -> JavaStr { 102 | let pest_code_len = pest_code_len as usize; 103 | let pest_code_bytes = 104 | unsafe { Vec::::from_raw_parts(pest_code, pest_code_len, pest_code_len) }; 105 | let pest_code = str::from_utf8(&pest_code_bytes).unwrap(); 106 | let pest_code_result = parser::parse(Rule::grammar_rules, pest_code).map_err(|error| { 107 | error.renamed_rules(|rule| match *rule { 108 | Rule::grammar_rule => "rule".to_owned(), 109 | Rule::_push => "push".to_owned(), 110 | Rule::assignment_operator => "`=`".to_owned(), 111 | Rule::silent_modifier => "`_`".to_owned(), 112 | Rule::atomic_modifier => "`@`".to_owned(), 113 | Rule::compound_atomic_modifier => "`$`".to_owned(), 114 | Rule::non_atomic_modifier => "`!`".to_owned(), 115 | Rule::opening_brace => "`{`".to_owned(), 116 | Rule::closing_brace => "`}`".to_owned(), 117 | Rule::opening_paren => "`(`".to_owned(), 118 | Rule::positive_predicate_operator => "`&`".to_owned(), 119 | Rule::negative_predicate_operator => "`!`".to_owned(), 120 | Rule::sequence_operator => "`&`".to_owned(), 121 | Rule::choice_operator => "`|`".to_owned(), 122 | Rule::optional_operator => "`?`".to_owned(), 123 | Rule::repeat_operator => "`*`".to_owned(), 124 | Rule::repeat_once_operator => "`+`".to_owned(), 125 | Rule::comma => "`,`".to_owned(), 126 | Rule::closing_paren => "`)`".to_owned(), 127 | Rule::quote => "`\"`".to_owned(), 128 | Rule::insensitive_string => "`^`".to_owned(), 129 | Rule::range_operator => "`..`".to_owned(), 130 | Rule::single_quote => "`'`".to_owned(), 131 | other_rule => format!("{:?}", other_rule), 132 | }) 133 | }); 134 | let pairs = match pest_code_result { 135 | Ok(pairs) => pairs, 136 | Err(err) => { 137 | let cstr = CString::new(format!("Err[{:?}]", convert_error(err, &pest_code))).unwrap(); 138 | let ptr = cstr.as_ptr() as *mut _; 139 | mem::forget(pest_code_bytes); 140 | mem::forget(cstr); 141 | return ptr; 142 | } 143 | }; 144 | 145 | if let Err(errors) = validator::validate_pairs(pairs.clone()) { 146 | let cstr = CString::new(format!( 147 | "Err{:?}", 148 | errors 149 | .into_iter() 150 | .map(|e| convert_error(e, &pest_code)) 151 | .collect::>() 152 | )) 153 | .unwrap(); 154 | let ptr = cstr.as_ptr() as *mut _; 155 | mem::forget(pest_code_bytes); 156 | mem::forget(cstr); 157 | return ptr; 158 | } 159 | 160 | let ast = match parser::consume_rules(pairs) { 161 | Ok(ast) => ast, 162 | Err(errors) => { 163 | let cstr = CString::new(format!( 164 | "Err{:?}", 165 | errors 166 | .into_iter() 167 | .map(|e| convert_error(e, &pest_code)) 168 | .collect::>() 169 | )) 170 | .unwrap(); 171 | let ptr = cstr.as_ptr() as *mut _; 172 | mem::forget(pest_code_bytes); 173 | mem::forget(cstr); 174 | return ptr; 175 | } 176 | }; 177 | 178 | let rules: Vec<_> = ast.iter().map(|rule| rule.name.clone()).collect(); 179 | unsafe { 180 | VM = Some(Vm::new(optimizer::optimize(ast))); 181 | } 182 | 183 | let cstr = CString::new(format!("{:?}", rules)).unwrap(); 184 | let ptr = cstr.as_ptr() as *mut _; 185 | mem::forget(pest_code_bytes); 186 | mem::forget(cstr); 187 | ptr 188 | } 189 | 190 | /// Convert the pair information to a string that the plugin 191 | /// understands. 192 | fn join_pairs(result: &mut Vec, pair: Pair<&str>) { 193 | let span = pair.as_span(); 194 | let start = span.start(); 195 | let end = span.end(); 196 | result.push(format!("{:?}^{:?}^{}", start, end, pair.as_rule())); 197 | for child in pair.into_inner() { 198 | join_pairs(result, child); 199 | } 200 | } 201 | 202 | #[unsafe(no_mangle)] 203 | #[allow(static_mut_refs)] 204 | /// After loading the VM, this function can parse the code with the 205 | /// currently loaded VM. 206 | /// Assumes the VM is already loaded, otherwise it'll panic. 207 | pub extern "C" fn render_code( 208 | rule_name: JavaStr, 209 | rule_name_len: i32, 210 | user_code: JavaStr, 211 | user_code_len: i32, 212 | ) -> JavaStr { 213 | let vm = unsafe { VM.as_ref().unwrap() }; 214 | let rule_name_len = rule_name_len as usize; 215 | let rule_name_bytes = 216 | unsafe { Vec::::from_raw_parts(rule_name, rule_name_len, rule_name_len) }; 217 | let rule_name = str::from_utf8(&rule_name_bytes).unwrap(); 218 | let user_code_len = user_code_len as usize; 219 | let user_code_bytes = 220 | unsafe { Vec::::from_raw_parts(user_code, user_code_len, user_code_len) }; 221 | let user_code = str::from_utf8(&user_code_bytes).unwrap(); 222 | let cstr = CString::new(match vm.parse(rule_name, user_code) { 223 | Ok(pairs) => { 224 | let mut result = vec![]; 225 | for pair in pairs { 226 | join_pairs(&mut result, pair); 227 | } 228 | format!("{:?}", result) 229 | } 230 | Err(err) => format!("Err{}", err.renamed_rules(|r| r.to_string())), 231 | }) 232 | .unwrap(); 233 | mem::forget(rule_name_bytes); 234 | mem::forget(user_code_bytes); 235 | let ptr = cstr.as_ptr() as *mut _; 236 | mem::forget(cstr); 237 | ptr 238 | } 239 | --------------------------------------------------------------------------------