├── .DS_Store ├── .gitignore ├── .vscode └── launch.json ├── README.md ├── color_theme_compatibility ├── dark_defaults.json ├── dark_plus.json ├── dark_vs.json └── display.mjs ├── doc ├── code_1.png ├── code_1.xi ├── code_2.png ├── code_2.xi ├── code_3.png ├── code_3.xi ├── heading.xi ├── heading_1.png ├── highlight.png ├── highlight.xi ├── link3.xi ├── link_1.png ├── link_1.xi ├── link_2.png ├── link_3.png ├── list.png ├── list.xi ├── md_to_xi_dst.png ├── md_to_xi_dst.xi ├── md_to_xi_src.md ├── md_to_xi_src.png ├── meta_heading.png ├── named_param_1.png ├── named_param_1.xi ├── named_param_2.png ├── named_param_2.xi ├── paragraph_1.png ├── paragraph_1.xi └── xi_illustration.png ├── esbuild.mjs ├── examples ├── lua_syntax.xi └── neurotransmitter.xi ├── icon.png ├── icon.svg ├── language-configuration.json ├── out └── .gitignore ├── package-lock.json ├── package.json ├── src ├── extension.mjs ├── get_editor_change_handler.mjs ├── get_fold_provider.mjs ├── get_go_back_cmd.mjs ├── get_jump_to_anchor.mjs ├── get_link_provider.mjs ├── get_lookup_cmd.mjs ├── get_open_cmd.mjs ├── icon.afdesign ├── tools.mjs ├── xi-markdown-injection.tmLanguage.json ├── xi.tmLanguage.json └── xi_parser.mjs └── test ├── editor_change_handler.mjs ├── fold_provider.mjs ├── go_back_cmd.mjs ├── jump_test.xi ├── jump_to_anchor.mjs ├── link_provider.mjs ├── open_cmd.mjs ├── test.md ├── test.xi └── xi_parser.mjs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [{ 3 | "name": "Debug extension", 4 | "type": "extensionHost", 5 | "request": "launch", 6 | "args": [ 7 | "--extensionDevelopmentPath=${workspaceFolder}" 8 | ] 9 | }, { 10 | "name": "Debug test(s)", 11 | "type": "node", 12 | "request": "launch", 13 | "runtimeExecutable": "npm", 14 | "runtimeArgs": ["exec", "mocha"] 15 | }] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xi personal wiki language 2 | 3 | "Xi" is a markdown-like language designed for a personal knowledge base. 4 | Color and indentation are used to highlight headings, paragraphs, links, 5 | source code examples and definitions. You can read and write Xi from 6 | within a VSCode without a need to "render" it into a "visual" web page 7 | or PDF. This makes Xi a very **fast** tool to keep records: you grep 8 | someting, skim through Xi pages, add or change some text from within a same 9 | editor, without a need to switch between 'edit' and 'previes' modes. An 10 | example of Xi page from my personal knowledge base with 11 | [Memory](https://marketplace.visualstudio.com/items?itemName=grigoryvp.memory-theme) 12 | color theme: 13 | 14 | 15 | 16 | 17 | ## VSCode extension installation guide 18 | 19 | This extension is published to the "Extension Marketplace". To install it, 20 | click on the "Extensions" icon in the Activity Bar on the side of VSCode 21 | and enter "Xi markup language" in the search field. Click on the "install" 22 | button near the extension listed in the search results. 23 | 24 | ## General 25 | 26 | The example below shows a simple markdown syntax with nested headings and 27 | paragraphs. With Xi, heading is marked with indentation and tail space-dot 28 | instead of indentation. So, heading 2 in markdown that is '## Heading 2' 29 | becomes ' Heading 2 .' in Xi (tail space-dot defines that it's a heading 30 | and leading two spaces defines second level. Third level will be 4 spaces 31 | and up to 10 spaces for a maximum nesting level 5). Paragraphs are marked 32 | with dot-space, effectively saving one empty line used in markdown: 33 | 34 | 35 | 36 | 37 | The extension provides a single command **extension.xi.lookup** that is binded 38 | to the **ctrl+k x** by default. This command brings file search for the 39 | directory with xi files, which is **~/.xi** by default and can be changed 40 | via the **xi.lookupPath** configuration option. It assumes workflow where if 41 | you are in any VSCode project and want to consult your personal knowledge 42 | base or add something to it, you hit "ctrl+k x", enter part of file name 43 | and open corresponding file for read and update. For example, writing some 44 | Python source code you want to remind yourself about API nuances. You 45 | know, that you store programming languages API records in files named 46 | "language_api.xi", so you hit "ctrl+k x", enter "python_a" that narrows 47 | search to "python_api.xi", hit enter, open file, check your notes and 48 | return to your code. 49 | 50 | Wikiwords and links are marked as VSCode "links" and can be opened via 51 | standard VSCode means, for example the **editor.action.openLink** keybinding. 52 | 53 | ## Paragraph 54 | 55 | Simplest building block of a knowledge base, first paragraph line is prefixed 56 | with dot-and-space, while each next line is prefixed with two spaces. 57 | Paragraph itself can be indented with increments of two spaces in order to 58 | nest under different heading or other paragraphs. Compared to markdown 59 | notion of separating paragraphs with empty line, such layout saves a lot 60 | of vertical space, which is vital for documenting software-related things 61 | like programming lanugae syntax, APIs, frameworks etc. We tend to have 62 | short paragraphs that do not tend to follow each other, but tend to form 63 | a really complex nested strucure with lots of headings, meta information, 64 | code samples etc. Such paragrapah layout, used alongside colored headings and 65 | proper indentation, proved it's worth during my 10 years of using Xi as a 66 | personal knowledge base. 67 | 68 | 69 | 70 | ## Heading 71 | 72 | Alongside paragraph, heading is a second most used building block for a 73 | knowledge base. Heading type is denoted by space-dot suffix, while heading 74 | nesting level is denoted by increments of two spaces. In most cases headings 75 | are combined with paragraphs in order to create hierarchical structures 76 | that are easy to read **and modify**. 77 | 78 | 79 | 80 | ## Links 81 | 82 | Here Xi starts to differenciate from Markdown. Simple "wikiword" link looks 83 | like one in Markdown, `[foo]`, but clicking on such link will try to open 84 | corresponding file, so everything works locally within VSCode. File name 85 | is created by lowercasing text between square braces, replacing spaces with 86 | underscores and appending `.xi` suffix. So clicking on the `[foo]` wikiword 87 | will instruct VSCode to open `foo.xi` in same folder, or ask to create one 88 | if it does not exist. 89 | 90 | 91 | ![wikiword link](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/link_1.png) 92 | 93 | Wikiword links with anchors extend this concept by allowing to add heading 94 | name after `#`. Clicking on the like like `[js api#search]` will try to 95 | open `js_api.xi` file and scroll to the first `search` hading (that starts 96 | with spaces and ends with space-dot). Nested anchors are supported by 97 | chaining multiple '#' like `[js api#* String#- search]`. 98 | 99 | Header links transforms a header into link by adding `[] .` instead of 100 | space-and-dot. Mostly useful while describing APIs or frameworks with 101 | complex tree structures where some headers points to articles of their own. 102 | Header can also start and/or end with wikiword links. 103 | 104 | 105 | ![wikiword link](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/link_2.png) 106 | 107 | Anchor links starts with `#` and jumps within same document (mostly used for 108 | "Reference" section at bottom). Anchor target is specified by placing 109 | `#` as a last symbol of a wikiword link, so clicking on the link `[#1]` 110 | will jump into a line with `[1#]` anchor. 111 | 112 | 113 | ![wikiword link](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/link_3.png) 114 | 115 | ## Text highlight 116 | 117 | Xi supports a number of ways to highligh important parts of text. Pipes are 118 | used to highlight "special text" which are terms, API method names etc. 119 | Backticks are used to highlight something important with bright color. Please 120 | note that if [memory color theme](https://marketplace.visualstudio.com/items?itemName=grigoryvp.memory-theme) 121 | is used, highlight syntax is dimmed: 122 | 123 | 124 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/highlight.png) 125 | 126 | ## Code samples 127 | 128 | Xi was created with the main goal to keep all my notes about different 129 | programming languages, framework and APIs I use. It is tailored to 130 | represent code samples in multiple ways. Most common is a "block code 131 | sample" which is a paragraph (dot-space prefix) that starts with a pipe 132 | character, followed by optional meta information inside curly braces. Meta 133 | syntax is a list of key-value pairs, a key separated from a value with the 134 | colon character and pairs separated from each other with the semicolon 135 | character. I use the 'lng' key to denote a programming language type, with 136 | a file extension as a value. So the block code sample for a Python 137 | programming language will start with `. |{lng:py}`, which will be dimmed 138 | out by a syntax highlighter. Each line of the sample is prefixed with a 139 | pipe, indented to match the position of the pipe in the `|{lng:py}`. 140 | 141 | 142 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/code_1.png) 143 | 144 | Code sample can be a paragraph of it's own or appear as a part of paragraph 145 | with text: 146 | 147 | 148 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/code_2.png) 149 | 150 | Code sample can also be inlined into a paragraph text by starting code 151 | right after `{}` meta inormation and terminating code with a pipe. for 152 | multiline code paragraph the optional meta information line should not 153 | contain any symbols after the meta: 154 | 155 | 156 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/code_3.png) 157 | 158 | ## Named parameters 159 | 160 | This Xi feature is more specialized for programming languages. While 161 | taking notes on APIs it's often required to reference method parameters 162 | inside of a method description. While this can be done via the text highlight 163 | feature, doing so will not distinguish parameters among other highlighted 164 | words. Specialized syntax allows to include named parameter between curled 165 | braces. In addition, char-space prefix can be added after opening curled 166 | brace to mark input or output parameters. It's also possible to include 167 | parameter type wihin round braces after a mark character: all with syntax 168 | highlighting for a visual navigation: 169 | 170 | 171 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/named_param_1.png) 172 | 173 | Usage example that describes an "insert" method that takes two named and 174 | positional parameters, "indice" and "item". 175 | 176 | 177 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/named_param_2.png) 178 | 179 | ## Lists 180 | 181 | Like in Markdown, lists in Xi are denoted with `#`, `*` or `-` characters. 182 | The syntax highlighting behaviour is the same as with paragraphs (that start 183 | with the `.` character). 184 | 185 | 186 | ![text highlight](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/list.png) 187 | 188 | ## Meta heading 189 | 190 | Sometimes a document contains some headers that denote meta information 191 | about the document. Two common examples of such headers are document name 192 | at top and the "reference" section at bottom. For them a special coloring 193 | is provided if they are terminated with the "@" character instead of the dot: 194 | 195 | 196 | ![meta heading](https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/master/doc/meta_heading.png) 197 | 198 | ## Examples 199 | 200 | * [Lua syntax](https://github.com/grigoryvp/vscode-language-xi/tree/master/examples/lua_syntax.xi) 201 | * [Neurophysiology article](https://github.com/grigoryvp/vscode-language-xi/tree/master/examples/neurotransmitter.xi) 202 | 203 | ## Todo 204 | 205 | * Re-take all screenshots. 206 | * Outline API support. 207 | 208 | ## License 209 | 210 | The following licensing applies to Xi personal wiki language: 211 | Attribution-NonCommercial-NoDerivatives 4.0 International 212 | (CC-BY-NC-ND-4.0). For more information go to 213 | [https://creativecommons.org/licenses/by-nc-nd/4.0/](https://creativecommons.org/licenses/by-nc-nd/4.0/) 214 | -------------------------------------------------------------------------------- /color_theme_compatibility/dark_defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vscode://schemas/color-theme", 3 | "name": "Dark Default Colors", 4 | "colors": { 5 | "editor.background": "#1E1E1E", 6 | "editor.foreground": "#D4D4D4", 7 | "editor.inactiveSelectionBackground": "#3A3D41", 8 | "editorIndentGuide.background": "#404040", 9 | "editorIndentGuide.activeBackground": "#707070", 10 | "editor.selectionHighlightBackground": "#ADD6FF26", 11 | "list.dropBackground": "#383B3D", 12 | "activityBarBadge.background": "#007ACC", 13 | "sideBarTitle.foreground": "#BBBBBB", 14 | "input.placeholderForeground": "#A6A6A6", 15 | "settings.textInputBackground": "#292929", 16 | "settings.numberInputBackground": "#292929" 17 | } 18 | } -------------------------------------------------------------------------------- /color_theme_compatibility/dark_plus.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vscode://schemas/color-theme", 3 | "name": "Dark+ (default dark)", 4 | "include": "./dark_vs.json", 5 | "tokenColors": [ 6 | { 7 | "name": "Function declarations", 8 | "scope": [ 9 | "entity.name.function", 10 | "support.function", 11 | "support.constant.handlebars" 12 | ], 13 | "settings": { 14 | "foreground": "#DCDCAA" 15 | } 16 | }, 17 | { 18 | "name": "Types declaration and references", 19 | "scope": [ 20 | "meta.return-type", 21 | "support.class", 22 | "support.type", 23 | "entity.name.type", 24 | "entity.name.class", 25 | "storage.type.numeric.go", 26 | "storage.type.byte.go", 27 | "storage.type.boolean.go", 28 | "storage.type.string.go", 29 | "storage.type.uintptr.go", 30 | "storage.type.error.go", 31 | "storage.type.rune.go", 32 | "storage.type.cs", 33 | "storage.type.generic.cs", 34 | "storage.type.modifier.cs", 35 | "storage.type.variable.cs", 36 | "storage.type.annotation.java", 37 | "storage.type.generic.java", 38 | "storage.type.java", 39 | "storage.type.object.array.java", 40 | "storage.type.primitive.array.java", 41 | "storage.type.primitive.java", 42 | "storage.type.token.java", 43 | "storage.type.groovy", 44 | "storage.type.annotation.groovy", 45 | "storage.type.parameters.groovy", 46 | "storage.type.generic.groovy", 47 | "storage.type.object.array.groovy", 48 | "storage.type.primitive.array.groovy", 49 | "storage.type.primitive.groovy" 50 | ], 51 | "settings": { 52 | "foreground": "#4EC9B0" 53 | } 54 | }, 55 | { 56 | "name": "Types declaration and references, TS grammar specific", 57 | "scope": [ 58 | "meta.type.cast.expr", 59 | "meta.type.new.expr", 60 | "support.constant.math", 61 | "support.constant.dom", 62 | "support.constant.json", 63 | "entity.other.inherited-class" 64 | ], 65 | "settings": { 66 | "foreground": "#4EC9B0" 67 | } 68 | }, 69 | { 70 | "name": "Control flow keywords", 71 | "scope": "keyword.control", 72 | "settings": { 73 | "foreground": "#C586C0" 74 | } 75 | }, 76 | { 77 | "name": "Variable and parameter name", 78 | "scope": [ 79 | "variable", 80 | "meta.definition.variable.name", 81 | "support.variable", 82 | "entity.name.variable" 83 | ], 84 | "settings": { 85 | "foreground": "#9CDCFE" 86 | } 87 | }, 88 | { 89 | "name": "Object keys, TS grammar specific", 90 | "scope": [ 91 | "meta.object-literal.key" 92 | ], 93 | "settings": { 94 | "foreground": "#9CDCFE" 95 | } 96 | }, 97 | { 98 | "name": "CSS property value", 99 | "scope": [ 100 | "support.constant.property-value", 101 | "support.constant.font-name", 102 | "support.constant.media-type", 103 | "support.constant.media", 104 | "constant.other.color.rgb-value", 105 | "constant.other.rgb-value", 106 | "support.constant.color" 107 | ], 108 | "settings": { 109 | "foreground": "#CE9178" 110 | } 111 | }, 112 | { 113 | "name": "Regular expression groups", 114 | "scope": [ 115 | "punctuation.definition.group.regexp", 116 | "punctuation.definition.group.assertion.regexp", 117 | "punctuation.definition.character-class.regexp", 118 | "punctuation.character.set.begin.regexp", 119 | "punctuation.character.set.end.regexp", 120 | "keyword.operator.negation.regexp", 121 | "support.other.parenthesis.regexp" 122 | ], 123 | "settings": { 124 | "foreground": "#CE9178" 125 | } 126 | }, 127 | { 128 | "scope": [ 129 | "constant.character.character-class.regexp", 130 | "constant.other.character-class.set.regexp", 131 | "constant.other.character-class.regexp", 132 | "constant.character.set.regexp" 133 | ], 134 | "settings": { 135 | "foreground": "#d16969" 136 | } 137 | }, 138 | { 139 | "scope": [ 140 | "keyword.operator.or.regexp", 141 | "keyword.control.anchor.regexp" 142 | ], 143 | "settings": { 144 | "foreground": "#DCDCAA" 145 | } 146 | }, 147 | { 148 | "scope": "keyword.operator.quantifier.regexp", 149 | "settings": { 150 | "foreground": "#d7ba7d" 151 | } 152 | }, 153 | { 154 | "scope": "constant.character", 155 | "settings": { 156 | "foreground": "#569cd6" 157 | } 158 | }, 159 | { 160 | "scope": "constant.character.escape", 161 | "settings": { 162 | "foreground": "#d7ba7d" 163 | } 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /color_theme_compatibility/dark_vs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "vscode://schemas/color-theme", 3 | "name": "Dark (Visual Studio)", 4 | "include": "./dark_defaults.json", 5 | "tokenColors": [ 6 | { 7 | "scope": [ 8 | "meta.embedded", 9 | "source.groovy.embedded" 10 | ], 11 | "settings": { 12 | "foreground": "#D4D4D4" 13 | } 14 | }, 15 | { 16 | "scope": "emphasis", 17 | "settings": { 18 | "fontStyle": "italic" 19 | } 20 | }, 21 | { 22 | "scope": "strong", 23 | "settings": { 24 | "fontStyle": "bold" 25 | } 26 | }, 27 | { 28 | "scope": "header", 29 | "settings": { 30 | "foreground": "#000080" 31 | } 32 | }, 33 | { 34 | "scope": "comment", 35 | "settings": { 36 | "foreground": "#6A9955" 37 | } 38 | }, 39 | { 40 | "scope": "constant.language", 41 | "settings": { 42 | "foreground": "#569cd6" 43 | } 44 | }, 45 | { 46 | "scope": [ 47 | "constant.numeric" 48 | ], 49 | "settings": { 50 | "foreground": "#b5cea8" 51 | } 52 | }, 53 | { 54 | "scope": "constant.regexp", 55 | "settings": { 56 | "foreground": "#646695" 57 | } 58 | }, 59 | { 60 | "scope": "entity.name.tag", 61 | "settings": { 62 | "foreground": "#569cd6" 63 | } 64 | }, 65 | { 66 | "scope": "entity.name.tag.css", 67 | "settings": { 68 | "foreground": "#d7ba7d" 69 | } 70 | }, 71 | { 72 | "scope": "entity.other.attribute-name", 73 | "settings": { 74 | "foreground": "#9cdcfe" 75 | } 76 | }, 77 | { 78 | "scope": [ 79 | "entity.other.attribute-name.class.css", 80 | "entity.other.attribute-name.class.mixin.css", 81 | "entity.other.attribute-name.id.css", 82 | "entity.other.attribute-name.parent-selector.css", 83 | "entity.other.attribute-name.pseudo-class.css", 84 | "entity.other.attribute-name.pseudo-element.css", 85 | "source.css.less entity.other.attribute-name.id", 86 | "entity.other.attribute-name.attribute.scss", 87 | "entity.other.attribute-name.scss" 88 | ], 89 | "settings": { 90 | "foreground": "#d7ba7d" 91 | } 92 | }, 93 | { 94 | "scope": "invalid", 95 | "settings": { 96 | "foreground": "#f44747" 97 | } 98 | }, 99 | { 100 | "scope": "markup.underline", 101 | "settings": { 102 | "fontStyle": "underline" 103 | } 104 | }, 105 | { 106 | "scope": "markup.bold", 107 | "settings": { 108 | "fontStyle": "bold", 109 | "foreground": "#569cd6" 110 | } 111 | }, 112 | { 113 | "scope": "markup.heading", 114 | "settings": { 115 | "fontStyle": "bold", 116 | "foreground": "#569cd6" 117 | } 118 | }, 119 | { 120 | "scope": "markup.italic", 121 | "settings": { 122 | "fontStyle": "italic" 123 | } 124 | }, 125 | { 126 | "scope": "markup.inserted", 127 | "settings": { 128 | "foreground": "#b5cea8" 129 | } 130 | }, 131 | { 132 | "scope": "markup.deleted", 133 | "settings": { 134 | "foreground": "#ce9178" 135 | } 136 | }, 137 | { 138 | "scope": "markup.changed", 139 | "settings": { 140 | "foreground": "#569cd6" 141 | } 142 | }, 143 | { 144 | "scope": "beginning.punctuation.definition.quote.markdown", 145 | "settings": { 146 | "foreground": "#6A9955" 147 | } 148 | }, 149 | { 150 | "scope": "beginning.punctuation.definition.list.markdown", 151 | "settings": { 152 | "foreground": "#6796e6" 153 | } 154 | }, 155 | { 156 | "scope": "markup.inline.raw", 157 | "settings": { 158 | "foreground": "#ce9178" 159 | } 160 | }, 161 | { 162 | "scope": "meta.selector", 163 | "settings": { 164 | "foreground": "#d7ba7d" 165 | } 166 | }, 167 | { 168 | "name": "brackets of XML/HTML tags", 169 | "scope": "punctuation.definition.tag", 170 | "settings": { 171 | "foreground": "#808080" 172 | } 173 | }, 174 | { 175 | "scope": "meta.preprocessor", 176 | "settings": { 177 | "foreground": "#569cd6" 178 | } 179 | }, 180 | { 181 | "scope": "meta.preprocessor.string", 182 | "settings": { 183 | "foreground": "#ce9178" 184 | } 185 | }, 186 | { 187 | "scope": "meta.preprocessor.numeric", 188 | "settings": { 189 | "foreground": "#b5cea8" 190 | } 191 | }, 192 | { 193 | "scope": "meta.structure.dictionary.key.python", 194 | "settings": { 195 | "foreground": "#9cdcfe" 196 | } 197 | }, 198 | { 199 | "scope": "meta.diff.header", 200 | "settings": { 201 | "foreground": "#569cd6" 202 | } 203 | }, 204 | { 205 | "scope": "storage", 206 | "settings": { 207 | "foreground": "#569cd6" 208 | } 209 | }, 210 | { 211 | "scope": "storage.type", 212 | "settings": { 213 | "foreground": "#569cd6" 214 | } 215 | }, 216 | { 217 | "scope": "storage.modifier", 218 | "settings": { 219 | "foreground": "#569cd6" 220 | } 221 | }, 222 | { 223 | "scope": "string", 224 | "settings": { 225 | "foreground": "#ce9178" 226 | } 227 | }, 228 | { 229 | "scope": "string.tag", 230 | "settings": { 231 | "foreground": "#ce9178" 232 | } 233 | }, 234 | { 235 | "scope": "string.value", 236 | "settings": { 237 | "foreground": "#ce9178" 238 | } 239 | }, 240 | { 241 | "scope": "string.regexp", 242 | "settings": { 243 | "foreground": "#d16969" 244 | } 245 | }, 246 | { 247 | "name": "String interpolation", 248 | "scope": [ 249 | "punctuation.definition.template-expression.begin", 250 | "punctuation.definition.template-expression.end", 251 | "punctuation.section.embedded" 252 | ], 253 | "settings": { 254 | "foreground": "#569cd6" 255 | } 256 | }, 257 | { 258 | "name": "Reset JavaScript string interpolation expression", 259 | "scope": [ 260 | "meta.template.expression" 261 | ], 262 | "settings": { 263 | "foreground": "#d4d4d4" 264 | } 265 | }, 266 | { 267 | "scope": [ 268 | "support.type.vendored.property-name", 269 | "support.type.property-name", 270 | "variable.css", 271 | "variable.scss", 272 | "variable.other.less", 273 | "source.coffee.embedded" 274 | ], 275 | "settings": { 276 | "foreground": "#9cdcfe" 277 | } 278 | }, 279 | { 280 | "scope": "keyword", 281 | "settings": { 282 | "foreground": "#569cd6" 283 | } 284 | }, 285 | { 286 | "scope": "keyword.control", 287 | "settings": { 288 | "foreground": "#569cd6" 289 | } 290 | }, 291 | { 292 | "scope": "keyword.operator", 293 | "settings": { 294 | "foreground": "#d4d4d4" 295 | } 296 | }, 297 | { 298 | "scope": [ 299 | "keyword.operator.new", 300 | "keyword.operator.expression", 301 | "keyword.operator.cast", 302 | "keyword.operator.sizeof", 303 | "keyword.operator.instanceof", 304 | "keyword.operator.logical.python" 305 | ], 306 | "settings": { 307 | "foreground": "#569cd6" 308 | } 309 | }, 310 | { 311 | "scope": "keyword.other.unit", 312 | "settings": { 313 | "foreground": "#b5cea8" 314 | } 315 | }, 316 | { 317 | "scope": [ 318 | "punctuation.section.embedded.begin.php", 319 | "punctuation.section.embedded.end.php" 320 | ], 321 | "settings": { 322 | "foreground": "#569cd6" 323 | } 324 | }, 325 | { 326 | "scope": "support.function.git-rebase", 327 | "settings": { 328 | "foreground": "#9cdcfe" 329 | } 330 | }, 331 | { 332 | "scope": "constant.sha.git-rebase", 333 | "settings": { 334 | "foreground": "#b5cea8" 335 | } 336 | }, 337 | { 338 | "name": "coloring of the Java import and package identifiers", 339 | "scope": [ 340 | "storage.modifier.import.java", 341 | "variable.language.wildcard.java", 342 | "storage.modifier.package.java" 343 | ], 344 | "settings": { 345 | "foreground": "#d4d4d4" 346 | } 347 | }, 348 | { 349 | "name": "this.self", 350 | "scope": "variable.language", 351 | "settings": { 352 | "foreground": "#569cd6" 353 | } 354 | } 355 | ] 356 | } -------------------------------------------------------------------------------- /color_theme_compatibility/display.mjs: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import theme_default from './dark_defaults.json' assert {type: 'json'}; 3 | import theme_vs from './dark_vs.json' assert {type: 'json'}; 4 | import theme_plus from './dark_plus.json' assert {type: 'json'}; 5 | 6 | const colorBg = theme_default.colors['editor.background'].toLowerCase(); 7 | const colorFg = theme_default.colors['editor.foreground'].toLowerCase(); 8 | const tokenSeq = [...theme_vs.tokenColors, ...theme_plus.tokenColors]; 9 | 10 | const scopeTree = {color: '', childMap: {}}; 11 | const colorMap = new Set(); 12 | 13 | for (const token of tokenSeq) { 14 | if (!token.scope || !token.settings.foreground) continue; 15 | if (!Array.isArray(token.scope)) token.scope = [token.scope]; 16 | for (const scope of token.scope) { 17 | let root = scopeTree; 18 | const partSeq = scope.split('.'); 19 | for (let i = 0; i < partSeq.length; i ++) { 20 | const part = partSeq[i]; 21 | if (!root.childMap[part]) root.childMap[part] = {childMap: {}}; 22 | root = root.childMap[part]; 23 | if (i === partSeq.length - 1) { 24 | root.color = token.settings.foreground.toLowerCase(); 25 | colorMap[root.color] = true; 26 | } 27 | } 28 | } 29 | } 30 | 31 | function drawScopeRecursive(childMap, offset = 0) { 32 | for (const [key, val] of Object.entries(childMap)) { 33 | let str = ""; 34 | for (let i = 0; i < offset; i ++) str += " "; 35 | str += key; 36 | if (val.color) { 37 | console.log(chalk.bgHex(colorBg).hex(val.color)(str)); 38 | } 39 | else { 40 | console.log(chalk.bgHex(colorBg).hex('#888')(str)); 41 | } 42 | drawScopeRecursive(val.childMap, offset + 1); 43 | } 44 | } 45 | 46 | function allScopesForColor(childMap, color, path = []) { 47 | let res = []; 48 | for (const [key, val] of Object.entries(childMap)) { 49 | if (val.color === color) res.push([...path, key].join('.')); 50 | res.push(...allScopesForColor(val.childMap, color, [...path, key])); 51 | } 52 | return res; 53 | } 54 | 55 | console.log(`colors: ${Object.keys(colorMap).length}`); 56 | console.log(chalk.bgHex(colorBg).hex(colorFg)(colorFg)); 57 | for (const color of Object.keys(colorMap)) { 58 | const scopeSeq = allScopesForColor(scopeTree.childMap, color); 59 | const count = String(scopeSeq.length).padEnd(2); 60 | const scope = scopeSeq.sort()[0]; 61 | let str = `${color} ${count} ${scope}`; 62 | console.log(chalk.bgHex(colorBg).hex(color)(str)); 63 | } 64 | -------------------------------------------------------------------------------- /doc/code_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/code_1.png -------------------------------------------------------------------------------- /doc/code_1.xi: -------------------------------------------------------------------------------- 1 | . |{lng:py;file:foo.py} 2 | | print("paragraph") 3 | -------------------------------------------------------------------------------- /doc/code_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/code_2.png -------------------------------------------------------------------------------- /doc/code_2.xi: -------------------------------------------------------------------------------- 1 | . Paragraph with a 2 | |{lng:py;file:foo.py} 3 | | print("code sample") 4 | -------------------------------------------------------------------------------- /doc/code_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/code_3.png -------------------------------------------------------------------------------- /doc/code_3.xi: -------------------------------------------------------------------------------- 1 | . Inline |{lng:py}"code"| sample. 2 | -------------------------------------------------------------------------------- /doc/heading.xi: -------------------------------------------------------------------------------- 1 | Heading 1 . 2 | One more heading 1 . 3 | Heading 2 . 4 | Heading 3 . 5 | Heading 4 . 6 | Heading 5 . 7 | Heading 6 . 8 | No more . 9 | -------------------------------------------------------------------------------- /doc/heading_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/heading_1.png -------------------------------------------------------------------------------- /doc/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/highlight.png -------------------------------------------------------------------------------- /doc/highlight.xi: -------------------------------------------------------------------------------- 1 | . We `highlight` part of text and |Term| within it. 2 | -------------------------------------------------------------------------------- /doc/link3.xi: -------------------------------------------------------------------------------- 1 | . Basic loop is 100 times faster compared to [python]/[ruby]/[php] [#1]. 2 | 3 | Reference . 4 | . [1#] |http://habrahabr.ru/post/162885/#comment_5598039|. 5 | -------------------------------------------------------------------------------- /doc/link_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/link_1.png -------------------------------------------------------------------------------- /doc/link_1.xi: -------------------------------------------------------------------------------- 1 | Python . 2 | . [about python]. 3 | . [python syntax]. 4 | . [python ecosystem]. 5 | -------------------------------------------------------------------------------- /doc/link_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/link_2.png -------------------------------------------------------------------------------- /doc/link_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/link_3.png -------------------------------------------------------------------------------- /doc/list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/list.png -------------------------------------------------------------------------------- /doc/list.xi: -------------------------------------------------------------------------------- 1 | * Unordered list first item. 2 | * Unordered list second item. 3 | # Ordered sublist first item. 4 | # Ordered sublist second item. 5 | -------------------------------------------------------------------------------- /doc/md_to_xi_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/md_to_xi_dst.png -------------------------------------------------------------------------------- /doc/md_to_xi_dst.xi: -------------------------------------------------------------------------------- 1 | Heading 1 . 2 | . First paragraph 3 | multiline text. 4 | . Second paragraph text. 5 | Heading 2 . 6 | . Third paragraph text. 7 | -------------------------------------------------------------------------------- /doc/md_to_xi_src.md: -------------------------------------------------------------------------------- 1 | # Heading 1 2 | First paragraph 3 | multiline text. 4 | 5 | Second paragraph text. 6 | ## Heading 2 7 | Third paragraph text. 8 | -------------------------------------------------------------------------------- /doc/md_to_xi_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/md_to_xi_src.png -------------------------------------------------------------------------------- /doc/meta_heading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/meta_heading.png -------------------------------------------------------------------------------- /doc/named_param_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/named_param_1.png -------------------------------------------------------------------------------- /doc/named_param_1.xi: -------------------------------------------------------------------------------- 1 | . {param-name} is not a |term|. 2 | . Input {i named_param}. 3 | . Output typed {o(str) param}. 4 | -------------------------------------------------------------------------------- /doc/named_param_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/named_param_2.png -------------------------------------------------------------------------------- /doc/named_param_2.xi: -------------------------------------------------------------------------------- 1 | - insert . 2 | . After specified {i indice} inserts specified {i item}. 3 | -------------------------------------------------------------------------------- /doc/paragraph_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/paragraph_1.png -------------------------------------------------------------------------------- /doc/paragraph_1.xi: -------------------------------------------------------------------------------- 1 | . This is a single-single line paragraph. 2 | . And this is a multi-line paragraph that 3 | uses two spaces to indent a second line. 4 | . This is a nested paragraph. Normally 5 | we use headings with nested paragraphs. 6 | . Paragraphs without headings is a mess. 7 | -------------------------------------------------------------------------------- /doc/xi_illustration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/doc/xi_illustration.png -------------------------------------------------------------------------------- /esbuild.mjs: -------------------------------------------------------------------------------- 1 | import * as esbuild from 'esbuild'; 2 | 3 | esbuild.build({ 4 | bundle: true, 5 | outfile: './out/extension.js', 6 | entryPoints: ['./src/extension.mjs'], 7 | external: ['vscode', 'process', 'util', 'path', 'fs', 'os'], 8 | format: 'cjs', 9 | platform: 'neutral', 10 | }); 11 | -------------------------------------------------------------------------------- /examples/lua_syntax.xi: -------------------------------------------------------------------------------- 1 | Lua syntax @ 2 | 3 | . [lua table]. 4 | . [lua error handling]. 5 | . [lua metatable]. 6 | . [lua metamethod]. 7 | . [lua garbage collector]. 8 | . [lua multiple assignment]. 9 | 10 | General . 11 | . Case-sensitive. 12 | . First-class functions. 13 | 14 | Nonusual . 15 | . Items are indexed from 1. 16 | . Referencing unbound name evaluates to |nil|. 17 | . All variables are in global scope by default, unless |local| keyword. 18 | . Strings are 8-bit without terminating null, no [unicode] support. 19 | . 0, empty string and empty containers are coerced into |true|. 20 | . All [composite data type]s (list, map etc) are single [lua table] type. 21 | . Using length on non-sequence tables with holes is implementation-specific. 22 | . All numbers are 64-bit [float]s. 23 | . Has |goto| statement. 24 | . Strings concatenated via |..|, not |+|. 25 | . No |++| or |+=|. 26 | . "Not equal" is |~=|. 27 | . |for| range includes both ends. 28 | . |#| "length" prefix operator. 29 | . |else if| is |elseif|, not |elif| or |else if|. 30 | . |repeat| with |until| loop. 31 | . No |continue| for loops. 32 | . Calls with one string or table param don't need parens. 33 | . "Instance methods" [lua metatable] is set for strings, but not tables. 34 | 35 | Comment[] . 36 | . Line comments are from |--| to |EOL|. 37 | . Block comments are from |--[[| to |]]|. 38 | 39 | Entry point . 40 | . Not required. 41 | 42 | Name binding[] . 43 | Scope[] . 44 | . Expressions are evaluated and then assigned. 45 | |{lng:lua} 46 | | x = y + 2 -- first evaluate 'y + 2', than assign result to 'x' 47 | . [Augmented assignment] not implemented due to a single-pass compiler. 48 | . [Destructuring assignment] via [lua multiple assignment]. 49 | . Name is any string of Latin letters, Arabic-Indic digits, and underscores, 50 | not beginning with a digit and not being a reserved word. 51 | . As a convention, programs should avoid creating names that start with 52 | an underscore followed by one or more uppercase letters, ex |_VERSION|. 53 | . Variable defined in a loop operator is in operator's body scope, ex: 54 | |{lng:lua} 55 | | for i = 1, 2 do foo() end print(i) -- prints nil, 'i' is out of scope 56 | All other variables defined in Lua default to the `global` scope unless 57 | they use `local` keyword, ex: 58 | |{lng:lua} 59 | | function foo() a = 1 end 60 | | print( a ) -- nil 61 | | foo(); print( a ) -- 1, in foo() 'a' is defined in global scope 62 | | function bar() local b = 1 end 63 | | bar(); print( b ) -- nil, 'b' is in bar()'s scope, not in our scope 64 | 65 | Data type[] . 66 | Primitive data type[] . 67 | . No primitive data types, everything is an object. 68 | Composite data type[] . 69 | . Via [lua table]s. 70 | Anonymous data type[] . 71 | . Via [lua table]s. 72 | Nullable data type[] . 73 | . The type |nil| has one single value, |nil|. 74 | Boolean data type[] . 75 | . The type |boolean| has two values, |false| and |true|. 76 | . |false| and |nil| are caerced to |false|, everything else to |true| 77 | ! Including 0, "", empty containers. 78 | . No |boolean| type before [lua version#5.0], |nil| was used instead. 79 | Literal constructor[] . 80 | . [lua table#literal constructor]. 81 | . A raw string "long literal" starts with "[", followed by zero or more "=" 82 | and then "[" again. Ends with the matching square braces and equals: 83 | |{lng:lua} 84 | | level0 = [[ 85 | | foo 86 | | ]] 87 | | level2 = [==[ bar ]==] 88 | String . 89 | . Immutable. 90 | . Strings can contain any 8-bit value, including embedded zeros ('\0'): 91 | |{lng:lua} 92 | | a, b, c = "test", 'test', [[test]] 93 | Unicode . 94 | . Not supported, strings are bytes, third-party libs are used. 95 | Escape . 96 | . [c]-like escape sequences recognized within quoted strings. 97 | . The escape sequence |\z| skips the following span of whitespace 98 | characters, including line breaks. 99 | . The [utf-8] encoding of a Unicode character can be inserted in 100 | a literal string with the escape sequence |\u{XXX}| (with mandatory 101 | enclosing braces), where XXX is a sequence of one or more hexadecimal 102 | digits representing the character code point. 103 | Concatenation . 104 | . Via [lua syntax#operator#concatenation]. 105 | Substring . 106 | . Via [lua api#$ string]. 107 | Length . 108 | . Via the [lua syntax#operator#length]. 109 | Line span . 110 | . Via "long literal" constructor. 111 | String interpolation[] . 112 | . Via [lua api#string#format]. 113 | List[] . 114 | . Emulated via general [lua table] data type. 115 | Set[] . 116 | . Emulated via general [lua table]s: 117 | |{lng:lua} 118 | | local set = {pear = true, plum = true} 119 | Map[] . 120 | . Emulated via general [lua table] data type. 121 | Enum . 122 | . Emulated via general [lua table]s: 123 | |{lng:lua} 124 | | Colors = {RED = 1, GREEN = 2, BLUE = 3} 125 | Tuple[] . 126 | . Via [lua multiple assignment]. 127 | Range[] . 128 | . Built into [lua syntax#for loop] syntax. 129 | Type conversion[] . 130 | . See [lua syntax#boolean data type]. 131 | . [lua api#string#format] converts numbers to strings. 132 | . [lua api#tonumber] explicitly converts strings to numbers. 133 | Type coercion[] . 134 | . Bitwise operators always convert float operands to integers. 135 | . Exp and float division always convert integer operands to floats. All 136 | other arithmetic operations applied to mixed numbers (integers and 137 | floats) convert the integer operand to a float. 138 | . String concatenation accepts numbers as arguments, besides strings. 139 | . Conversion from float to integer fails if no exact representation exists. 140 | . The string library sets [lua metamethod]s that try to coerce 141 | [lua api#* string] to numbers in all arithmetic operations. They are not 142 | always applied; in particular, |"1"==1| is false and |"1"<1| raises 143 | an error. These coercions exist mainly for compatibility and may be 144 | removed in future versions of the language. 145 | Type inference[] . 146 | . Not needed for dynamic language. 147 | Object copy[] . 148 | . [shallow copy] array [lua table] via [lua api#$ table#unpack]. 149 | 150 | Operator[] . 151 | . Supports most [c syntax#operator]s. 152 | . Power operator |^|. 153 | . |{lng:lua} 154 | | 3 / 2 -- 1.5, all numbers are float 155 | . Ternary emulated via [programming language tricks]: "and or", ex: 156 | |{lng:lua} 157 | | a = b and c or d -- c must be not nil and not false 158 | Bitwise . 159 | . Supported since [lua version#5.3], |bit32| library before that. 160 | . |&|, |||, |^|, |~| ("exclusive or", "unary not"), |<<|, |>>|. 161 | Logical . 162 | . [short circuit logic]. 163 | . |and|, |or|, |not|, |>|, |>=|, |<|, |<=|. 164 | |{lng:lua} 165 | | not false == true; not nil == true; -- everything else, ex 0, is false 166 | | - a -- -(-1) == 1 167 | | not foo and not bar or baz -- ((not foo) and (not bar)) or baz 168 | Value equality . 169 | . |==| and |~=| uses following logic: 170 | * |false| if types are different. 171 | * |true| if value is same for strings and numbers. Do not convert 172 | strings to numbers or vice versa. 173 | * |true| if [lua table], |userdate| or |thread| reference the same object: 174 | |{lng:lua} 175 | | {} == {} -- false; references to two different objects 176 | * [lua metamethod#__eq], if any. 177 | Object identity test operator[] . 178 | . [lua api#rawequal]. 179 | Sequence membership . 180 | . Accessing [lua table] key without a value evaluats to |nil|. 181 | Operator overload[] . 182 | . Via [lua metamethod]s. 183 | Length . 184 | . |#| prefix operator that maps to the [lua metamethod#__len]: 185 | |{lng:lua} 186 | | len1, len2 = # str1, #str2 187 | . Applied on a [lua table] returns a border in that table (index present in 188 | the table that is followed by an absent index; or zero, when index 1 is 189 | absent). 190 | Concatenation . 191 | . |..| operator that maps to the [lua metamethod#__concat]. 192 | 193 | Expression[] . 194 | Compound . 195 | . Operators and |()|. 196 | Conditional . 197 | . Via [lua syntax#operator#logical]. 198 | Relational . 199 | . |{lng:lua} 200 | | a == 1 -- evaluates to true or false 201 | 202 | Statement[] . 203 | . Can be separated with optional |;|. 204 | . Newlines are same token separators as spaces: 205 | |{lng:lua} 206 | | foo = 207 | | 2 208 | If-then-else . 209 | . |{lng:lua} 210 | | if a > 0 then print(a) end 211 | | if a > 0 then print(a) else print("b") end 212 | | if a > 0 then print(a) elseif a < 0 print("-") end 213 | For loop . 214 | . |{lng:lua} 215 | | for i = 1, 5 do print(i) end -- range includes both ends 216 | | for i = 5, 1, -1 do print(i) end -- optional step 217 | For iterator . 218 | . Via [lua api#pairs], [lua api#ipairs], [lua api#next]. 219 | While loop . 220 | . |{lng:lua} 221 | | while foo() do bar() end 222 | Do-while loop . 223 | . |{lng:lua} 224 | | repeat foo() until bar() 225 | Break loop . 226 | . |{lng:lua} 227 | | break 228 | . Must be last statement in a block (becouse breaks block), ex: 229 | |{lng:lua} 230 | | while true do if a > 5 then a = 0; break end end -- ok 231 | | while true do if a > 5 then break; a = 0 end -- syntax error 232 | 233 | Callable[] . 234 | . Calls with one string or table param don't need parens: 235 | |{lng:lua} 236 | | print "foo" 237 | | print {bar = 1} 238 | | print 1 -- syntax error 239 | . |function|, |end| pair forms an expression: 240 | |{lng:lua} 241 | | hs.hotkey.bind({}, "capslock", function() hs.alert.show("debug") end) 242 | Procedure . 243 | . |{lng:lua} 244 | | bar = {} 245 | | local function foo(x) return x * 2 end 246 | | function bar.baz() return ":)" end 247 | This is a syntax sugar to function [definition] assignment [statement]: 248 | |{lng:lua} 249 | | local foo; foo = function (x) return x * 2 end 250 | | bar.baz = function() return ":)" end 251 | Parameter[] . 252 | . When a Lua function is called, it adjusts its list of arguments to 253 | the length of its list of parameters, unless the function is a vararg 254 | function, which is indicated by |...| at the end of its parameter list. 255 | . |...| within vararg functin expands into "rest arguments": 256 | |{lng:lua} 257 | | function foo(a, b, ...) 258 | | c, d = ... -- 3, 4 259 | | end 260 | | foo(1, 2, 3, 4) 261 | . |...| can be used with [lua table] constructor: 262 | |{lng:lua} 263 | | function foo(...) for k, v in pairs({...}) do print(k, v) end end 264 | Return[] . 265 | . Multiple return value emulated via [lua multiple assignment]. 266 | Anonymous function . 267 | . As in [js] and [scheme] all functions are anonymous. A "named 268 | function" is a variable holding a reference to a function object. 269 | . Functions encloses parent scope(s). 270 | 271 | Class[] . 272 | . |:| during a call is a syntactic sugar to pass the "call receiver" as 273 | a first argument. Following lines generate the same bytecode: 274 | |{lng:lua} 275 | | foo:bar() 276 | | foo.bar(foo) 277 | . |:| during callable declaration is a syntactic sugar that creates 278 | callable with a hidden first parameter named |self| (and sets this 279 | callable as an attribute for a left-hand lua table). Following lines 280 | generate the same bytecode: 281 | |{lng:lua} 282 | | function obj:foo() end 283 | | function obj.foo(self) end 284 | | obj.foo = function(self) end 285 | . Class emulation via |self| and [lua metamethod#__index]: 286 | |{lng:lua} 287 | | -- 'Dog' is class object 288 | | function Dog:new() 289 | | local inst = {sound = 'woof'} 290 | | -- Use the class object for attributes not found on the instance object 291 | | return setmetatable(inst, {__index = self}) 292 | | end 293 | | -- Method is bound to the class object, but available via metatable 294 | | function Dog:makeSound() 295 | | -- 'self' here will be an instance object (call receiver) 296 | | print('I say ' .. self.sound) 297 | | end 298 | | mrDog = Dog:new() 299 | | mrDog:makeSound() 300 | . It'a also ideomatic to set class object as a metatable for class instances, 301 | so special [lua metamethod]s can be defined on class object: 302 | |{lng:lua} 303 | | function Dog:new() 304 | | self.__index = self 305 | | return setmetatable({}, self) 306 | | end 307 | 308 | Inheritance[] . 309 | 310 | Interface[] . 311 | Abstract class[] . 312 | 313 | Generic[] . 314 | 315 | Decorator[] . 316 | 317 | Property[] . 318 | 319 | Category[] . 320 | Mixin[] . 321 | 322 | Generator[] . 323 | 324 | Preprocessor[] . 325 | 326 | Dependency management[] . 327 | . |a.lua|: 328 | |{lng:lua} 329 | | function foo() print("foo") end 330 | | local val = 1 331 | |b.lua|: 332 | |{lng:lua} 333 | | require "a" 334 | | a.foo() 335 | | print(a.val) -- error, package local 336 | Build-in table |package| allows to enumerate and manage modules: 337 | |{lng:lua} 338 | | for k, v in pairs(package) do print(k, v) end -- list packages 339 | | package.loaded.a = nil; require "a" -- reload package 340 | . |require| searches directories specified in the |package.path|, which 341 | can be set via the |LUA_PATH| environment variable: 342 | |{lng:lua} 343 | | package.path = package.path .. ";/Users/user/dotfiles/hammerspoon/?.lua" 344 | | require "lib" 345 | | someNameFromLibNamespace() 346 | 347 | Error handling[] . 348 | . [lua error handling]. 349 | 350 | Reflection[] . 351 | . [lua debug]. 352 | 353 | Test[] . 354 | Assert . 355 | . |{lng:lua} 356 | | assert(exp); assert(exp. "error") 357 | ! |assert(0)| don't work, use |assert(nil)| or |assert(false)|. 358 | -------------------------------------------------------------------------------- /examples/neurotransmitter.xi: -------------------------------------------------------------------------------- 1 | Neurotransmitter @ 2 | 3 | . [synapse]. 4 | 5 | . Kind of [neuromediator]. 6 | . English literature reference both [neurotransmitter]s and [neuromodulator]s 7 | as "neurotransmitters". 8 | . Neurotransmitter can be |Excitatory| for some receptors and |Excitatory| to 9 | other. Грубая оценка: глутамат возбуждающий, ГАМК тормозный, ацетилхолин 10 | возбуждающий, глицин тормозный. Всё. По остальным уже больше "это зависит". 11 | . Monoamine neurotransmitters control epigenetic regulation of genes [#1]. 12 | . By using at least fifty different chemical signals, the brain can carry on 13 | many different conversations at once: each neurotransmitter stimulates 14 | a different set of cells or alters their sensitivity to different chemical 15 | messengers. 16 | 17 | Excitatory . 18 | Возбуждающие . 19 | . [glutamate]. 20 | . [acetylcholine]. 21 | . [dopamine]. 22 | . [adenosine]. 23 | . [noradrenaline]. 24 | 25 | Inhibitory . 26 | Тормозящие . 27 | Ингибирующие . 28 | . Makes neuron less likely to fire an action potential or release 29 | neurotransmitters. 30 | . [gaba] (ГАМК). 31 | . [glycine]. 32 | . [adenosine]. 33 | . [serotonin]. 34 | 35 | Reference @ 36 | . [1#] [https://bit.ly/31RfKw9]. 37 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/icon.png -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "|", 4 | "blockComment": [] 5 | }, 6 | "brackets": [], 7 | "autoClosingPairs": [ 8 | ["{", "}"], 9 | ["[", "]"], 10 | ["(", ")"], 11 | ["\"", "\""], 12 | ["'", "'"], 13 | ["`", "`"] 14 | ], 15 | "surroundingPairs": [ 16 | ["{", "}"], 17 | ["[", "]"], 18 | ["(", ")"], 19 | ["\"", "\""], 20 | ["'", "'"], 21 | ["`", "`"], 22 | ["|", "|"] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /out/.gitignore: -------------------------------------------------------------------------------- 1 | *.* 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "language-xi", 3 | "displayName": "Xi markup language", 4 | "description": "Syntax highlighting and wikiword jumps support for Xi markup language", 5 | "repository": "https://github.com/grigoryvp/vscode-language-xi", 6 | "icon": "icon.png", 7 | "version": "1.10.67", 8 | "publisher": "grigoryvp", 9 | "license": "SEE LICENSE IN README.md", 10 | "engines": { 11 | "vscode": ">=1.75.0" 12 | }, 13 | "categories": [ 14 | "Other" 15 | ], 16 | "scripts": { 17 | "vscode:prepublish": "node esbuild.mjs", 18 | "color-theme": "node --no-warnings ./color_theme_compatibility/display.mjs", 19 | "test": "npm exec mocha", 20 | "build": "vsce package --out ./out/extension.vsix", 21 | "build-pre-release": "vsce package --pre-release --out ./out/extension.vsix", 22 | "publish": "open-cli https://marketplace.visualstudio.com/manage" 23 | }, 24 | "main": "./out/extension.js", 25 | "browser": "./out/extension.js", 26 | "files": [ 27 | "./out/extension.js", 28 | "./src/*.json" 29 | ], 30 | "contributes": { 31 | "languages": [{ 32 | "id": "xi", 33 | "extensions": [ 34 | ".xi" 35 | ], 36 | "configuration": "./language-configuration.json" 37 | }, { 38 | "id": "xi-markdown-injection" 39 | }], 40 | "grammars": [{ 41 | "language": "xi", 42 | "scopeName": "text.xi", 43 | "path": "./src/xi.tmLanguage.json" 44 | }, { 45 | "language": "xi-markdown-injection", 46 | "scopeName": "markdown.xi.codeblock", 47 | "injectTo": [ 48 | "text.html.markdown" 49 | ], 50 | "embeddedLanguages": { 51 | "meta.embedded.block.xi": "xi" 52 | }, 53 | "path": "./src/xi-markdown-injection.tmLanguage.json" 54 | }], 55 | "commands": [{ 56 | "command": "extension.xi.lookup", 57 | "title": "Xi: Look up a .xi file", 58 | "when": "!isWeb" 59 | }], 60 | "configuration": { 61 | "type": "object", 62 | "title": "Xi configuration", 63 | "properties": { 64 | "xi.lookupPath": { 65 | "type": "string", 66 | "default": "~/.xi", 67 | "description": "Path to a dir with .xi files" 68 | }, 69 | "xi.debug": { 70 | "type": "boolean", 71 | "default": false, 72 | "description": "true to enable debug output" 73 | } 74 | } 75 | }, 76 | "keybindings": [{ 77 | "command": "extension.xi.lookup", 78 | "key": "ctrl+k x", 79 | "mac": "cmd+k x", 80 | "when": "!isWeb" 81 | }] 82 | }, 83 | "devDependencies": { 84 | "@types/chai": "^4.3.11", 85 | "@vscode/vsce": "^2.22.0", 86 | "chai": "^4.3.10", 87 | "chalk": "^5.3.0", 88 | "esbuild": "^0.19.11", 89 | "mocha": "^10.8.2", 90 | "open-cli": "^7.2.0" 91 | }, 92 | "dependencies": { 93 | "vscode-uri": "^3.0.8" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/extension.mjs: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { Utils } from 'vscode-uri' 3 | import getLinkProvider from './get_link_provider.mjs'; 4 | import getFoldProvider from './get_fold_provider.mjs'; 5 | import getEditorChangeHandler from './get_editor_change_handler.mjs'; 6 | import getGoBackCmd from './get_go_back_cmd.mjs'; 7 | import getOpenCmd from './get_open_cmd.mjs'; 8 | 9 | 10 | // Not exposed by default 11 | vscode.Uri.Utils = Utils; 12 | 13 | 14 | export async function activate(ctx) { 15 | const HISTORY_KEY = 'file-history'; 16 | const LinkProvider = getLinkProvider(vscode); 17 | const FoldProvider = getFoldProvider(vscode); 18 | const onEditorChange = getEditorChangeHandler(ctx, HISTORY_KEY); 19 | const goBackCmd = getGoBackCmd(vscode, ctx, HISTORY_KEY); 20 | const openCmd = getOpenCmd(vscode); 21 | 22 | vscode.window.onDidChangeActiveTextEditor(onEditorChange); 23 | const regDocLinkProvider = vscode.languages.registerDocumentLinkProvider; 24 | ctx.subscriptions.push(regDocLinkProvider('xi', new LinkProvider())); 25 | const regFoldProvider = vscode.languages.registerFoldingRangeProvider; 26 | ctx.subscriptions.push(regFoldProvider('xi', new FoldProvider())); 27 | const regCmd = vscode.commands.registerCommand; 28 | ctx.subscriptions.push(regCmd('extension.xi.goBack', goBackCmd)); 29 | ctx.subscriptions.push(regCmd('extension.xi.open', openCmd)); 30 | 31 | // FIXME: not implemented for web context, need use cases. 32 | if (vscode.env.uiKind !== vscode.UIKind.Web ) { 33 | const loadedModule = await import('./get_lookup_cmd.mjs'); 34 | const lookupCmd = loadedModule.default(vscode); 35 | ctx.subscriptions.push(regCmd('extension.xi.lookup', lookupCmd)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/get_editor_change_handler.mjs: -------------------------------------------------------------------------------- 1 | export default function getEditorChangeHandler(ctx, key) { 2 | return function(e) { 3 | if (!e) return; 4 | if (!e.document) return; 5 | const filePath = e.document.fileName; 6 | if (filePath.endsWith('.xi')) { 7 | let history = ctx.globalState.get(key); 8 | if (!Array.isArray(history)) history = []; 9 | if (history.length > 100) history = history.slice(-100); 10 | history.push(filePath); 11 | ctx.globalState.update(key, history); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/get_fold_provider.mjs: -------------------------------------------------------------------------------- 1 | export default function getFoldProvider(vscode) { 2 | 3 | 4 | class FoldProvider { 5 | 6 | 7 | provideFoldingRanges(doc, context, token) { 8 | // return [vscode.FoldingRange(0, 10, vscode.FoldingRangeKind.Region)]; 9 | return []; 10 | } 11 | } 12 | 13 | 14 | return FoldProvider; 15 | } 16 | -------------------------------------------------------------------------------- /src/get_go_back_cmd.mjs: -------------------------------------------------------------------------------- 1 | export default function getGoBackCmf(vscode, ctx, key) { 2 | return function() { 3 | const history = ctx.globalState.get(key); 4 | if (Array.isArray(history) && history.length > 1) { 5 | // Current file. 6 | history.pop(); 7 | // Previous file 8 | const filePath = history[history.length - 1]; 9 | ctx.globalState.update(key, history); 10 | const uri = vscode.Uri.file(filePath); 11 | vscode.workspace.openTextDocument(uri).then(doc => { 12 | vscode.window.showTextDocument(doc); 13 | }); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/get_jump_to_anchor.mjs: -------------------------------------------------------------------------------- 1 | export default function getJumpToAnchor(vscode) { 2 | 3 | // From MDN 4 | function escapeRegExp(string) { 5 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 6 | } 7 | 8 | 9 | function jumpToPos(editor, idx) { 10 | const pos = editor.document.positionAt(idx); 11 | const range = new vscode.Range(pos, pos); 12 | editor.revealRange(range, vscode.TextEditorRevealType.InCenter); 13 | const selection = new vscode.Selection(pos, pos); 14 | editor.selection = selection; 15 | } 16 | 17 | 18 | return function(anchor) { 19 | anchor = anchor.toLowerCase(); 20 | const editor = vscode.window.activeTextEditor; 21 | if (!editor) return; 22 | let text = editor.document.getText().toLowerCase(); 23 | 24 | // Special case, link to same document like [#foo] 25 | if (anchor.startsWith('#')) { 26 | const query = escapeRegExp(`[${anchor.slice(1)}#]`); 27 | const idx = text.search(query); 28 | if (!idx) return; 29 | jumpToPos(editor, idx); 30 | return; 31 | } 32 | 33 | let lastFoundIdx = null; 34 | let lastFoundSize = 0; 35 | // Handle nested anchor like [foo#bar#baz] 36 | for (const fragment of anchor.split('#')) { 37 | const query = (() => { 38 | const needle = escapeRegExp(fragment); 39 | //! Can't use '\s' since it matches '\n' in multiline mode. 40 | const link = `(\\[[^\\]]*\\])?`; 41 | const selflink = `(\\[\\])?`; 42 | // Type for headers that defines something that has a type: 43 | // @ property . 44 | // @(seq) typed-property . 45 | const type = `([^ \\t\\n]+[ \\t])?`; 46 | const begin = `[ \\t]*${type}${link}[ \\t]*`; 47 | const end = `${selflink}[ \\t]*${link}[ \\t]\\.`; 48 | const query = `^${begin}${needle}${end}$`; 49 | // No 'g', first match. 50 | return new RegExp(query, 'im'); 51 | })(); 52 | 53 | const match = text.match(query); 54 | // Break on first not found and use last found index, if any. 55 | if (!match) break; 56 | 57 | if (!lastFoundIdx) { 58 | lastFoundIdx = match.index; 59 | } 60 | else { 61 | // We are searching remainaing text, so it's a relative index. 62 | lastFoundIdx += lastFoundSize + match.index; 63 | } 64 | 65 | // Remaining text. 66 | lastFoundSize = match[0].length; 67 | text = text.slice(match.index + lastFoundSize); 68 | 69 | } 70 | 71 | if (lastFoundIdx) { 72 | jumpToPos(editor, lastFoundIdx); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/get_link_provider.mjs: -------------------------------------------------------------------------------- 1 | import * as tools from './tools.mjs'; 2 | 3 | 4 | // Requires vscode.Uri.Utils to reference { Utils } from 'vscode-uri' 5 | export default function getLinkProvider(vscode) { 6 | const Utils = vscode.Uri.Utils; 7 | 8 | 9 | class LinkProvider { 10 | 11 | 12 | async provideDocumentLinks(doc, cancel) { 13 | const cfg = vscode.workspace.getConfiguration('xi'); 14 | if (cfg.debug) { 15 | tools.debug(vscode, "LinkProvider().provideDocumentLinks()"); 16 | const dirUri = Utils.dirname(vscode.Uri.file(doc.fileName)); 17 | tools.debug(vscode, ` doc ${doc.fileName} dir ${dirUri.toString()}`); 18 | } 19 | 20 | const res = []; 21 | res.push(... await this._getWikiwordLinks(doc, cancel)); 22 | if (cancel.isCancellationRequested) return []; 23 | res.push(... await this._getHeaderLinks(doc, cancel)); 24 | if (cancel.isCancellationRequested) return []; 25 | return res; 26 | } 27 | 28 | 29 | // Give link name inside [], like "foo#bar" evaluates to vscode.Uri 30 | // or null. 31 | async _linkNameToVSCodeURI(doc, name) { 32 | if (name.startsWith('#')) { 33 | const anchor = name.slice(1).trim(); 34 | if (anchor.length) { 35 | return vscode.Uri.parse(`command:extension.xi.open?${ 36 | encodeURIComponent(JSON.stringify({ 37 | // Without file it's not header anchor, but a wikiword. 38 | anchor, 39 | })) 40 | }`); 41 | } 42 | else { 43 | // [#] or #[] is meaningless 44 | return null; 45 | } 46 | } 47 | else if (name.match(/^[a-zA-Z_-]+:\/\//)) { 48 | return vscode.Uri.parse(name); 49 | } 50 | else { 51 | const [link, ...anchorSeq] = name.split('#'); 52 | const anchor = anchorSeq.join('#'); 53 | // Always use lowercase since different wikiwords can have 54 | // different writing depending on context, ex capitalized at 55 | // the sentence start. 56 | const fileName = `${link.replace(/ /g, '_')}.xi`.toLowerCase(); 57 | const dirUri = Utils.dirname(vscode.Uri.file(doc.fileName)); 58 | let fileUri = null; 59 | if (dirUri.path && dirUri.path.length > 0) { 60 | fileUri = Utils.joinPath(dirUri, fileName); 61 | } else { 62 | fileUri = vscode.Uri.file(fileName); 63 | } 64 | 65 | if (anchor) { 66 | return vscode.Uri.parse(`command:extension.xi.open?${ 67 | encodeURIComponent(JSON.stringify({file: fileUri.path, anchor})) 68 | }`); 69 | } 70 | else { 71 | try { 72 | await vscode.workspace.fs.stat(fileUri); 73 | // Open existing file reusing current editor tab if possible 74 | return vscode.Uri.parse(`command:extension.xi.open?${ 75 | encodeURIComponent(JSON.stringify({file: fileUri.path})) 76 | }`); 77 | } catch(e) { 78 | // If no anchor like [foo#bar] or foo#bar[] is specified, use 79 | // normal file // uri so VSCode will ask to create a file if 80 | // it does not exists. This will use a new editor tab. 81 | return fileUri; 82 | } 83 | } 84 | } 85 | } 86 | 87 | 88 | // Gather links for simple wikiwords like "[foo#bar]" 89 | async _getWikiwordLinks(doc, cancel) { 90 | const res = []; 91 | const text = doc.getText(); 92 | const textLen = text.length 93 | const query = /\[[^ \]\r\n]\]|\[[^ \]\r\n][^\]\r\n]*[^ \]\r\n]\]/gm; 94 | while (true) { 95 | if (cancel.isCancellationRequested) return []; 96 | const match = query.exec(text); 97 | if (!match) break; 98 | 99 | // Skip '[' and ']', min length is 3 for wikiwords like '[v]'. 100 | const beginIdx = match.index + 1; 101 | const endIdx = match.index + match[0].length - 1; 102 | let lineBeginIdx = match.index; 103 | while (lineBeginIdx > 0 && text[lineBeginIdx - 1] !== "\n") { 104 | lineBeginIdx --; 105 | } 106 | let lineEndIdx = match.index; 107 | while (lineEndIdx < textLen && text[lineEndIdx + 1] !== "\n") { 108 | lineEndIdx += 1; 109 | } 110 | const name = text.substr(beginIdx, endIdx - beginIdx); 111 | // String from line start to link match. 112 | const prefix = text.substr(lineBeginIdx, match.index - lineBeginIdx); 113 | // Line containing the link 114 | const line = text.substr(lineBeginIdx, lineEndIdx - lineBeginIdx + 1); 115 | 116 | // Do not match links in code smaples like ` | [foo]` 117 | if (prefix.match(/^\s*\|\s+/)) continue; 118 | // Do not match links in code smaples like `. | [foo]` 119 | if (prefix.match(/^\s*\. \|\s+/)) continue; 120 | // Do not match links that contain '|', most probably it's 121 | // something like "|[| |]]|" 122 | if (name.includes('|')) continue; 123 | 124 | const charBefore = beginIdx > 1 ? text[beginIdx - 2] : null; 125 | const charAfter = endIdx < text.length - 1 ? text[endIdx + 1] : null; 126 | // Can be something like |[foo]| or |{lng:py} foo[bar]| 127 | if (line.includes('|')) { 128 | let markBeginIdx = null; 129 | let found = false; 130 | // Go from left to right and find all marked words and code 131 | // samples. They are started with pipe plus adjasted character 132 | // to the right and ended with pipe plus adjasted character 133 | // to the left, with any characters inbetween. 134 | for (let i = lineBeginIdx + 1; i <= lineEndIdx - 1; i ++) { 135 | const curChar = text[i]; 136 | const prevChar = text[i - 1]; 137 | const nextChar = text[i + 1]; 138 | if (curChar != ' ' && prevChar == '|') { 139 | markBeginIdx = i; 140 | continue; 141 | } 142 | if (curChar != ' ' && nextChar == '|' && markBeginIdx) { 143 | const markEndIdx = i; 144 | // Link within a marked word or a code sample? 145 | if (beginIdx >= markBeginIdx && endIdx <= markEndIdx) { 146 | found = true; 147 | break; 148 | } 149 | markBeginIdx = null; 150 | } 151 | } 152 | // Do not match links inside marked words: `|foo| |bar| |[baz]|` 153 | // and inside code samples with meta info: `|{lng:js}[foo]|` 154 | if (found) continue; 155 | } 156 | 157 | //! Can't rely on pipe count: `||| [foo]`. 158 | // if (charBefore === '|' && charAfter === '|') continue; 159 | 160 | const uri = await this._linkNameToVSCodeURI(doc, name); 161 | if (uri) { 162 | const toPosition = (v) => doc.positionAt(v); 163 | const [begin, end] = [beginIdx, endIdx].map(toPosition); 164 | const range = new vscode.Range(begin, end); 165 | res.push(new vscode.DocumentLink(range, uri)); 166 | } 167 | } 168 | return res; 169 | } 170 | 171 | 172 | // Gather links for header wikiwords like " [foo] bar baz[] [foo] ." 173 | // This also includes header anchor links like #todo[] 174 | async _getHeaderLinks(doc, cancel) { 175 | const res = []; 176 | const text = doc.getText(); 177 | const query = (() => { 178 | const link = `\\[[^\\]]+\\]`; 179 | // Header can't end with space, ex 'foo [] .': '[]' or ' .' should 180 | // be adjasted to it. 181 | const header = `[^\\[\\r\\n]*[^ \\[\\r\\n]`; 182 | const terminator = `\\s\\.`; 183 | const begin = `\\s*(?:${link}\\s+)?`; 184 | const end = `\\[\\](?:\\s+${link})?${terminator}`; 185 | const query = `^(${begin})(${header})(${end})$`; 186 | return new RegExp(query, 'gm'); 187 | })(); 188 | while (true) { 189 | var match = query.exec(text); 190 | if (!match) break; 191 | if (cancel.isCancellationRequested) return []; 192 | // We have exactly 3 capture groups 193 | if (match.length !== 4) continue; 194 | // match[1] is 'begin' query part 195 | let beginIdx = match.index + match[1].length; 196 | // match[3] is 'end' query part 197 | let endIdx = match.index + match[0].length - match[3].length; 198 | const name = text.substr(beginIdx, endIdx - beginIdx); 199 | 200 | const uri = await this._linkNameToVSCodeURI(doc, name) 201 | if (uri) { 202 | const toPosition = (v) => doc.positionAt(v); 203 | const [begin, end] = [beginIdx, endIdx].map(toPosition); 204 | const range = new vscode.Range(begin, end); 205 | res.push(new vscode.DocumentLink(range, uri)); 206 | } 207 | } 208 | return res; 209 | } 210 | } 211 | 212 | return LinkProvider; 213 | } 214 | -------------------------------------------------------------------------------- /src/get_lookup_cmd.mjs: -------------------------------------------------------------------------------- 1 | import * as process from 'process'; 2 | import * as util from 'util'; 3 | import * as path from 'path'; 4 | import * as fs from 'fs'; 5 | import * as os from 'os'; 6 | import * as tools from './tools.mjs'; 7 | const p = util.promisify; 8 | 9 | 10 | export default function getLookupCmd(vscode) { 11 | return async () => { 12 | const cfg = vscode.workspace.getConfiguration('xi'); 13 | let xiDir = cfg.lookupPath; 14 | // Something like "~/Documents/PowerShell/xi" on any OS. 15 | if (!xiDir) { 16 | return tools.error(vscode, tools.TXT_NO_LOOKUP_PATH); 17 | } 18 | xiDir = xiDir.replace(/^~/, `${os.homedir()}/`); 19 | xiDir = xiDir.replace(/[\/\\]/g, path.sep); 20 | xiDir = xiDir.replace(/\${env:([^}]*)}/g, (match, p1) => { 21 | return process.env[p1] || "" 22 | }); 23 | try { 24 | const stat = await p(fs.stat)(xiDir); 25 | if (!stat.isDirectory()) { 26 | return tools.error(vscode, tools.TXT_NOT_DIR, xiDir); 27 | } 28 | } 29 | catch (e) { 30 | return tools.error(vscode, tools.TXT_NOT_DIR, xiDir); 31 | } 32 | 33 | let nameSeq = []; 34 | try { 35 | nameSeq = await p(fs.readdir)(xiDir); 36 | } 37 | catch (e) { 38 | return tools.error(vscode, tools.TXT_ERR_READ, xiDir); 39 | } 40 | 41 | nameSeq = nameSeq.filter(v => { 42 | if (v === '.git') return false; 43 | return true; 44 | }); 45 | if (!nameSeq.length) return; 46 | 47 | const name = await vscode.window.showQuickPick(nameSeq); 48 | if (!name) return; 49 | 50 | const uri = vscode.Uri.file(path.join(xiDir, name)); 51 | try { 52 | const doc = await vscode.workspace.openTextDocument(uri); 53 | await vscode.window.showTextDocument(doc); 54 | } 55 | catch (e) { 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/get_open_cmd.mjs: -------------------------------------------------------------------------------- 1 | import getJumpToAnchor from './get_jump_to_anchor.mjs'; 2 | 3 | 4 | export default function getOpenCmd(vscode) { 5 | const jumpToAnchor = getJumpToAnchor(vscode); 6 | 7 | 8 | return function(argmap) { 9 | if (!argmap) return; 10 | 11 | const {file, anchor} = argmap; 12 | // Link like [#foo] to [#foo] anchor in same file. 13 | if (!file && anchor) { 14 | // jump to anchor like [foo#] 15 | jumpToAnchor(`#${anchor}`); 16 | } 17 | // Link like [foo] or [foo#bar]. 18 | else { 19 | const uri = vscode.Uri.file(file); 20 | vscode.workspace.openTextDocument(uri).then(doc => { 21 | vscode.window.showTextDocument(doc).then(() => { 22 | if (!anchor) return; 23 | // Search headings like ` * [link] some header[] [another link] .` 24 | jumpToAnchor(anchor); 25 | }); 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/icon.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grigoryvp/vscode-language-xi/976d1bbb04f56bcf55b0c1f13a6773a8c73fbef7/src/icon.afdesign -------------------------------------------------------------------------------- /src/tools.mjs: -------------------------------------------------------------------------------- 1 | export const TXT_NO_LOOKUP_PATH = `'xi.lookupPath' config not specified`; 2 | export const TXT_NOT_DIR = `'{0}' is not a directory`; 3 | export const TXT_ERR_READ = `Can't read '{0}'`; 4 | 5 | 6 | // Wait some time for VSCode to fully load before using this. 7 | export function debug(vscode, ...msgList) { 8 | if (!this._vscode) { 9 | this._vscode = vscode; 10 | this._channel = this._vscode.window.createOutputChannel("language-xi"); 11 | } 12 | _channel.show(); 13 | _channel.append(`${msgList.join(", ")}\n`); 14 | } 15 | 16 | 17 | // Display error message with interpolation. 18 | export function error(vscode, msg, ...valSeq) { 19 | if (!this._vscode) { 20 | this._vscode = vscode; 21 | this._channel = this._vscode.window.createOutputChannel("language-xi"); 22 | } 23 | // error("{0} not found", "foo") => "foo not found" 24 | for (const [i, val] of Object.entries(valSeq)) { 25 | msg = msg.replace(`{${i}}`, val); 26 | } 27 | _channel.append(`${msg}\n`); 28 | _vscode.window.showErrorMessage(msg); 29 | } 30 | -------------------------------------------------------------------------------- /src/xi-markdown-injection.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": [ 3 | "https://stackoverflow.com/a/76239666/69882" 4 | ], 5 | "fileTypes": [], 6 | "injectionSelector": "L:text.html.markdown", 7 | "patterns": [{ 8 | "include": "#xi-code-block" 9 | }], 10 | "repository": { 11 | "xi-code-block": { 12 | "begin": "(^|\\G)(\\s*)(\\`{3,}|~{3,})\\s*(?i:(xi)(\\s+[^`~]*)?$)", 13 | "name": "markup.fenced_code.block.markdown", 14 | "end": "(^|\\G)(\\2|\\s{0,3})(\\3)\\s*$", 15 | "beginCaptures": { 16 | "3": { 17 | "name": "punctuation.definition.markdown" 18 | }, 19 | "4": { 20 | "name": "fenced_code.block.language.markdown" 21 | }, 22 | "5": { 23 | "name": "fenced_code.block.language.attributes.markdown" 24 | } 25 | }, 26 | "endCaptures": { 27 | "3": { 28 | "name": "punctuation.definition.markdown" 29 | } 30 | }, 31 | "patterns": [{ 32 | "begin": "(^|\\G)(\\s*)(.*)", 33 | "while": "(^|\\G)(?!\\s*([`~]{3,})\\s*$)", 34 | "contentName": "meta.embedded.block.xi", 35 | "patterns": [{ 36 | "include": "text.xi" 37 | }] 38 | }] 39 | } 40 | }, 41 | "scopeName": "markdown.xi.codeblock" 42 | } 43 | -------------------------------------------------------------------------------- /src/xi.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "comment": [ 3 | "TODO: none yet" 4 | ], 5 | "patterns": [{ 6 | "comment": "caption", 7 | "name": "beginning.punctuation.definition.list.markdown.xi", 8 | "match": "^[^ ].* @$", 9 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 10 | }, 11 | 12 | { 13 | "comment": "headers" 14 | }, { 15 | "comment": "h1, keyword, tag name", 16 | "name": "entity.name.function.xi", 17 | "match": "^[^ ].* \\.$", 18 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 19 | }, { 20 | "comment": "h2, callable", 21 | "name": "entity.name.class.xi", 22 | "match": "^ {2}[^ ].* \\.$", 23 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 24 | }, { 25 | "comment": "h3, property", 26 | "name": "constant.character.character-class.regexp.xi", 27 | "match": "^ {4}[^ ].* \\.$", 28 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 29 | }, { 30 | "comment": "h4, type", 31 | "name": "constant.regexp.xi", 32 | "match": "^ {6}[^ ].* \\.$", 33 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 34 | }, { 35 | "comment": "h5, enum, preprocessor, constant", 36 | "name": "keyword.control.xi", 37 | "match": "^ {8}[^ ].* \\.$", 38 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 39 | }, { 40 | "comment": "h6, number", 41 | "name": "invalid.xi", 42 | "match": "^ {10}[^ ].* \\.$", 43 | "captures": {"0": {"patterns": [{"include": "#header-end"}]}} 44 | }, 45 | 46 | { 47 | "comment": "paragraph with code block start", 48 | "match": "^ *([\\.\\?\\!\\*\\#\\+\\-] )*\\|($| .+$|\\{[^\\}]*\\}$|\\{[^\\}]*\\} .+$)", 49 | "captures": { 50 | "0": {"patterns": [{"include": "#code-block"}]}, 51 | "1": {"name": "constant.other.color.rgb-value.xi"} 52 | } 53 | }, 54 | 55 | { 56 | "comment": "paragraph start", 57 | "match": "^ *([\\.\\?\\!\\*\\#\\+\\-]) (.+)", 58 | "captures": { 59 | "1": {"name": "constant.other.color.rgb-value.xi"}, 60 | "2": { 61 | "patterns": [{ 62 | "include": "#http-link" 63 | }, { 64 | "comment": "before 'special', ex |{lng:foo}bar| is code", 65 | "include": "#code-inline" 66 | }, { 67 | "include": "#wikiword" 68 | }, { 69 | "include": "#accent" 70 | }, { 71 | "include": "#parameter" 72 | }, { 73 | "include": "#special" 74 | }] 75 | } 76 | } 77 | }, 78 | 79 | { 80 | "comment": "[http://foo|", 81 | "include": "#http-link" 82 | }, 83 | 84 | { 85 | "comment": "block code sample, spaces followed by '|' and space", 86 | "match": "^ *\\|($| .+$|\\{[^\\}]*\\} .+$)", 87 | "captures": { 88 | "0": {"patterns": [{"include": "#code-block"}]} 89 | } 90 | }, 91 | 92 | { 93 | "comment": "inline code sample, |{descr}code|; before 'special'", 94 | "include": "#code-inline" 95 | }, 96 | 97 | { 98 | "comment": "text between '[' and ']' without adjasted spaces", 99 | "include": "#wikiword" 100 | }, 101 | 102 | { 103 | "comment": "text between '`' without adjasted spaces", 104 | "include": "#accent" 105 | }, 106 | 107 | { 108 | "comment": "text between {} prefixed with a single letter and a space", 109 | "include": "#parameter" 110 | }, 111 | 112 | { 113 | "comment": "text between '|' and '|' without adjasted spaces", 114 | "include": "#special" 115 | }], 116 | 117 | "repository": { 118 | "header-end": { 119 | "patterns": [{ 120 | "comment": "plain header, ex 'foo .'", 121 | "match": "[^ ] (\\.|@)$", 122 | "captures": { 123 | "1": {"name": "punctuation.definition.tag.xi"} 124 | } 125 | },{ 126 | "comment": "header that is itself a link, ex 'foo[] .', no spaces", 127 | "match": "[^ ](\\[\\]) (\\.|@)$", 128 | "captures": { 129 | "1": {"name": "constant.character.xi"}, 130 | "2": {"name": "punctuation.definition.tag.xi"} 131 | } 132 | }, { 133 | "comment": "header with link, ex 'foo [bar] .', space separated", 134 | "match": "[^ ] +(\\[.+\\]) (\\.|@)$", 135 | "captures": { 136 | "1": { 137 | "name": "punctuation.definition.tag.xi", 138 | "patterns": [{ 139 | "match": "\\[(.+)\\]", 140 | "captures": { 141 | "1": {"name": "constant.character.xi"} 142 | } 143 | }] 144 | }, 145 | "2": {"name": "punctuation.definition.tag.xi"} 146 | } 147 | }] 148 | }, 149 | 150 | "special": { 151 | "patterns": [{ 152 | "comment": "text like ||foo||", 153 | "match": "\\|([^ ]+)\\|", 154 | "name": "punctuation.definition.tag.xi", 155 | "captures": {"1": {"name": "beginning.punctuation.definition.quote.markdown.xi"}} 156 | }, { 157 | "comment": "text like |foo bar baz|", 158 | "match": "\\|([^ \\|][^\\|]*[^ \\|])\\|", 159 | "name": "punctuation.definition.tag.xi", 160 | "captures": {"1": {"name": "beginning.punctuation.definition.quote.markdown.xi"}} 161 | }] 162 | }, 163 | 164 | "accent": { 165 | "patterns": [{ 166 | "comment": "`foo bar baz`", 167 | "match": "`([^ `]?)`|`([^ `][^`]*[^ `])`", 168 | "name": "punctuation.definition.tag.xi", 169 | "captures": { 170 | "1": {"name": "accent.xi"}, 171 | "2": {"name": "accent.xi"} 172 | } 173 | }] 174 | }, 175 | 176 | "parameter": { 177 | "patterns": [{ 178 | "comment": "'{i}", 179 | "match": "(\\{)([^ \\}])(\\})", 180 | "name": "constant.other.color.rgb-value.xi", 181 | "captures": { 182 | "1": {"name": "punctuation.definition.tag.xi"}, 183 | "2": {"name": "constant.character.character-class.regexp.xi"}, 184 | "3": {"name": "punctuation.definition.tag.xi"} 185 | } 186 | }, { 187 | "comment": "'{i foo}", 188 | "match": "(\\{)([^ \\}]) [^\\}]+(\\})", 189 | "name": "constant.other.color.rgb-value.xi", 190 | "captures": { 191 | "1": {"name": "punctuation.definition.tag.xi"}, 192 | "2": {"name": "constant.character.character-class.regexp.xi"}, 193 | "3": {"name": "punctuation.definition.tag.xi"} 194 | } 195 | }, { 196 | "comment": "{i(type)}", 197 | "match": "(\\{)([^ \\}])(\\()([^\\)]+)(\\))(\\})", 198 | "name": "constant.other.color.rgb-value.xi", 199 | "captures": { 200 | "1": {"name": "punctuation.definition.tag.xi"}, 201 | "2": {"name": "constant.character.character-class.regexp.xi"}, 202 | "3": {"name": "punctuation.definition.tag.xi"}, 203 | "4": {"name": "beginning.punctuation.definition.quote.markdown.xi"}, 204 | "5": {"name": "punctuation.definition.tag.xi"}, 205 | "6": {"name": "punctuation.definition.tag.xi"} 206 | } 207 | }, { 208 | "comment": "'{i(type) foo}", 209 | "match": "(\\{)([^ \\}])(\\()([^\\)]+)(\\)) [^\\}]+(\\})", 210 | "name": "constant.other.color.rgb-value.xi", 211 | "captures": { 212 | "1": {"name": "punctuation.definition.tag.xi"}, 213 | "2": {"name": "constant.character.character-class.regexp.xi"}, 214 | "3": {"name": "punctuation.definition.tag.xi"}, 215 | "4": {"name": "beginning.punctuation.definition.quote.markdown.xi"}, 216 | "5": {"name": "punctuation.definition.tag.xi"}, 217 | "6": {"name": "punctuation.definition.tag.xi"} 218 | } 219 | }, { 220 | "comment": "Most generic case, last to match: {foo}, {Foo} etc", 221 | "match": "(\\{)[^ \\}][^\\}]+(\\})", 222 | "name": "constant.other.color.rgb-value.xi", 223 | "captures": { 224 | "1": {"name": "punctuation.definition.tag.xi"}, 225 | "2": {"name": "punctuation.definition.tag.xi"} 226 | } 227 | }] 228 | }, 229 | 230 | "wikiword": { 231 | "patterns": [{ 232 | "comment": "[#link-like]", 233 | "match": "\\[(#[^\\]]*)\\]", 234 | "name": "punctuation.definition.tag.xi", 235 | "captures": { 236 | "1": {"name": "constant.character.xi"} 237 | } 238 | }, { 239 | "comment": "[https://link-like]", 240 | "match": "\\[([^:\\]]*://[^\\]]*)\\]", 241 | "name": "punctuation.definition.tag.xi", 242 | "captures": { 243 | "1": {"name": "constant.character.xi"}, 244 | "2": {"name": "constant.character.xi"} 245 | } 246 | }, { 247 | "comment": "[foo bar baz]", 248 | "match": "\\[([^ \\]]?)\\]|\\[([^ \\]][^\\]]*[^ \\]])\\]", 249 | "name": "punctuation.definition.tag.xi", 250 | "captures": { 251 | "1": {"name": "wikiword.xi"}, 252 | "2": {"name": "wikiword.xi"} 253 | } 254 | }] 255 | }, 256 | 257 | "http-link": { 258 | "patterns": [{ 259 | "comment": "|http://foo|", 260 | "match": "\\|(http://[^\\|]+)\\|", 261 | "name": "punctuation.definition.tag.xi", 262 | "captures": {"1": {"name": "constant.character.xi"}} 263 | }, { 264 | "comment": "|https://foo|", 265 | "match": "\\|(https://[^\\|]+)\\|", 266 | "name": "punctuation.definition.tag.xi", 267 | "captures": {"1": {"name": "constant.character.xi"}} 268 | }] 269 | }, 270 | 271 | "code-block": { 272 | "comment": "block code sample, spaces followed by '|' and space", 273 | "patterns": [{ 274 | "match": "^ *([\\.\\?\\!\\*\\#\\+\\-] )*(\\|)(\\{[^\\}]*\\})*(.*)$", 275 | "captures": { 276 | "1": {"name": "constant.other.color.rgb-value.xi"}, 277 | "2": {"name": "punctuation.definition.tag.xi"}, 278 | "3": { 279 | "patterns": [{ 280 | "match": "\\{[^\\}]*\\}", 281 | "name": "punctuation.definition.tag.xi" 282 | }] 283 | }, 284 | "4": {"name": "constant.other.color.rgb-value.xi"} 285 | } 286 | }] 287 | }, 288 | 289 | "code-inline": { 290 | "patterns": [{ 291 | "comment": "inline code sample, |{descr}foo|bar|", 292 | "match": "\\|\\{[^\\}]+\\}([^ ]+)\\|", 293 | "name": "punctuation.definition.tag.xi", 294 | "captures": { 295 | "1": {"name": "constant.other.color.rgb-value.xi"} 296 | } 297 | }, { 298 | "comment": "inline code sample, |{descr}foo bar baz|", 299 | "match": "\\|\\{[^\\}]+\\}([^\\|]+)\\|", 300 | "name": "punctuation.definition.tag.xi", 301 | "captures": { 302 | "1": {"name": "constant.other.color.rgb-value.xi"} 303 | } 304 | }] 305 | } 306 | }, 307 | 308 | "scopeName": "text.xi" 309 | } 310 | -------------------------------------------------------------------------------- /src/xi_parser.mjs: -------------------------------------------------------------------------------- 1 | export function parse(text, cancel) { 2 | return []; 3 | } 4 | 5 | 6 | export class Token { 7 | // Indentation followed by dot and space: marks paragraph begin. 8 | static BEGIN_MARK = 2; 9 | // Space followed by dot and EOL: marks heading end. 10 | static END_MARK = 3; 11 | // Anything else. 12 | static TEXT = 4; 13 | 14 | 15 | constructor() { 16 | this.type = null; 17 | this.pos = 0; 18 | this.indent = 0; 19 | this.text = ""; 20 | } 21 | } 22 | 23 | 24 | // Parse text into tokens which has position within text. Tokens before 25 | // and after of the current one are analyzed in order to get meaning 26 | // from the grammar. Tokens should be organized a way so no knowledge of 27 | // previous or next token is required to determine current token. 28 | export function tokenize(text, cancel) { 29 | const STR_BEGIN = 2; 30 | const STR_INDENT = 3; 31 | const BEGIN_MARK_WAIT_SPACE = 4; 32 | const END_MARK_WAIT_DOT = 5; 33 | const END_MARK_WAIT_EOL = 6; 34 | const TEXT = 7; 35 | const EOF = null; 36 | let char = ""; 37 | let mode = STR_BEGIN; 38 | let pos = -1; 39 | let acc = ""; 40 | let token = null; 41 | let tokens = []; 42 | 43 | while(pos < text.length && !cancel.isCancellationRequested) { 44 | pos += 1; 45 | if (pos < text.length) { 46 | char = text[pos]; 47 | } 48 | else { 49 | char = EOF; 50 | } 51 | switch(char) { 52 | case " ": 53 | acc += char; 54 | switch(mode) { 55 | case STR_BEGIN: 56 | case STR_INDENT: 57 | // If string starts with spaces it can be a paragraph indent 58 | mode = STR_INDENT; 59 | break; 60 | case BEGIN_MARK_WAIT_SPACE: 61 | token = new Token(); 62 | token.text = acc; 63 | token.pos = pos - (acc.length - 1); 64 | token.type = Token.BEGIN_MARK; 65 | tokens.push(token); 66 | acc = ""; 67 | mode = TEXT; 68 | break; 69 | case TEXT: 70 | mode = END_MARK_WAIT_DOT; 71 | break; 72 | default: 73 | mode = TEXT; 74 | break; 75 | } 76 | break; 77 | case ".": 78 | acc += char; 79 | switch(mode) { 80 | case STR_INDENT: 81 | const indentLen = acc.length - 1; 82 | if (indentLen % 2 == 0) { 83 | mode = BEGIN_MARK_WAIT_SPACE; 84 | } 85 | else { 86 | mode = TEXT; 87 | } 88 | break; 89 | case END_MARK_WAIT_DOT: 90 | mode = END_MARK_WAIT_EOL; 91 | break; 92 | default: 93 | mode = TEXT; 94 | break; 95 | } 96 | break; 97 | case "\r": 98 | case "\n": 99 | case EOF: 100 | switch(mode) { 101 | case END_MARK_WAIT_EOL: 102 | console.assert(acc.length > 2); 103 | const endMarkLen = 2; 104 | token = new Token(); 105 | token.text = acc.substr(0, acc.length - endMarkLen); 106 | const eolPosOffset = 1; 107 | token.pos = pos - eolPosOffset - (acc.length - 1); 108 | token.type = Token.TEXT; 109 | tokens.push(token); 110 | token = new Token(); 111 | token.text = " ."; 112 | token.pos = pos - eolPosOffset - 1; 113 | token.type = Token.END_MARK; 114 | tokens.push(token); 115 | acc = ""; 116 | mode = TEXT; 117 | break; 118 | default: 119 | if (acc.length) { 120 | token = new Token(); 121 | token.text = acc; 122 | const eolPosOffset = 1; 123 | token.pos = pos - eolPosOffset - (acc.length - 1); 124 | token.type = Token.TEXT; 125 | tokens.push(token); 126 | } 127 | mode = STR_BEGIN; 128 | acc = ""; 129 | break; 130 | } 131 | default: 132 | mode = TEXT; 133 | acc += char; 134 | break; 135 | } 136 | } 137 | 138 | return tokens; 139 | } 140 | -------------------------------------------------------------------------------- /test/editor_change_handler.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getHandler from './../src/get_editor_change_handler.mjs'; 4 | 5 | 6 | describe("'active editor changed' handler", () => { 7 | const state = {}; 8 | const ctx = { 9 | globalState: { 10 | get: (key) => state[key], 11 | update: (key, val) => state[key] = val, 12 | }, 13 | document: { 14 | fileName: 'foo.xi' 15 | } 16 | }; 17 | 18 | 19 | it("adds document to history", () => { 20 | const handler = getHandler(ctx, 'key'); 21 | handler(ctx); 22 | expect(state).deep.includes({key: ['foo.xi']}); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/fold_provider.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getFoldProvider from './../src/get_fold_provider.mjs'; 4 | 5 | 6 | describe("FoldProvider class", () => { 7 | 8 | const vscode = { 9 | FoldingRange: (start, end, kind) => {start, end, kind}, 10 | FoldingRangeKind: { 11 | Region: 3, 12 | }, 13 | }; 14 | 15 | 16 | it("can be instantiated", () => { 17 | const FoldProvider = getFoldProvider(vscode); 18 | const inst = new FoldProvider(); 19 | expect(inst).to.be.ok; 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/go_back_cmd.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getCmd from './../src/get_go_back_cmd.mjs'; 4 | 5 | 6 | describe("'go back' command", () => { 7 | const state = {key: ['foo.xi', 'bar.xi']}; 8 | const vscode = { 9 | Uri: { 10 | file: (path) => ({path}), 11 | parse: (text) => ({text}), 12 | }, 13 | workspace: { 14 | openTextDocument: (uri) => new Promise((resolve) => resolve({uri})), 15 | }, 16 | window: { 17 | showTextDocument: (doc) => {}, 18 | }, 19 | }; 20 | const ctx = { 21 | globalState: { 22 | get: (key) => state[key], 23 | update: (key, val) => state[key] = val, 24 | }, 25 | document: { 26 | fileName: 'foo.xi' 27 | } 28 | }; 29 | 30 | 31 | it("gets document from history", () => { 32 | const cmd = getCmd(vscode, ctx, 'key'); 33 | vscode.window.showTextDocument = (doc) => { 34 | expect(doc).deep.includes({uri: {path: 'foo.xi'}}); 35 | }; 36 | cmd(ctx); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/jump_test.xi: -------------------------------------------------------------------------------- 1 | jump test @ 2 | 3 | . [test]. 4 | . [does not exist]. 5 | -------------------------------------------------------------------------------- /test/jump_to_anchor.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getJumpToAnchor from './../src/get_jump_to_anchor.mjs'; 4 | 5 | 6 | describe("jump to anchor", () => { 7 | const vscode = { 8 | Range: function(...v) { [this.begin, this.end] = [...v]; }, 9 | Selection: function(...v) { [this.begin, this.end] = [...v]; }, 10 | window: { 11 | activeTextEditor: { 12 | document: { 13 | getText: () => '', 14 | positionAt: (v) => v, 15 | }, 16 | revealRange: () => {}, 17 | } 18 | }, 19 | TextEditorRevealType: { 20 | InCenter: 1, 21 | }, 22 | }; 23 | 24 | 25 | it("jumps to anchor", () => { 26 | const jumpToAnchor = getJumpToAnchor(vscode); 27 | vscode.window.activeTextEditor.document.getText = () => { 28 | return `\nfoo .`; 29 | } 30 | vscode.window.activeTextEditor.revealRange = (range) => { 31 | expect(range).deep.includes({begin: 1, end: 1}); 32 | } 33 | jumpToAnchor('foo'); 34 | }); 35 | 36 | 37 | it("jumps to self document anchor", () => { 38 | const jumpToAnchor = getJumpToAnchor(vscode); 39 | vscode.window.activeTextEditor.document.getText = () => { 40 | return `\n[foo#]`; 41 | } 42 | vscode.window.activeTextEditor.revealRange = (range) => { 43 | expect(range).deep.includes({begin: 1, end: 1}); 44 | } 45 | jumpToAnchor('#foo'); 46 | }); 47 | 48 | 49 | it("jumps to nested anchor", () => { 50 | const jumpToAnchor = getJumpToAnchor(vscode); 51 | vscode.window.activeTextEditor.document.getText = () => { 52 | return `\nfoo .\n bar .`; 53 | } 54 | vscode.window.activeTextEditor.revealRange = (range) => { 55 | expect(range).deep.includes({begin: 7, end: 7}); 56 | } 57 | jumpToAnchor('foo#bar'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/link_provider.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getLinkProvider from './../src/get_link_provider.mjs'; 4 | 5 | 6 | describe("LinkProvider class", async () => { 7 | 8 | const vscode = { 9 | Range: function(...v) { [this.begin, this.end] = [...v]; }, 10 | DocumentLink: function(...v) { [this.range, this.uri] = [...v]; }, 11 | Uri: { 12 | file: (path) => ({path}), 13 | parse: (text) => ({text}), 14 | Utils: { 15 | dirname: (path) => "", 16 | joinPath: (...items) => items.filter(v => v).join("/") 17 | } 18 | }, 19 | workspace: { 20 | getConfiguration: () => { 21 | return { 22 | debug: false, 23 | }; 24 | }, 25 | } 26 | }; 27 | 28 | const doc = { 29 | getText: () => '', 30 | fileName: 'foo.xi', 31 | positionAt: (v) => v, 32 | }; 33 | 34 | const cancel = {}; 35 | 36 | 37 | it("can be instantiated", () => { 38 | const LinkProvider = getLinkProvider(vscode); 39 | const inst = new LinkProvider(); 40 | expect(inst).to.be.ok; 41 | }); 42 | 43 | 44 | it("matches simple link", async () => { 45 | const LinkProvider = getLinkProvider(vscode); 46 | const inst = new LinkProvider(); 47 | doc.getText = () => "[a]"; 48 | const ret = await inst.provideDocumentLinks(doc, cancel); 49 | expect(ret).to.have.lengthOf(1); 50 | const link = ret[0]; 51 | expect(link).deep.includes({range: {begin: 1, end: 2}}); 52 | expect(link).deep.includes({uri: {path: "a.xi"}}); 53 | }); 54 | 55 | 56 | it("lowercase file names for links", async () => { 57 | const LinkProvider = getLinkProvider(vscode); 58 | const inst = new LinkProvider(); 59 | doc.getText = () => "[Ab]"; 60 | const ret = await inst.provideDocumentLinks(doc, cancel); 61 | expect(ret).to.have.lengthOf(1); 62 | const link = ret[0]; 63 | expect(link).deep.includes({range: {begin: 1, end: 3}}); 64 | expect(link).deep.includes({uri: {path: "ab.xi"}}); 65 | }); 66 | 67 | 68 | it("matches simple header link", async () => { 69 | const LinkProvider = getLinkProvider(vscode); 70 | const inst = new LinkProvider(); 71 | doc.getText = () => "foo[] ."; 72 | const ret = await inst.provideDocumentLinks(doc, cancel); 73 | expect(ret).to.have.lengthOf(1); 74 | const link = ret[0]; 75 | expect(link).deep.includes({range: {begin: 0, end: 3}}); 76 | }); 77 | 78 | 79 | it("matches simple header link with offset", async () => { 80 | const LinkProvider = getLinkProvider(vscode); 81 | const inst = new LinkProvider(); 82 | doc.getText = () => " foo[] ."; 83 | const ret = await inst.provideDocumentLinks(doc, cancel); 84 | expect(ret).to.have.lengthOf(1); 85 | const link = ret[0]; 86 | expect(link).deep.includes({range: {begin: 2, end: 5}}); 87 | }); 88 | 89 | 90 | it("matches header link for wikiword at start", async () => { 91 | const LinkProvider = getLinkProvider(vscode); 92 | const inst = new LinkProvider(); 93 | doc.getText = () => "[foo] bar[] ."; 94 | const ret = await inst.provideDocumentLinks(doc, cancel); 95 | expect(ret).to.have.lengthOf(2); 96 | const link = ret[1]; 97 | expect(link).deep.includes({range: {begin: 6, end: 9}}); 98 | }); 99 | 100 | 101 | it("matches header link for wikiword at end", async () => { 102 | const LinkProvider = getLinkProvider(vscode); 103 | const inst = new LinkProvider(); 104 | doc.getText = () => "foo[] [bar] ."; 105 | const ret = await inst.provideDocumentLinks(doc, cancel); 106 | expect(ret).to.have.lengthOf(2); 107 | const link = ret[1]; 108 | expect(link).deep.includes({range: {begin: 0, end: 3}}); 109 | }); 110 | 111 | 112 | it("matches header link to anchor", async () => { 113 | const LinkProvider = getLinkProvider(vscode); 114 | const inst = new LinkProvider(); 115 | doc.getText = () => "#foo[] ."; 116 | const ret = await inst.provideDocumentLinks(doc, cancel); 117 | expect(ret).to.have.lengthOf(1); 118 | const link = ret[0]; 119 | expect(link).deep.includes({range: {begin: 0, end: 4}}); 120 | const prefix = 'command:extension.xi.open?'; 121 | const text = prefix + encodeURIComponent(JSON.stringify({ 122 | anchor: "foo", 123 | })); 124 | expect(link).deep.includes({uri: {text}}); 125 | }); 126 | 127 | 128 | it("matches header link to url", async () => { 129 | const LinkProvider = getLinkProvider(vscode); 130 | const inst = new LinkProvider(); 131 | doc.getText = () => "http://foo[] ."; 132 | const ret = await inst.provideDocumentLinks(doc, cancel); 133 | expect(ret).to.have.lengthOf(1); 134 | const link = ret[0]; 135 | expect(link).deep.includes({range: {begin: 0, end: 10}}); 136 | expect(link).deep.includes({uri: {text: 'http://foo'}}); 137 | }); 138 | 139 | 140 | it("not matches inside single line code sample", async () => { 141 | const LinkProvider = getLinkProvider(vscode); 142 | const inst = new LinkProvider(); 143 | doc.getText = () => "|{lng}[a]|"; 144 | const ret = await inst.provideDocumentLinks(doc, cancel); 145 | expect(ret).to.have.lengthOf(0); 146 | }); 147 | 148 | 149 | it("not matches inside multiline code sample", async () => { 150 | const LinkProvider = getLinkProvider(vscode); 151 | const inst = new LinkProvider(); 152 | doc.getText = () => " | foo [a]"; 153 | const ret = await inst.provideDocumentLinks(doc, cancel); 154 | expect(ret).to.have.lengthOf(0); 155 | }); 156 | 157 | 158 | it("not matches inside paragraph code sample", async () => { 159 | const LinkProvider = getLinkProvider(vscode); 160 | const inst = new LinkProvider(); 161 | doc.getText = () => ". | foo [a]"; 162 | const ret = await inst.provideDocumentLinks(doc, cancel); 163 | expect(ret).to.have.lengthOf(0); 164 | }); 165 | 166 | 167 | it("not matches inside marked text", async () => { 168 | const LinkProvider = getLinkProvider(vscode); 169 | const inst = new LinkProvider(); 170 | doc.getText = () => "|foo| |bar| |[baz]|"; 171 | let ret = await inst.provideDocumentLinks(doc, cancel); 172 | expect(ret).to.have.lengthOf(0); 173 | 174 | // Wrong number of pipes before link. 175 | doc.getText = () => "||a| [b]"; 176 | ret = await inst.provideDocumentLinks(doc, cancel); 177 | expect(ret).to.have.lengthOf(1); 178 | const link = ret[0]; 179 | expect(link).deep.includes({range: {begin: 6, end: 7}}); 180 | }); 181 | 182 | 183 | it("matches link with anchor", async () => { 184 | const LinkProvider = getLinkProvider(vscode); 185 | const inst = new LinkProvider(); 186 | doc.getText = () => "[a#b]"; 187 | const ret = await inst.provideDocumentLinks(doc, cancel); 188 | expect(ret).to.have.lengthOf(1); 189 | const link = ret[0]; 190 | expect(link).deep.includes({range: {begin: 1, end: 4}}); 191 | const prefix = 'command:extension.xi.open?'; 192 | const text = prefix + encodeURIComponent(JSON.stringify({ 193 | file: "a.xi", 194 | anchor: "b", 195 | })); 196 | expect(link).deep.includes({uri: {text}}); 197 | }); 198 | 199 | 200 | it("matches link with nested anchor", async () => { 201 | const LinkProvider = getLinkProvider(vscode); 202 | const inst = new LinkProvider(); 203 | doc.getText = () => "[a#b#c]"; 204 | const ret = await inst.provideDocumentLinks(doc, cancel); 205 | expect(ret).to.have.lengthOf(1); 206 | const link = ret[0]; 207 | expect(link).deep.includes({range: {begin: 1, end: 6}}); 208 | const prefix = 'command:extension.xi.open?'; 209 | const text = prefix + encodeURIComponent(JSON.stringify({ 210 | file: "a.xi", 211 | anchor: "b#c", 212 | })); 213 | expect(link).deep.includes({uri: {text}}); 214 | }); 215 | 216 | 217 | it("matches link to anchor", async () => { 218 | const LinkProvider = getLinkProvider(vscode); 219 | const inst = new LinkProvider(); 220 | doc.getText = () => "[#b]"; 221 | const ret = await inst.provideDocumentLinks(doc, cancel); 222 | expect(ret).to.have.lengthOf(1); 223 | const link = ret[0]; 224 | expect(link).deep.includes({range: {begin: 1, end: 3}}); 225 | const prefix = 'command:extension.xi.open?'; 226 | const text = prefix + encodeURIComponent(JSON.stringify({ 227 | anchor: "b" 228 | })); 229 | expect(link).deep.includes({uri: {text}}); 230 | }); 231 | 232 | 233 | it("matches http link", async () => { 234 | const LinkProvider = getLinkProvider(vscode); 235 | const inst = new LinkProvider(); 236 | doc.getText = () => "[http://foo]"; 237 | const ret = await inst.provideDocumentLinks(doc, cancel); 238 | expect(ret).to.have.lengthOf(1); 239 | const link = ret[0]; 240 | expect(link).deep.includes({range: {begin: 1, end: 11}}); 241 | expect(link).deep.includes({uri: {text: 'http://foo'}}); 242 | }); 243 | 244 | it("matches link anchors with slashes", async () => { 245 | const LinkProvider = getLinkProvider(vscode); 246 | const inst = new LinkProvider(); 247 | doc.getText = () => "[a#b/#c/]"; 248 | const ret = await inst.provideDocumentLinks(doc, cancel); 249 | expect(ret).to.have.lengthOf(1); 250 | const link = ret[0]; 251 | expect(link).deep.includes({range: {begin: 1, end: 8}}); 252 | const prefix = 'command:extension.xi.open?'; 253 | const text = prefix + encodeURIComponent(JSON.stringify({ 254 | file: "a.xi", 255 | anchor: "b/#c/", 256 | })); 257 | expect(link).deep.includes({uri: {text}}); 258 | }); 259 | 260 | it("not matches between marked text", async () => { 261 | const LinkProvider = getLinkProvider(vscode); 262 | const inst = new LinkProvider(); 263 | doc.getText = () => "|[| |]]|"; 264 | const ret = await inst.provideDocumentLinks(doc, cancel); 265 | expect(ret).to.have.lengthOf(0); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /test/open_cmd.mjs: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | const expect = chai.expect; 3 | import getCmd from './../src/get_open_cmd.mjs'; 4 | 5 | 6 | describe("'open' command", () => { 7 | const vscode = { 8 | Uri: { 9 | file: (path) => ({path}), 10 | }, 11 | workspace: { 12 | openTextDocument: (uri) => new Promise((resolve) => resolve({uri})), 13 | }, 14 | window: { 15 | showTextDocument: (doc) => new Promise((resolve) => resolve()), 16 | }, 17 | }; 18 | 19 | 20 | it("opens a link", () => { 21 | const cmd = getCmd(vscode); 22 | vscode.window.showTextDocument = (doc) => { 23 | expect(doc).deep.includes({uri: {path: 'foo.xi'}}); 24 | return new Promise((resolve) => resolve()); 25 | } 26 | cmd({file: 'foo.xi'}); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/test.md: -------------------------------------------------------------------------------- 1 | ```python 2 | a = 1 # comment 3 | ``` 4 | ```xi 5 | foo . 6 | bar . 7 | ``` 8 | -------------------------------------------------------------------------------- /test/test.xi: -------------------------------------------------------------------------------- 1 | xi test @ 2 | 3 | Todo . 4 | #todo[] . 5 | # Links are not working in this test. 6 | # '#' list symbol is not working. 7 | 8 | . Text with {i input param}, {- cli param}, {i(typed) param}, {i} single 9 | param, single {i(typed)} param, {param} without i/o modifier and 10 | {Upper-case param}. 11 | 12 | ||| 13 | [http://foo] 14 | http://foo[] . 15 | [https://foo] 16 | ||| text 17 | text ||| 18 | text ||| text 19 | ||special|| 20 | text ||special|| 21 | ||special|| text 22 | text ||special|| text 23 | |special with spaces| 24 | text |special with spaces| text 25 | text | text 26 | text | text | 27 | text | text | text 28 | text |{lng:foo}not a special but language| text 29 | . text |{lng:foo}not a special but language| text 30 | . |{}special|. 31 | . |{}| this is special. 32 | . ||| 33 | . ||| text 34 | . ||special|| 35 | . ||special|| text 36 | . text ||| 37 | . text ||| text 38 | . text ||special|| 39 | . text ||special|| text 40 | . [http://foo] 41 | . [https://foo] 42 | | code 43 | |{Lng:foo} code 44 | | {lng:foo} code 45 | | code 46 | . | code 47 | . |{lng:foo} code 48 | |{Lng:foo} code 49 | |{lng:foo} 50 | | 51 | . | {lng:foo} code 52 | . |not a code 53 | ! | code 54 | ? | code 55 | . text | not a code 56 | [0.0...1.0) range, not a link. 57 | . |{}| non-text item (no meta). 58 | . |{| and |}| two non-text items. 59 | . |{x}| Non-text item, not a language specifier. 60 | . |{x}|: Non-text item and |:|. 61 | |{lng:py}a = 1| inline code with spaces, one per line, no '|'. 62 | |{lng:py}a=1| inline code without spaces, |{lng:py}a=1|2|. 63 | . |{ng:py}a = 1|: inline code after paragraph. 64 | . |[notlink]|. 65 | . |{lng:js}[notlink]|. 66 | . |{lng:js} 67 | | [notlink] 68 | . | [notlink] 69 | [jump test] 70 | [c] - single wikiword. 71 | kh1 . 72 | h1 link[] . 73 | h1 local link [#1] . 74 | h1 wikiword link [wikiword] . 75 | [wikiword] h1 wikiword link . 76 | [wikiword] h1 wikiword link [wikiword] . 77 | [wikiword] h1 wikiword link[] [wikiword] . 78 | . Paragraph. 79 | h2 . 80 | h2 link[] . 81 | h2 local link [#1] . 82 | h2 wikiword link [wikiword] . 83 | . Paragraph. 84 | h3 . 85 | h3 link[] . 86 | h3 local link [#1] . 87 | h3 wikiword link [wikiword] . 88 | . Paragraph. 89 | h4 . 90 | h4 link[] . 91 | h4 local link [#1] . 92 | h4 wikiword link [wikiword] . 93 | . Paragraph. 94 | h5 . 95 | h5 link[] . 96 | h5 local link [#1] . 97 | h5 wikiword link [wikiword] . 98 | . Paragraph. 99 | h6 . 100 | h6 link[] . 101 | h5 local link [#1] . 102 | h6 wikiword link [wikiword] . 103 | . Paragraph. 104 | . | dot at end is always header . 105 | . dot at end is always header $ . 106 | not a link [] . 107 | paragraph$ . 108 | wrong color of following square braces [] . 109 | wrong header color $ . 110 | [] . 111 | . Above is h1. 112 | [] . 113 | . Above is h2. 114 | | . text . 115 | . Above is h3. 116 | . [todo#] anchor. 117 | -------------------------------------------------------------------------------- /test/xi_parser.mjs: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs/promises'; 2 | import * as chai from 'chai'; 3 | const expect = chai.expect; 4 | 5 | import * as parser from './../src/xi_parser.mjs'; 6 | 7 | 8 | describe("Xi grammar parsing", () => { 9 | 10 | let _sample = null; 11 | const _cancel = { 12 | isCancellationRequested: false, 13 | }; 14 | 15 | 16 | before("load sample", async () => { 17 | _sample = await fs.readFile('./test/test.xi', 'utf-8'); 18 | }); 19 | 20 | 21 | it("starts parsing", async () => { 22 | parser.parse(_sample).exist; 23 | }); 24 | 25 | 26 | it("tokenisez input", async () => { 27 | parser.tokenize(_sample, _cancel).exist; 28 | }); 29 | 30 | 31 | it("finds begin mark token", async () => { 32 | const tokens = parser.tokenize(" . foo", _cancel); 33 | expect(tokens).length(2); 34 | expect(tokens[0]).contains({type: parser.Token.BEGIN_MARK, pos: 0}); 35 | expect(tokens[1]).contains({type: parser.Token.TEXT, pos: 4}); 36 | }); 37 | 38 | 39 | it("finds end mark token", async () => { 40 | const tokens = parser.tokenize("foo .", _cancel); 41 | expect(tokens).length(2); 42 | expect(tokens[0]).contains({type: parser.Token.TEXT, pos: 0}); 43 | expect(tokens[1]).contains({type: parser.Token.END_MARK, pos: 3}); 44 | }); 45 | 46 | 47 | it("has indent info for end mark", async () => { 48 | // WIP 49 | return 50 | tokens = parser.tokenize(" foo .", _cancel); 51 | expect(tokens).length(3); 52 | expect(tokens[0]).contains({type: parser.Token.INDENT, pos: 0}); 53 | expect(tokens[0]).contains({type: parser.Token.TEXT, pos: 2}); 54 | expect(tokens[1]).contains({type: parser.Token.END_MARK, pos: 5}); 55 | }); 56 | 57 | 58 | it("has indent info for begin mark", async () => { 59 | // WIP 60 | return 61 | tokens = parser.tokenize(" . foo", _cancel); 62 | expect(tokens).length(3); 63 | expect(tokens[0]).contains({type: parser.Token.INDENT, pos: 0}); 64 | expect(tokens[0]).contains({type: parser.Token.BEGIN_MARK, pos: 2}); 65 | expect(tokens[0]).contains({type: parser.Token.TEXT, pos: 4}); 66 | }); 67 | }); 68 | --------------------------------------------------------------------------------