├── .eslintrc.json ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .vsixmanifest ├── LICENSE ├── README.md ├── images ├── z80-macroasm-completion-demo.gif ├── z80-macroasm-completion.png ├── z80-macroasm-definition-peek.gif ├── z80-macroasm-definition.png ├── z80-macroasm-hover.gif └── z80-macroasm-rename.gif ├── language.configuration.json ├── logo.png ├── package.json ├── snippets └── z80-macroasm.json ├── src ├── completionProvider.ts ├── configProperties.ts ├── definitionProvider.ts ├── defs_list.ts ├── defs_regex.ts ├── extension.ts ├── formatProcessor.ts ├── hover.ts ├── renameProvider.ts ├── symbolProcessor.ts ├── symbolProvider.ts └── utils.ts ├── syntaxes └── z80-macroasm.tmLanguage.json ├── tsconfig.json └── vsc-extension-quickstart.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true 4 | }, 5 | "root": true, 6 | "parser": "@typescript-eslint/parser", 7 | "parserOptions": { 8 | "project": "./tsconfig.json", 9 | "tsconfigRootDir": ".", 10 | "sourceType": "module", 11 | "ecmaVersion": 2021 12 | }, 13 | "plugins": [ 14 | "@typescript-eslint" 15 | ], 16 | "extends": [ 17 | "plugin:@typescript-eslint/recommended" 18 | ], 19 | "rules": { 20 | "indent": [ 21 | "error", 22 | "tab", 23 | { 24 | "SwitchCase": 1, 25 | "ignoreComments": true 26 | } 27 | ], 28 | "getter-return": "error", 29 | "no-compare-neg-zero": "error", 30 | "no-cond-assign": "error", 31 | "no-console": [ 32 | "warn", 33 | { 34 | "allow": [ 35 | "warn", 36 | "error" 37 | ] 38 | } 39 | ], 40 | "no-constant-condition": "error", 41 | "no-control-regex": "warn", 42 | "no-debugger": "warn", 43 | "no-dupe-args": "error", 44 | "no-dupe-keys": "error", 45 | "no-duplicate-case": "error", 46 | "no-empty": [ 47 | "error", 48 | { 49 | "allowEmptyCatch": true 50 | } 51 | ], 52 | "no-empty-character-class": "error", 53 | "no-ex-assign": "error", 54 | "no-extra-boolean-cast": "error", 55 | "no-extra-parens": "off", 56 | "no-extra-semi": "warn", 57 | "no-func-assign": "error", 58 | "no-inner-declarations": "error", 59 | "no-invalid-regexp": "error", 60 | "no-irregular-whitespace": "error", 61 | "no-regex-spaces": "error", 62 | "no-sparse-arrays": "error", 63 | "no-template-curly-in-string": "off", 64 | "no-unexpected-multiline": "error", 65 | "no-unreachable": "error", 66 | "no-unsafe-negation": "error", 67 | "valid-typeof": "error", 68 | "curly": "error", 69 | "eqeqeq": [ 70 | "error", 71 | "smart" 72 | ], 73 | "key-spacing": [ 74 | "warn", 75 | { 76 | "beforeColon": false, 77 | "afterColon": true 78 | } 79 | ], 80 | "max-classes-per-file": "off", 81 | "no-alert": "error", 82 | "no-fallthrough": "error", 83 | "no-floating-decimal": "off", 84 | "no-labels": "error", 85 | "no-lone-blocks": "error", 86 | "no-mixed-spaces-and-tabs": "error", 87 | "no-multi-spaces": [ 88 | "warn", 89 | { 90 | "ignoreEOLComments": true 91 | } 92 | ], 93 | "no-param-reassign": "off", 94 | "no-return-assign": [ 95 | "error", 96 | "except-parens" 97 | ], 98 | "no-self-assign": "error", 99 | "no-self-compare": "error", 100 | "no-spaced-func": "warn", 101 | "no-trailing-spaces": "error", 102 | "no-unmodified-loop-condition": "error", 103 | "no-unused-expressions": "off", 104 | "no-useless-concat": "error", 105 | "no-warning-comments": "off", 106 | "no-whitespace-before-property": "error", 107 | "no-nested-ternary": "error", 108 | "object-curly-spacing": [ 109 | "error", 110 | "always" 111 | ], 112 | "quotes": [ 113 | "error", 114 | "single", 115 | { 116 | "avoidEscape": true 117 | } 118 | ], 119 | "radix": "off", 120 | "semi": [ 121 | "error", 122 | "always", 123 | { 124 | "omitLastInOneLineBlock": true 125 | } 126 | ], 127 | "semi-spacing": [ 128 | "error", 129 | { 130 | "before": false, 131 | "after": true 132 | } 133 | ], 134 | "semi-style": "error", 135 | "space-before-blocks": [ 136 | "error", 137 | "always" 138 | ], 139 | "space-before-function-paren": [ 140 | "error", 141 | { 142 | "anonymous": "never", 143 | "named": "never", 144 | "asyncArrow": "always" 145 | } 146 | ], 147 | "space-infix-ops": "error", 148 | "block-spacing": "error", 149 | "brace-style": [ 150 | "error", 151 | "stroustrup", 152 | { 153 | "allowSingleLine": true 154 | } 155 | ], 156 | "camelcase": "off", 157 | "constructor-super": "error", 158 | "no-const-assign": "error", 159 | "no-this-before-super": "error", 160 | "prefer-const": "warn", 161 | "no-var": "error", 162 | 163 | "@typescript-eslint/class-literal-property-style": "error", 164 | "@typescript-eslint/explicit-module-boundary-types": "off", 165 | "@typescript-eslint/explicit-member-accessibility": "off", 166 | "@typescript-eslint/explicit-function-return-type": [ 167 | "off", 168 | { 169 | "allowExpressions": true 170 | } 171 | ], 172 | "@typescript-eslint/member-ordering": "off", 173 | "@typescript-eslint/no-inferrable-types": "off", 174 | "@typescript-eslint/no-explicit-any": "off", 175 | "@typescript-eslint/no-empty-function": "off", 176 | "@typescript-eslint/no-non-null-assertion": "off", 177 | "@typescript-eslint/no-this-alias": "error", 178 | "@typescript-eslint/no-unused-vars": "warn", 179 | "@typescript-eslint/no-unused-expressions": "warn", 180 | "@typescript-eslint/no-useless-constructor": "error", 181 | "@typescript-eslint/type-annotation-spacing": [ 182 | "warn", 183 | { 184 | "before": false, 185 | "after": true, 186 | "overrides": { 187 | "arrow": { 188 | "before": true, 189 | "after": true 190 | } 191 | } 192 | } 193 | ] 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /out 3 | /package-lock.json 4 | /yarn.lock 5 | 6 | # OS generated files # 7 | ###################### 8 | .DS_Store 9 | .DS_Store? 10 | ._* 11 | .Spotlight-V100 12 | .Trashes 13 | ehthumbs.db 14 | Thumbs.db 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": [ 11 | "--disable-extensions", 12 | "--extensionDevelopmentPath=${workspaceRoot}" 13 | ], 14 | "stopOnEntry": false, 15 | "sourceMaps": true, 16 | "outFiles": [ "${workspaceRoot}/out/src/**" ], 17 | "preLaunchTask": "npm: watch" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 4, 3 | "editor.insertSpaces": false, 4 | "editor.rulers": [120], 5 | "files.eol": "\n", 6 | 7 | "[json]": { 8 | "editor.tabSize": 2, 9 | "editor.insertSpaces": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | test/** 4 | src/** 5 | **/*.map 6 | .vsixmanifest 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | -------------------------------------------------------------------------------- /.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Z80 Macro-Assembler 6 | Support for Z80 macro-assemblers in Visual Studio Code 7 | assembly,macro,assembler,Zilog,Z80,Sinclair,ZX-Spectrum,Amstrad,CPC 8 | Programming Languages 9 | Public 10 | extension/LICENSE 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Imanol Barriuso (Imanolea) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Support for Z80 macro-assemblers in Visual Studio Code 2 | 3 | The **Z80 Macro-Assembler** extension for Visual Studio Code provides the following features inside VS Code: 4 | 5 | * syntax highlighting for Z80 assembly sources of well known Z80 macro-assemblers, for example: 6 | - [SjASM](http://www.xl2s.tk/) or [SjASMPlus](https://github.com/z00m128/sjasmplus) 7 | - [Macroassembler AS](http://john.ccac.rwth-aachen.de:8000/as/) 8 | - [Pasmo](http://pasmo.speccy.org/) 9 | - [rasm](http://www.roudoudou.com/rasm/) 10 | - [tniASM](http://www.tni.nl/products/tniasm.html) (v0.x series) 11 | * [problem matchers](#problem-matchers) for **SjASMPlus**, **Macroassembler AS**, **Pasmo**, **rasm** and **tniASM** compilation output 12 | * label or symbol [defintion documentation](#definitions), suggestions on hover or typing 13 | * macro documentation and argument definition suggestions 14 | * semi-automatic [completition](#completion) with formatting 15 | * [renaming](#renaming) of labels or symbols 16 | * [formatting](#formatter) of block or whole document (experimental feature) 17 | * snippets for macros and source control keywords 18 | 19 | ## ⚙️ Settings 20 | 21 | These few options allows you to configure extension's behavior but primarily your code-formatting preferences and code completion: 22 | 23 | - `z80-macroasm.files.exclude` - Choose files or directories to exclude _(e.g `'**/*.{lst}'`)_. 24 | - `z80-macroasm.files.include` - Files to include and work with. If you, or your macro-assembler using a different conventions of source file extensions then change it here. 25 | > default: `"**/*.{a80,asm,inc,s}"` 26 | - `z80-macroasm.seekSymbolsThroughWorkspace` - If true, extension will crawl through all your workspace files to document all symbols. Otherwise, only includes are taken into account. 27 | > default: `false` 28 | - `z80-macroasm.suggestOnInstructions` - Extension will suggest also basic instruction mnemonics for the auto-completion, not only instruction arguments. 29 | > default: `false` 30 | - `z80-macroasm.suggestCommitWithCustomKeys` - Extension will commit suggesttions with extension's custom keys (enter, tab, or comma). Otherwise, only predefined shortcut will commit suggestion. 31 | > default: `false` 32 | 33 | ### Formatter: 34 | - `z80-macroasm.format.enabled` - Turn on the experimental feature of format on-type or on-save. 35 | > default: `false` 36 | - `z80-macroasm.format.baseIndent` - How many tabstops you prefer before the instructions or keywords. 37 | > default: `2` 38 | - `z80-macroasm.format.controlIndent` - How many tabstops you prefer before the control structure keywords, selections, modules, or blocks. 39 | > default: `1` 40 | - `z80-macroasm.format.whitespaceAfterInstruction` - Which type of whitespace you want to put after the instruction - `"tab"`, `"single-space"` or `"auto"` which autodetect file's tabstop type and width. 41 | > default: `"auto"` 42 | - `z80-macroasm.format.spaceAfterArgument` - If you want to put a single space character after comma (instruction's argument). 43 | > default: `false` 44 | - `z80-macroasm.format.spaceAfterInstruction` - If you want to put a single space character after instruction (before colon separator). 45 | > default: `true` 46 | - `z80-macroasm.format.spacesAroundOperators` - If you want to wrap an operators with spaces. 47 | > default: `false` 48 | - `z80-macroasm.format.uppercaseKeywords` - If true, uppercase all keywords, instructions and registers. False means all lowercased and `"auto"` tries to auto-detect your code-style while typing. 49 | > default: `"auto"` 50 | - `z80-macroasm.format.bracketType` - Define which type of brackets around the instruction's arguments (pointers) you prefer: `(round)` or `[square]`. 51 | > default: `"no-change"` 52 | - `z80-macroasm.format.colonAfterLabels` - Put colon after each label or symbol (true or false, `"no-change"` keeps it untouched). 53 | - `z80-macroasm.format.hexaNumberStyle` - Define which hexadecimal number format you prefer to reformat: 54 | >+ **"no-change"** - no reformat happen (default) 55 | >+ **"hash"**: `#1fff` | `#B45D` 56 | >+ **"motorola"**: `$1fff` | `$B45D` 57 | >+ **"intel"**: `1fffh` | `0B45Dh` 58 | >+ **"intel-uppercase"**: `1fffH` | `0B45DH` 59 | >+ **"c-style"**: `0x1fff` | `0xB45D` 60 | - `z80-macroasm.format.hexaNumberCase` - When reformatting of hexadecimal numbers was enabled, whether it's to be additional case processing applied when `true` means uppercased, `false` lowercased. 61 | > default: `"no-change"` 62 | - `z80-macroasm.format.splitInstructionsByColon` - Split colon separated instructions to lines. 63 | > default: `true` 64 | 65 | These keys/values can be used in your workspace or global `settings.json`. 66 | [See example »»](https://github.com/mborik/z80-macroasm-vscode/wiki/settings.json) 67 | 68 | 69 | ## 🚨 Problem matchers 70 | 71 | There are some predefined problem matchers to handle reported errors from compilation output: 72 | - `errmatcher-as` for **Macroassembler AS** 73 | - `errmatcher-sjasmplus` for **SjASMPlus** 74 | - `errmatcher-sjasm` for **SjASM** 75 | - `errmatcher-pasmo` for **Pasmo** 76 | - `errmatcher-rasm` for **rasm** 77 | - `errmatcher-tniasm` and `errmatcher-tniasm-preprocessor` for **tniASM** 78 | 79 | These values can be used in `.vscode/tasks.json` of your project's build task. 80 | [See example »»](https://github.com/mborik/z80-macroasm-vscode/wiki/tasks.json) 81 | 82 | 83 | ## 💡 IntelliSense showcase 84 | 85 | ### Symbol suggestions: 86 | - provide symbols or labels 87 | - in current file in "Go to Symbol in File..." [`Ctrl+Shift+O`, `Cmd+Shift+O`] 88 | - in all includes in "Go to Symbol in Workspace..." [`Ctrl+T`, `Cmd+T`] 89 | - in Outline side-bar 90 | 91 | ### Definitions: 92 | > ![Definitions](images/z80-macroasm-definition.png) 93 | 94 | - Generated map of every symbol defined considers also modules or temporal labels: 95 | > ![Peek Definition demo](images/z80-macroasm-definition-peek.gif) 96 | 97 | - Show symbol's value or specific definiton on hover: 98 | > ![Hover over symbol demo](images/z80-macroasm-hover.gif) 99 | 100 | ### Completion: 101 | > ![Completion](images/z80-macroasm-completion.png) 102 | 103 | - Inteligent completion of directives, pseudo-instructions, Z80 instructions, registers, labels or symbols: 104 | > ![Completion demo](images/z80-macroasm-completion-demo.gif) 105 | 106 | ### Renaming: 107 | - Allow to rename labels, local labels, module names or macro indetifiers in InteliSense meaning. 108 | > ![Renaming demo](images/z80-macroasm-rename.gif) 109 | 110 | 111 | ## ⌨️ Credits 112 | 113 | **Martin Bórik** is leading the development of this extension with some inspirations from these VS Code extensions: 114 | - [`z80asm-vscode`](https://github.com/Imanolea/z80asm-vscode) by **Imanol Barriuso** 115 | - [`vscode-pasmo`](https://github.com/BouKiCHi/vscode-pasmo) by **BouKiCHi** 116 | - [`rgbds-vscode`](https://github.com/DonaldHays/rgbds-vscode) by **Donald Hays** 117 | 118 | #### Contributors: 119 | - [Romain Giot](https://github.com/rgiot) 120 | - [Néstor Sancho](https://github.com/theNestruo) 121 | - [Tomaz Kragelj](https://github.com/tomaz) 122 | - [Alexander Kovalenko](https://github.com/alexanderk23) 123 | - [Alexander Kolnogorov](https://github.com/kolnogorov) 124 | 125 | ## 📋 License 126 | 127 | The Z80 Assembly extension is subject to [these license terms](LICENSE). 128 | 129 | The source code to this extension is available on [github](https://github.com/mborik/z80-macroasm-vscode) and licensed under the [MIT license](LICENSE). 130 | -------------------------------------------------------------------------------- /images/z80-macroasm-completion-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-completion-demo.gif -------------------------------------------------------------------------------- /images/z80-macroasm-completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-completion.png -------------------------------------------------------------------------------- /images/z80-macroasm-definition-peek.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-definition-peek.gif -------------------------------------------------------------------------------- /images/z80-macroasm-definition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-definition.png -------------------------------------------------------------------------------- /images/z80-macroasm-hover.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-hover.gif -------------------------------------------------------------------------------- /images/z80-macroasm-rename.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/images/z80-macroasm-rename.gif -------------------------------------------------------------------------------- /language.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | // symbol used for single line comment. Remove this entry if your language does not support line comments 4 | "lineComment": ";", 5 | // symbols used for start and end a block comment. Remove this entry if your language does not support block comments 6 | "blockComment": [ "/*", "*/" ] 7 | }, 8 | // symbols used as brackets 9 | "brackets": [ 10 | ["(", ")"], 11 | ["[", "]"] 12 | ], 13 | // symbols that are auto closed when typing 14 | "autoClosingPairs": [ 15 | ["(", ")"], 16 | ["[", "]"], 17 | ["\"", "\""], 18 | ["'", "'"] 19 | ], 20 | // symbols that that can be used to surround a selection 21 | "surroundingPairs": [ 22 | ["(", ")"], 23 | ["[", "]"], 24 | ["\"", "\""], 25 | ["'", "'"] 26 | ], 27 | // symbols that are considered as folding regions 28 | "folding": { 29 | "markers": { 30 | "start": "^\\s*;+\\s*#?region\\b", 31 | "end": "^\\s*;+\\s*#?endregion\\b" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mborik/z80-macroasm-vscode/b38ebbac6cd10bf274900e53cea46c50ce3eb884/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "z80-macroasm", 3 | "displayName": "Z80 Macro-Assembler", 4 | "description": "Support for Z80 macro-assemblers in Visual Studio Code", 5 | "version": "0.8.1", 6 | "icon": "logo.png", 7 | "publisher": "mborik", 8 | "categories": [ 9 | "Programming Languages" 10 | ], 11 | "keywords": [ 12 | "assembly", 13 | "macro", 14 | "assembler", 15 | "Zilog", 16 | "Z80", 17 | "Sinclair", 18 | "ZX-Spectrum", 19 | "Amstrad", 20 | "CPC", 21 | "MSX" 22 | ], 23 | "homepage": "https://github.com/mborik/z80-macroasm-vscode", 24 | "bugs": "https://github.com/mborik/z80-macroasm-vscode/issues", 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/mborik/z80-macroasm-vscode.git" 28 | }, 29 | "pricing": "Free", 30 | "license": "MIT", 31 | "engines": { 32 | "vscode": "^1.52.0", 33 | "node": "^16.2.0" 34 | }, 35 | "activationEvents": [ 36 | "onLanguage" 37 | ], 38 | "main": "./out/src/extension", 39 | "contributes": { 40 | "configuration": { 41 | "type": "object", 42 | "title": "Z80 Macro-Assembler configuration", 43 | "properties": { 44 | "z80-macroasm.files.include": { 45 | "type": "string", 46 | "default": "**/*.{a80,asm,inc,s}", 47 | "description": "Files to include and work with. If you, or your macro-assembler using a different conventions of source file extensions then change it here." 48 | }, 49 | "z80-macroasm.files.exclude": { 50 | "type": [ 51 | "string", 52 | "null" 53 | ], 54 | "default": null, 55 | "description": "Choose files or directories to exclude (e.g '**/*.{lst}')." 56 | }, 57 | "z80-macroasm.format.enabled": { 58 | "type": "boolean", 59 | "default": false, 60 | "description": "Formatter: Turn on the experimental feature of format on-type or on-save." 61 | }, 62 | "z80-macroasm.format.baseIndent": { 63 | "type": "number", 64 | "default": 2, 65 | "description": "Formatter: How many tabstops you prefer before the instructions or keywords (default 2)." 66 | }, 67 | "z80-macroasm.format.controlIndent": { 68 | "type": "number", 69 | "default": 1, 70 | "description": "Formatter: How many tabstops you prefer before the control structure keywords, selections, modules, or blocks (default 1)." 71 | }, 72 | "z80-macroasm.format.whitespaceAfterInstruction": { 73 | "type": "string", 74 | "enum": [ 75 | "auto", 76 | "tab", 77 | "single-space" 78 | ], 79 | "default": "auto", 80 | "description": "Formatter: Which type of whitespace you want to put after the instruction (default `auto` will autodetect file's tabstop type and width)." 81 | }, 82 | "z80-macroasm.format.spaceAfterArgument": { 83 | "type": "boolean", 84 | "default": false, 85 | "description": "Formatter: If you want to put a single space character after comma (instruction's argument)." 86 | }, 87 | "z80-macroasm.format.spaceAfterInstruction": { 88 | "type": "boolean", 89 | "default": true, 90 | "description": "Formatter: If you want to put a single space character after instruction (before colon separator)." 91 | }, 92 | "z80-macroasm.format.spacesAroundOperators": { 93 | "type": "boolean", 94 | "default": false, 95 | "description": "Formatter: If you want to wrap an operators with spaces." 96 | }, 97 | "z80-macroasm.format.uppercaseKeywords": { 98 | "type": [ 99 | "boolean", 100 | "string" 101 | ], 102 | "enum": [ 103 | "auto", 104 | true, 105 | false 106 | ], 107 | "default": "auto", 108 | "description": "Formatter: Uppercase all keywords, instructions and registers (autodetect case by default)." 109 | }, 110 | "z80-macroasm.format.bracketType": { 111 | "type": "string", 112 | "enum": [ 113 | "no-change", 114 | "round", 115 | "square" 116 | ], 117 | "default": "no-change", 118 | "description": "Formatter: Define which type of brackets around the instruction's arguments (pointers) you prefer: `(hl)` or `[hl]` (by default keep untouched)." 119 | }, 120 | "z80-macroasm.format.splitInstructionsByColon": { 121 | "type": "boolean", 122 | "default": true, 123 | "description": "Formatter: Split colon separated instructions to lines." 124 | }, 125 | "z80-macroasm.format.colonAfterLabels": { 126 | "type": [ 127 | "boolean", 128 | "string" 129 | ], 130 | "enum": [ 131 | "no-change", 132 | true, 133 | false 134 | ], 135 | "default": "no-change", 136 | "description": "Formatter: Put colon after each label or symbol (by default keep untouched)." 137 | }, 138 | "z80-macroasm.format.hexaNumberStyle": { 139 | "type": "string", 140 | "enum": [ 141 | "no-change", 142 | "hash", 143 | "motorola", 144 | "intel", 145 | "intel-uppercase", 146 | "c-style" 147 | ], 148 | "default": "no-change", 149 | "description": "Formatter: Define which hexadecimal number format you prefer (disabled by default, see README for full specs)." 150 | }, 151 | "z80-macroasm.format.hexaNumberCase": { 152 | "type": [ 153 | "boolean", 154 | "string" 155 | ], 156 | "enum": [ 157 | "no-change", 158 | true, 159 | false 160 | ], 161 | "default": "no-change", 162 | "description": "Formatter: When formatting of hexadecimal numbers was enabled, whether it's to be additional case processing applied (by default keep untouched)." 163 | }, 164 | "z80-macroasm.suggestOnInstructions": { 165 | "type": "boolean", 166 | "default": false, 167 | "description": "If true, extension will suggest basic instruction mnemonics for the auto-completion. Otherwise, only instruction arguments will be suggested." 168 | }, 169 | "z80-macroasm.suggestCommitWithCustomKeys": { 170 | "type": "boolean", 171 | "default": false, 172 | "description": "If true, extension will commit suggesttions with extension's custom keys (enter, tab, or comma). Otherwise, only predefined `acceptSelectedSuggestion` shortcut will commit suggestion." 173 | }, 174 | "z80-macroasm.seekSymbolsThroughWorkspace": { 175 | "type": "boolean", 176 | "default": false, 177 | "description": "If true, extension will crawl through all workspace files to document all symbols. Otherwise, only includes are taken into account." 178 | } 179 | } 180 | }, 181 | "problemMatchers": [ 182 | { 183 | "name": "errmatcher-as", 184 | "owner": "z80-macroasm", 185 | "fileLocation": [ 186 | "relative", 187 | "${workspaceFolder}" 188 | ], 189 | "pattern": [ 190 | { 191 | "regexp": "^> > >(.*)\\(([0-9]+)\\): (error|warning):\\s+(.*)(\\x1B\\[K)?$", 192 | "file": 1, 193 | "line": 2, 194 | "severity": 3, 195 | "message": 4 196 | } 197 | ] 198 | }, 199 | { 200 | "name": "errmatcher-sjasmplus", 201 | "owner": "z80-macroasm", 202 | "fileLocation": [ 203 | "autoDetect", 204 | "${workspaceFolder}" 205 | ], 206 | "pattern": [ 207 | { 208 | "regexp": "^(.*)\\(([0-9]+)\\): (error|warning):\\s+(.*)$", 209 | "file": 1, 210 | "line": 2, 211 | "severity": 3, 212 | "message": 4 213 | } 214 | ] 215 | }, 216 | { 217 | "name": "errmatcher-sjasm", 218 | "owner": "z80-macroasm", 219 | "fileLocation": [ 220 | "absolute" 221 | ], 222 | "pattern": [ 223 | { 224 | "regexp": "^([^\\(]+)(?:\\()(\\d+)(?:\\)\\s*\\:\\s+)(.+)$", 225 | "file": 1, 226 | "line": 2, 227 | "message": 3 228 | } 229 | ] 230 | }, 231 | { 232 | "name": "errmatcher-tniasm", 233 | "owner": "z80-macroasm", 234 | "fileLocation": [ 235 | "relative", 236 | "${workspaceFolder}" 237 | ], 238 | "pattern": [ 239 | { 240 | "regexp": "^(Warning|Error|Syntax Error) in line (\\d+) \\(([^)]+)\\): (.*)$", 241 | "severity": 1, 242 | "line": 2, 243 | "file": 3, 244 | "message": 4 245 | } 246 | ] 247 | }, 248 | { 249 | "name": "errmatcher-tniasm-preprocessor", 250 | "owner": "z80-macroasm", 251 | "pattern": [ 252 | { 253 | "kind": "file", 254 | "regexp": "^(Warning|Error) .* (\\S+)! .*$", 255 | "message": 0, 256 | "severity": 1, 257 | "file": 2 258 | } 259 | ] 260 | }, 261 | { 262 | "name": "errmatcher-rasm", 263 | "owner": "z80-macroasm", 264 | "fileLocation": [ 265 | "relative", 266 | "${workspaceFolder}" 267 | ], 268 | "pattern": [ 269 | { 270 | "regexp": "^\\[(.*):(\\d+)\\]\\s+(Warning)?:?(.*)$", 271 | "file": 1, 272 | "line": 2, 273 | "severity": 3, 274 | "message": 4 275 | } 276 | ] 277 | }, 278 | { 279 | "name": "errmatcher-pasmo", 280 | "owner": "z80-macroasm", 281 | "fileLocation": [ 282 | "relative", 283 | "${workspaceFolder}" 284 | ], 285 | "pattern": [ 286 | { 287 | "regexp": "^([^:]+):([0-9]+)\\s+(.*)$", 288 | "file": 1, 289 | "line": 2, 290 | "message": 3 291 | } 292 | ] 293 | } 294 | ], 295 | "languages": [ 296 | { 297 | "id": "z80-macroasm", 298 | "aliases": [ 299 | "Z80 Macro-Assembler", 300 | "z80-asm" 301 | ], 302 | "extensions": [ 303 | ".a80", 304 | ".asm", 305 | ".inc", 306 | ".s" 307 | ], 308 | "configuration": "./language.configuration.json" 309 | } 310 | ], 311 | "snippets": [ 312 | { 313 | "language": "z80-macroasm", 314 | "path": "./snippets/z80-macroasm.json" 315 | } 316 | ], 317 | "grammars": [ 318 | { 319 | "language": "z80-macroasm", 320 | "scopeName": "source.z80asm", 321 | "path": "./syntaxes/z80-macroasm.tmLanguage.json" 322 | } 323 | ] 324 | }, 325 | "scripts": { 326 | "vscode:prepublish": "npm run compile", 327 | "compile": "tsc -p ./", 328 | "watch": "tsc -watch -p ./", 329 | "test": "eslint -c .eslintrc.json src/**/*.ts" 330 | }, 331 | "devDependencies": { 332 | "@types/node": "^15.6.1", 333 | "@types/vscode": "^1.52.0", 334 | "@typescript-eslint/eslint-plugin": "^4.28.0", 335 | "@typescript-eslint/parser": "^4.28.0", 336 | "eslint": "^7.29.0", 337 | "typescript": "^4.1.3", 338 | "vscode-test": "^1.5.2" 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /snippets/z80-macroasm.json: -------------------------------------------------------------------------------- 1 | { 2 | "macro-as-pasmo": { 3 | "prefix": "macro", 4 | "body": [ 5 | "${1:label}:\tmacro", 6 | "\t\t$0", 7 | "\tendm" 8 | ], 9 | "description": "AS/Pasmo macro" 10 | }, 11 | "macro-sjasm-rasm": { 12 | "prefix": "macro", 13 | "body": [ 14 | "macro ${1:name}", 15 | "$0", 16 | "endm" 17 | ], 18 | "description": "SjASM/rasm macro" 19 | }, 20 | "rept": { 21 | "prefix": "rept", 22 | "body": [ 23 | "rept ${1:n}", 24 | "$0", 25 | "endm" 26 | ], 27 | "description": "repeat macro" 28 | }, 29 | "repeat": { 30 | "prefix": "repeat", 31 | "body": [ 32 | "repeat ${1:n}${3:,${2:name}}", 33 | "$0", 34 | "rend" 35 | ], 36 | "description": "repeat count[,counter] by rasm" 37 | }, 38 | "dup-sjasm": { 39 | "prefix": "dup", 40 | "body": [ 41 | "dup ${1:n}", 42 | "$0", 43 | "edup" 44 | ], 45 | "description": "SjASM repeat macro" 46 | }, 47 | "module-sjasm": { 48 | "prefix": "module", 49 | "body": [ 50 | "module ${1:name}", 51 | "$0", 52 | "endmodule" 53 | ], 54 | "description": "SjASM module" 55 | }, 56 | "align": { 57 | "prefix": "align", 58 | "body": [ 59 | "align\t${1|2,4,8,16,32,64,128,256,512,1024,2048,4096,8192,16384,32768|}", 60 | "$0" 61 | ], 62 | "description": "fills zero or more bytes until the new address modulo ." 63 | }, 64 | "binclude-as": { 65 | "prefix": "binclude", 66 | "body": [ 67 | "binclude \"${1:file}\"${5:,${2:offset}${4:,${3:length}}}", 68 | "$0" 69 | ], 70 | "description": "binclude [,offset[,length]]\nembed binary data into the code generated by AS Macroassembler" 71 | }, 72 | "incbin": { 73 | "prefix": "incbin", 74 | "body": [ 75 | "incbin \"${1:file}\"${5:,${2:offset}${4:,${3:length}}}", 76 | "$0" 77 | ], 78 | "description": "incbin [,offset[,length]]\nembed binary data into the code generated by SjASM" 79 | }, 80 | "include": { 81 | "prefix": "include", 82 | "body": [ 83 | "include \"${1:file}\"", 84 | "$0" 85 | ], 86 | "description": "include another sourcefile given as a parameter into the current" 87 | }, 88 | "output-sjasm": { 89 | "prefix": "output", 90 | "body": [ 91 | "output \"${1:file}\"${3:,${2|t,r,a|}}", 92 | "$0", 93 | "outend" 94 | ], 95 | "description": "output [,mode]\nfollowing source code will be assembled into the file" 96 | }, 97 | "savebin-sjasm": { 98 | "prefix": "savebin", 99 | "body": [ 100 | "savebin \"${1:file}\",${2:begin},${3:length}", 101 | "$0" 102 | ], 103 | "description": "save block of RAM into the file" 104 | }, 105 | "savetrd-sjasm": { 106 | "prefix": "savetrd", 107 | "body": [ 108 | "${1|savetrd,savehob|} \"${2:image}\",\"${3:filename}\",${4:begin},${5:length}", 109 | "$0" 110 | ], 111 | "description": "save block of RAM into the TR-DOS image or Hobeta block for ZX-Spectrum emulators" 112 | }, 113 | "emptytrd-sjasm": { 114 | "prefix": "emptytrd", 115 | "body": [ 116 | "emptytrd \"${2:image}\"", 117 | "$0" 118 | ], 119 | "description": "create the empty TR-DOS image for ZX-Spectrum emulators" 120 | }, 121 | "savesna-sjasm": { 122 | "prefix": "savesna", 123 | "body": [ 124 | "savesna \"${1:file}\",${2:start}", 125 | "$0" 126 | ], 127 | "description": "save the snapshot for ZX-Spectrum emulators" 128 | }, 129 | "savetap-sjasm": { 130 | "prefix": "savetap", 131 | "body": [ 132 | "savetap \"${1:file}\",${2|BASIC,CODE,NUMBERS,CHARS,HEADLESS|}${4:,\"${3:tapheader}\"},${5:start},${6:length}", 133 | "$0" 134 | ], 135 | "description": "save the tape file for ZX-Spectrum emulators" 136 | }, 137 | "tapout-sjasm": { 138 | "prefix": "tapout", 139 | "body": [ 140 | "tapout \"${1:file}\"${2:,${3:flag}}", 141 | "$0", 142 | "tapend" 143 | ], 144 | "description": "output block to tape file for ZX-Spectrum emulators" 145 | }, 146 | "emptytap-sjasm": { 147 | "prefix": "emptytap", 148 | "body": [ 149 | "emptytap \"${2:file}\"", 150 | "$0" 151 | ], 152 | "description": "empty the tape file for ZX-Spectrum emulators" 153 | }, 154 | "cpu-as": { 155 | "prefix": "cpu", 156 | "body": [ 157 | "cpu\tz80undoc", 158 | "relaxed\ton", 159 | "page\t0", 160 | "$0" 161 | ], 162 | "description": "AS Macroassembler CPU definiton" 163 | }, 164 | "device-sjasm": { 165 | "prefix": "device", 166 | "body": [ 167 | "device\t${1|none,zxspectrum48,zxspectrum128,zxspectrum256,zxspectrum512,zxspectrum1024|}", 168 | "$0" 169 | ], 170 | "description": "SjASM device definition" 171 | }, 172 | "setcpc-rasm": { 173 | "prefix": "cpc", 174 | "body" : [ 175 | "setcpc ${1|0,1,2,3,4,5,6|}" 176 | ], 177 | "description": "CPC choice in SNA by rasm.\n0: CPC 464\n1: CPC 664\n2: CPC 6128\n4: 464 Plus\n5: 6128 Plus\n6: GX-4000" 178 | }, 179 | "while-rasm": { 180 | "prefix": "while", 181 | "body": [ 182 | "while ${1:test}", 183 | "$0", 184 | "wend" 185 | ], 186 | "description": "WHILE loop by rasm" 187 | }, 188 | "protect-rasm": { 189 | "prefix": "prot", 190 | "body" : [ 191 | "protect ${1:start} ${2:end}" 192 | ], 193 | "description": "PROTECT start end by rasm" 194 | }, 195 | "struct-rasm": { 196 | "prefix": "struct", 197 | "body": [ 198 | "struct ${1:name}", 199 | "$0", 200 | "endstruct" 201 | ], 202 | "description": "Structure creation by rasm" 203 | } 204 | } -------------------------------------------------------------------------------- /src/completionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ConfigProps, ConfigPropsProvider } from './configProperties'; 3 | import set from './defs_list'; 4 | import regex from './defs_regex'; 5 | import { SymbolProcessor } from './symbolProcessor'; 6 | import { 7 | isFirstLetterUppercase, 8 | pad, 9 | safeSplitStringByChar, 10 | uppercaseIfNeeded 11 | } from './utils'; 12 | 13 | interface InstructionMapperProps extends ConfigProps { 14 | snippet: string; 15 | uppercase: boolean; 16 | z80n?: boolean; 17 | range?: vscode.Range; 18 | } 19 | 20 | interface RegisterMapperProps extends ConfigProps { 21 | snippet: string; 22 | index: number; 23 | uppercase: boolean; 24 | secondArgument?: boolean; 25 | range?: vscode.Range; 26 | } 27 | 28 | export class Z80CompletionProvider extends ConfigPropsProvider implements vscode.CompletionItemProvider { 29 | constructor(public symbolProcessor: SymbolProcessor) { 30 | super(symbolProcessor.settings); 31 | } 32 | 33 | private _instructionMapper({ snippet, uppercase, z80n, range, ...opt }: InstructionMapperProps) { 34 | const delimiter = snippet.substr(-1); 35 | snippet = uppercaseIfNeeded(snippet, uppercase).trim(); 36 | 37 | const item = new vscode.CompletionItem(snippet, vscode.CompletionItemKind.Keyword); 38 | const snip = new vscode.SnippetString(snippet); 39 | if (delimiter === '\t') { 40 | if (opt.whitespaceAfterInstruction === 'single-space') { 41 | snip.appendText(' '); 42 | } 43 | else if (opt.whitespaceAfterInstruction === 'tab') { 44 | snip.appendText('\t'); 45 | } 46 | else if (opt.indentSpaces) { 47 | let tabSize = opt.indentSize; 48 | while (snippet.length > tabSize) { 49 | tabSize += opt.indentSize; 50 | } 51 | 52 | snip.appendText(' '.repeat(tabSize - snippet.length)); 53 | } 54 | else { 55 | snip.appendText('\t'); 56 | } 57 | } 58 | else if (delimiter === '\n' && opt.splitInstructionsByColon) { 59 | snip.appendText(opt.eol); 60 | } 61 | else { 62 | snip.appendText(' '); 63 | } 64 | 65 | snip.appendTabstop(0); 66 | item.insertText = snip; 67 | 68 | if (opt.suggestCommitWithCustomKeys) { 69 | item.commitCharacters = ['\t']; 70 | } 71 | if (z80n) { 72 | item.documentation = new vscode.MarkdownString('(Z80N)'); 73 | item.sortText = `z${snippet}`; // put on bottom... 74 | } 75 | if (range) { 76 | item.range = range; 77 | } 78 | 79 | return item; 80 | } 81 | 82 | private _registerMapper({ snippet, uppercase, index, secondArgument, range, ...opt }: RegisterMapperProps) { 83 | snippet = uppercaseIfNeeded(snippet, uppercase); 84 | 85 | // add space before selected completion, unless user has `formatOnType` enabled, 86 | // because in that case, formatter already added whitespace before the completion menu is shown. 87 | const prefix = (!opt.formatOnType && secondArgument && opt.spaceAfterArgument) ? ' ' : ''; 88 | 89 | // when `formatOnType` is enabled, we shouldn't add newline after the second argument, 90 | // because it will create a newline itself while we want Enter key just to confirm the item. 91 | const suffix = (!opt.formatOnType && secondArgument && opt.splitInstructionsByColon) ? opt.eol : ''; 92 | 93 | // commit characters are slightly different for second argument: 94 | // comma is accepted in addition tab or enter. 95 | const commitChars = ['\t', '\n']; 96 | if (!secondArgument) { 97 | commitChars.unshift(','); 98 | } 99 | 100 | if (opt.bracketType === 'square' && snippet.indexOf('(') === 0) { 101 | snippet = snippet.replace('(', '[').replace(')', ']'); 102 | } 103 | 104 | const item = new vscode.CompletionItem(snippet, vscode.CompletionItemKind.Value); 105 | const snip = new vscode.SnippetString(prefix + snippet.replace('*', '${1:0}')); 106 | 107 | snip.appendText(suffix); 108 | snip.appendTabstop(0); 109 | 110 | // put on the top of the list... 111 | item.sortText = `!${pad(index)}`; 112 | item.insertText = snip; 113 | 114 | if (opt.suggestCommitWithCustomKeys) { 115 | item.commitCharacters = commitChars; 116 | } 117 | if (range) { 118 | item.range = range; 119 | } 120 | 121 | return item; 122 | } 123 | 124 | private _shouldKeywordUppercase( 125 | part: string, 126 | uppercaseKeywords: ConfigProps['uppercaseKeywords'] 127 | ) { 128 | return uppercaseKeywords === 'auto' ? 129 | isFirstLetterUppercase(part) : 130 | uppercaseKeywords; 131 | } 132 | 133 | //--------------------------------------------------------------------------------------- 134 | async provideCompletionItems( 135 | document: vscode.TextDocument, 136 | position: vscode.Position, 137 | token: vscode.CancellationToken 138 | ) { 139 | const configProps = this.getConfigProps(document); 140 | let line = document.lineAt(position.line).text; 141 | let output: vscode.CompletionItem[] = []; 142 | let baseIndex = 0; 143 | 144 | const endCommentMatch = regex.endComment.exec(line); 145 | if (endCommentMatch && endCommentMatch.index < position.character) { 146 | return; 147 | } 148 | 149 | const labelMatch = regex.labelDefinition.exec(line); 150 | if (labelMatch) { 151 | const [ fullMatch ] = labelMatch; 152 | baseIndex += fullMatch.length; 153 | while (/\s/.test(line[baseIndex])) { 154 | baseIndex++; 155 | } 156 | line = line.substring(baseIndex); 157 | } 158 | 159 | line = line.substring(0, position.character - baseIndex); 160 | if (!line.trim()) { 161 | return; 162 | } 163 | 164 | const { fragment, lastFragmentIndex } = 165 | safeSplitStringByChar(line, ':').reduce( 166 | ({ currentIndex }, fragment) => ({ 167 | fragment, 168 | currentIndex: currentIndex + fragment.length + 1, // plus colon size 169 | lastFragmentIndex: currentIndex, 170 | }), 171 | { 172 | fragment: '', 173 | currentIndex: baseIndex, 174 | lastFragmentIndex: 0, 175 | } 176 | ); 177 | 178 | if (!fragment) { 179 | return; 180 | } 181 | 182 | const shouldSuggestInstructionMatch = regex.shouldSuggestInstruction.exec(fragment); 183 | if (shouldSuggestInstructionMatch) { 184 | if (!configProps.suggestOnInstructions) { 185 | vscode.commands.executeCommand('editor.action.triggerSuggest', { auto: false }); 186 | } 187 | 188 | const [ fullMatch,,,, instructionPart ] = shouldSuggestInstructionMatch; 189 | const uppercase = this._shouldKeywordUppercase( 190 | instructionPart, 191 | configProps.uppercaseKeywords 192 | ); 193 | 194 | const range = new vscode.Range( 195 | position.line, lastFragmentIndex + 196 | (instructionPart ? fullMatch.lastIndexOf(instructionPart) : fullMatch.length), 197 | position.line, position.character 198 | ); 199 | 200 | output = [ 201 | ...set.instructions.map((snippet) => this._instructionMapper({ 202 | ...configProps, 203 | uppercase, 204 | snippet, 205 | range, 206 | })), 207 | ...set.nextInstructions.map((snippet) => this._instructionMapper({ 208 | ...configProps, 209 | z80n: true, 210 | uppercase, 211 | snippet, 212 | range, 213 | })) 214 | ]; 215 | 216 | if (instructionPart) { 217 | const instructionToFind = uppercaseIfNeeded(instructionPart, uppercase); 218 | const preselected = output.find(snip => snip.label.toString().startsWith(instructionToFind)); 219 | if (preselected) { 220 | preselected.preselect = true; 221 | } 222 | } 223 | } 224 | else { 225 | const shouldSuggest1ArgRegisterMatch = regex.shouldSuggest1ArgRegister.exec(fragment); 226 | const shouldSuggest2ArgRegisterMatch = regex.shouldSuggest2ArgRegister.exec(fragment); 227 | const shouldSuggestConditionalsMatch = regex.shouldSuggestConditionals.exec(fragment); 228 | 229 | if (shouldSuggest2ArgRegisterMatch) { 230 | const uppercase = this._shouldKeywordUppercase( 231 | shouldSuggest2ArgRegisterMatch[1], 232 | configProps.uppercaseKeywords 233 | ); 234 | 235 | if (shouldSuggest2ArgRegisterMatch[1].toLowerCase() === 'ex' && 236 | shouldSuggest2ArgRegisterMatch[2].toLowerCase() === 'af') { 237 | 238 | const text = uppercaseIfNeeded("af'", uppercase); 239 | const item = new vscode.CompletionItem(text, vscode.CompletionItemKind.Value); 240 | item.insertText = new vscode.SnippetString(text) 241 | .appendText(configProps.eol) 242 | .appendTabstop(0); 243 | 244 | if (configProps.suggestCommitWithCustomKeys) { 245 | item.commitCharacters = ['\n']; 246 | } 247 | return [item]; 248 | } 249 | else { 250 | output = set.registers.map((snippet, index) => this._registerMapper({ 251 | ...configProps, 252 | secondArgument: true, 253 | uppercase, 254 | snippet, 255 | index 256 | })); 257 | } 258 | } 259 | else if (shouldSuggest1ArgRegisterMatch) { 260 | const { 261 | index: currentSuggestIndex, 262 | 1: instruction, 263 | 2: instructionR16, 264 | 3: instructionR8 265 | } = shouldSuggest1ArgRegisterMatch; 266 | 267 | let idxStart = 0, idxEnd = undefined; 268 | const uppercase = this._shouldKeywordUppercase( 269 | instruction, 270 | configProps.uppercaseKeywords 271 | ); 272 | 273 | if (instructionR16) { 274 | idxStart = set.regR16Index; 275 | idxEnd = set.regStackIndex; 276 | } 277 | else if (instructionR8) { 278 | idxEnd = set.regR16Index; 279 | } 280 | 281 | const range = new vscode.Range( 282 | position.line, lastFragmentIndex + currentSuggestIndex + instruction.length, 283 | position.line, position.character 284 | ); 285 | 286 | output = set.registers 287 | .slice(idxStart, idxEnd) 288 | .map( 289 | (snippet, index) => this._registerMapper({ 290 | ...configProps, 291 | uppercase, 292 | snippet, 293 | index, 294 | range 295 | }) 296 | ); 297 | } 298 | else if (shouldSuggestConditionalsMatch) { 299 | const { 1: instruction } = shouldSuggestConditionalsMatch; 300 | const uppercase = this._shouldKeywordUppercase( 301 | instruction, 302 | configProps.uppercaseKeywords 303 | ); 304 | 305 | output = set.conditionals.map( 306 | (snippet, index) => this._registerMapper({ 307 | ...configProps, 308 | uppercase, 309 | snippet, 310 | index, 311 | }) 312 | ); 313 | } 314 | } 315 | 316 | const symbols = await this.symbolProcessor.symbols(document); 317 | if (token.isCancellationRequested) { 318 | return; 319 | } 320 | 321 | for (const name in symbols) { 322 | const symbol = symbols[name]; 323 | 324 | // mark a suggested item with proper icon 325 | let kind = vscode.CompletionItemKind.Variable; 326 | 327 | // suggest also macros in place of instructions 328 | if (symbol.kind === vscode.SymbolKind.Module) { 329 | kind = vscode.CompletionItemKind.Module; 330 | 331 | if (shouldSuggestInstructionMatch) { 332 | continue; 333 | } 334 | } 335 | else if (symbol.kind === vscode.SymbolKind.Function) { 336 | kind = vscode.CompletionItemKind.Function; 337 | } 338 | else if (shouldSuggestInstructionMatch) { 339 | continue; 340 | } 341 | 342 | const item = new vscode.CompletionItem(name, kind); 343 | if (symbol.path.length > 1) { 344 | item.documentation = new vscode.MarkdownString(symbol.declaration); 345 | } 346 | if (symbol.documentation) { 347 | if (item.documentation instanceof vscode.MarkdownString) { 348 | item.documentation.appendMarkdown('\n\n' + symbol.documentation); 349 | } 350 | else { 351 | item.documentation = new vscode.MarkdownString(symbol.documentation); 352 | } 353 | } 354 | 355 | if (symbol.location.uri.fsPath === document.fileName) { 356 | // sort symbols by proximity to current line of current file 357 | const delta = Math.abs(symbol.line - position.line); 358 | item.sortText = `!z${pad(delta, 10)}`; 359 | } 360 | else { 361 | item.sortText = symbol.declaration; 362 | } 363 | 364 | if (name[0] === '.' && line.lastIndexOf('.') > 0) { 365 | item.range = new vscode.Range( 366 | position.line, baseIndex + line.lastIndexOf('.'), 367 | position.line, position.character 368 | ); 369 | } 370 | 371 | if (configProps.suggestCommitWithCustomKeys) { 372 | item.commitCharacters = ['\n']; 373 | } 374 | 375 | output.push(item); 376 | } 377 | 378 | return output; 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/configProperties.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { EXTENSION_LANGUAGE_ID } from './extension'; 3 | 4 | export interface ConfigProps { 5 | eol: string; 6 | formatOnType: boolean; 7 | indentSpaces: boolean; 8 | indentSize: number; 9 | indentDetector: RegExp; 10 | suggestOnInstructions: boolean; 11 | suggestCommitWithCustomKeys: boolean; 12 | 13 | // format section 14 | baseIndent: number; 15 | controlIndent: number; 16 | whitespaceAfterInstruction: 'auto' | 'tab' | 'single-space'; 17 | spaceAfterArgument: boolean; 18 | spaceAfterInstruction: boolean; 19 | spacesAroundOperators: boolean; 20 | uppercaseKeywords: 'auto' | boolean; 21 | bracketType: 'no-change' | 'round' | 'square'; 22 | splitInstructionsByColon: boolean; 23 | colonAfterLabels: 'no-change' | boolean; 24 | hexaNumberStyle: 'no-change' | 'hash' | 'motorola' | 'intel' | 'intel-uppercase' | 'c-style'; 25 | hexaNumberCase: 'no-change' | boolean; 26 | } 27 | 28 | export abstract class ConfigPropsProvider { 29 | constructor(public settings: vscode.WorkspaceConfiguration) {} 30 | 31 | getConfigProps(document: vscode.TextDocument) { 32 | const config = vscode.workspace.getConfiguration(undefined, { languageId: EXTENSION_LANGUAGE_ID }); 33 | 34 | const indentSize = parseInt(config.editor.tabSize, 10) || 8; 35 | const result: ConfigProps = { 36 | ...this.settings?.format, 37 | 38 | suggestOnInstructions: this.settings?.suggestOnInstructions === true, 39 | suggestCommitWithCustomKeys: this.settings?.suggestCommitWithCustomKeys === true, 40 | 41 | eol: (config.files.eol === vscode.EndOfLine.CRLF) ? '\r\n' : '\n', 42 | formatOnType: config.editor.formatOnType === true, 43 | indentSpaces: config.editor.insertSpaces === true, 44 | indentSize, 45 | 46 | indentDetector: RegExp('^' + 47 | `(\t|${' '.repeat(indentSize)})?`.repeat( 48 | Math.ceil(80 / indentSize) 49 | ) 50 | ) 51 | }; 52 | 53 | // if this document is open, use the settings from that window 54 | vscode.window.visibleTextEditors.some(editor => { 55 | if (editor.document && editor.document.fileName === document.fileName) { 56 | result.indentSpaces = (editor.options.insertSpaces === true); 57 | result.indentSize = parseInt(editor.options.tabSize as any, 10) || 8; 58 | result.eol = (editor.document.eol === vscode.EndOfLine.CRLF) ? '\r\n' : '\n'; 59 | return true; 60 | } 61 | return false; 62 | }); 63 | 64 | return result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/definitionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SymbolProcessor } from './symbolProcessor'; 3 | 4 | export class Z80DefinitionProvider implements vscode.DefinitionProvider { 5 | constructor(public symbolProcessor: SymbolProcessor) {} 6 | 7 | provideDefinition( 8 | document: vscode.TextDocument, 9 | position: vscode.Position, 10 | token: vscode.CancellationToken 11 | ): Thenable { 12 | return this.symbolProcessor.getFullSymbolAtDocPosition( 13 | document, position, token 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/defs_list.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-multi-spaces */ 2 | 3 | export default { 4 | // Z80 instruction set 5 | instructions: [ 6 | 'adc\t', 'add\t', 'and\t', 'bit\t', 'call\t', 'ccf\n', 'cp\t', 'cpd\n', 7 | 'cpdr\n', 'cpi\n', 'cpir\n', 'cpl\n', 'daa\n', 'dec\t', 'di\n', 'ei\n', 8 | 'djnz\t', 'ex\t', 'exa\n', 'exd\n', 'exx\n', 'halt\n', 'im\t', 'in\t', 9 | 'inc\t', 'ind\n', 'indr\n', 'ini\n', 'inir\n', 'jp\t', 'jr\t', 'ld\t', 10 | 'ldd\n', 'lddr\n', 'ldi\n', 'ldir\n', 'neg\n', 'nop\n', 'or\t', 'otdr\n', 11 | 'otir\n', 'out\t', 'outd\n', 'outi\n', 'pop\t', 'push\t', 'res\t', 'ret\t', 12 | 'reti\n', 'retn\n', 'rl\t', 'rla\n', 'rlc\t', 'rlca\n', 'rld\n', 'rr\t', 13 | 'rra\n', 'rrc\t', 'rrca\n', 'rrd\n', 'rst\t', 'sbc\t', 'scf\n', 'set\t', 14 | 'sla\t', 'slia\t', 'sll\t', 'sl1\t', 'swap\t', 'sra\t', 'srl\t', 'sub\t', 15 | 'xor\t' 16 | ], 17 | // Z80N - ZX-Spectrum Next extended instruction set 18 | nextInstructions: [ 19 | 'ldix\n', 'ldirx\n', 'lddx\n', 'lddrx\n', 'ldws\n', 'ldpirx\n', 'mirror\n', 20 | 'mul\t', 'nextreg\t','outinb\n', 'pixelad\n','pixeldn\n','setae\n', 'swapnib\n', 21 | 'test\t', 'bsla\t', 'bsra\t', 'bsrl\t', 'bsrf\t', 'brlc\t' 22 | ], 23 | registers: [ 24 | /* 0 */ 'a', 'b', 'c', 'd', 'e', 'h', 'l', 'i', 'r', 25 | /* 9 */ '(hl)', '(de)', '(bc)', '(ix+*)', '(iy+*)', '(c)', 26 | /* 15 */ 'ixl', 'ixh', 'ixu', 'lx', 'hx', 'xl', 'xh', 27 | /* 22 */ 'iyl', 'iyh', 'iyu', 'ly', 'hy', 'yl', 'yh', 28 | /* 29 */ 'hl', 'de', 'bc', 'af', 'ix', 'iy', 29 | /* 35 */ 'sp', '(sp)', '(ix)', '(iy)' 30 | ], 31 | conditionals: ['c', 'nc', 'z', 'nz', 'p', 'm', 'po', 'pe'], 32 | 33 | // quick pointers into `registers` 34 | regR16Index: 29, 35 | regStackIndex: 35, 36 | }; 37 | -------------------------------------------------------------------------------- /src/defs_regex.ts: -------------------------------------------------------------------------------- 1 | const mkRegex = (str: TemplateStringsArray, opts: string = 'i') => 2 | new RegExp(str.raw[0].replace(/\s/gm, ''), opts); 3 | 4 | export default { 5 | commentLine: /^(?:\s*;+|\/{2,})\s*(.*)$/, 6 | endComment: /(?:;+|\/{2,})\s*(.*)$/, 7 | includeLine: /(\binclude\s+)((["'])([^\3]+)\3).*$/i, 8 | macroLine: /\b(macro\s+)(\w+)(?:\s+([^\/;$]+))?/i, 9 | moduleLine: /\b(module\s+)(\w+)\b/i, 10 | endmoduleLine: /\bendmod(ule)?\b/i, 11 | controlKeywordLine: mkRegex` 12 | \b( 13 | rept|e?dup|end[mprsw]|exitm|endmod(ule)?|(?:de|un)?phase| 14 | (end)?(struct|section|switch|lua|maxnest)|while|repeat|[rw]end| 15 | if|ifn?def|ifn?used|ifn?exist|else(if)?|endif| 16 | until|(else|end)?case|default|break 17 | )\b`, 18 | horizontalRule: /^(.)\1+$/, 19 | fullLabel: /((\$\$(?!\.))?[\w\.]+)/, 20 | partialLabel: /(?:\$\$(?!\.)|\.)?(\w+)/, 21 | stringBounds: /(["'])(?:(?=(\\?))\2.)*?\1/g, 22 | bracketsBounds: /^\[([^\]\n]*?)]|\(([^\)\n]*?)\)$/, 23 | numerals: mkRegex` 24 | ( 25 | ((?:(?:\-|\b)(?:0b)|%)[01]+)| 26 | (\b[01]+b\b)| 27 | ((?:(?:\-|\b)(?:0x)|[\$#])[0-9a-f]+)| 28 | ((?:\-|\b)[0-9a-f]+h\b)| 29 | ((?:(?:\-|\b)(?:0q?)|@)[0-7]+)| 30 | ((?:\-|\b)[0-7]+o\b)| 31 | ((?:\-|\b)\d+) 32 | )(?!\w+)`, 33 | registers: /\b(?:[abcdefhlir]|ix|iy|af'?|bc|de|hl|pc|sp|ix[hlu]|iy[hlu]|[lh]x|x[lh]|[lh]y|y[lh])\b/i, 34 | condFlags: /\b(j[pr]|call|ret)(?:\s+([cmpz]|n[cz]|p[eo]))$/i, 35 | regsOrConds: /^([abcdefhlimprz]|ix|iy|af'?|bc|de|hl|pc|sp|ix[hlu]|iy[hlu]|[lh]x|x[lh]|[lh]y|y[lh]|n[cz]|p[eo])\b/i, 36 | operators: /(?<=["'\w\)】])((?:\s*(?:[+\-\/%\^#]|[><=~&!\^\|\*]{1,2}|>>>|>=|<=|=>|<>|!=)\s*)|(?:\s+(?:mod|shl|shr|and|or|xor)\s+))(?=[\w\$#%\.\(【'"])/gi, 37 | labelDefinition: /^\@?((\$\$(?!\.))?[\w\.]+)(:|\s|$)/, 38 | parentLabel: /^(((\@|\$\$)(?!\.))?\w[\w\.]*)(?::|\s|$)/, 39 | evalExpression: /^\@?([\w\.]+)((?:\:?\s*)=\s*|(?:\:\s*|\s+)(?:(?:equ|eval)\s+))(.+)(;.*)?$/i, 40 | shouldSuggestInstruction: /^(\@?((\$\$(?!\.))?[\w\.]+)[:\s])?\s*(\w+)?(?!.+)$/, 41 | shouldSuggest1ArgRegister: mkRegex` 42 | ((?: 43 | (pop|push)| 44 | (cp|in|s[lr]a|s[lr]l|slia|sl1|sub|and|te?st|x?or|mul)| 45 | (ex|ld|inc|dec|adc|add|sbc) 46 | ) 47 | \s+)[[(]?([a-z]\w*)?$`, 48 | shouldSuggest2ArgRegister: mkRegex` 49 | ( 50 | adc|add|bit|ex|ld|out|res|r[lr]c?|set| 51 | s[lr]a|s[lr]l|slia|sl1|sbc| 52 | nextreg|bs[lr]a|bsr[lf]|brlc 53 | ) 54 | \s+(\w+|\([^\)]+?\)|\[[^\]]+?\]),\s*?[[(]?([^[(\n]*)$`, 55 | shouldSuggestConditionals: /(j[pr]|call|ret)\s+$/, 56 | defineExpression: mkRegex` 57 | ^\@?([\w\.]+)\:?\s+( 58 | inc(?:bin|hob|trd)|b?include|includelua|insert|binary| 59 | inc(?:l4[89]|lz4|zx7|exo)|read| 60 | def[bdghlmswir]|d[bcghmswz]|abyte[cz]?|byte|d?word|hex 61 | )\s+([^\$;]+)(;.*)?$`, 62 | defineFileExpression: /^(inc\w*|insert|binary|read)$/i, 63 | keyword: mkRegex`^( 64 | equ|eval|[fr]?org|end|end?t|align|(?:de|un)?phase|shift| 65 | save(?:bin|dev|hob|nex|sna|tap|trd)|empty(?:tap|trd)| 66 | inc(?:bin|hob|trd)|b?include|includelua|insert|binary|out(?:put|end)|tap(?:out|end)| 67 | fpos|fname|slot|size|opt|page|newpage|radix|outradix|encoding|charset|codepage| 68 | macexp_(?:dft|ovr)|listing|(?:end)?(?:struct|section|switch|lua|maxnest)| 69 | cpu|device|proc|label|local|global|shared|public|forward|export| 70 | e?dup|block|rept|macro|end[mprsw]|exitm|module|endmod(?:ule)?|(?:de|un)?define| 71 | disp|textarea|map|mmu|field|defarray|segment|restore|pushv|popv|enum|enumconf|nextenum| 72 | list|nolist|let|labelslist|bplist|setbp|setbreakpoint|cspectmap| 73 | assert|fatal|error|warning|message|display|print|fail| 74 | shellexec|amsdos|breakpoint|buildcpr|buildsna|run|save|setcpc|setcrtc| 75 | repeat|until|(?:else|end)?case|default|break|stop|while|[rw]end|function| 76 | inc(?:l4[89]|lz4|zx7|exo)|lz(?:4[89]?|w7|exo|close)|read| 77 | bank|bankset|limit|protect|write\s+direct|str| 78 | def[bdlmswir]|d[bcdszw]|abyte[cz]?|byte|d?word|hex| 79 | if|ifn?def|ifn?used|ifn?exist|else(?:if)?|endif| 80 | ad[cd]|and|bit|call|ccf|cp|cp[di]r?|cpl|daa|dec|[de]i|djnz|ex[adx]?|halt| 81 | i[mn]|inc|in[di]r?|j[pr]|ld|ld[di]r?|neg|nop|ot[di]r|out|out[di]| 82 | pop|push|res|ret[in]?|rla?|rlca?|r[lr]d|rra?|rrca?|rst|sbc|scf|set| 83 | s[lr]a|s[lr]l|slia|sl1|sub|x?or| 84 | swap|ldir?x|ldws|lddr?x|ldpirx|outinb|swapnib| 85 | mul|mirror|nextreg|pixel(ad|dn)|setae|te?st| 86 | bs[lr]a|bsr[lf]|brlc 87 | )$`, 88 | }; 89 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { 3 | FormatProcessor, 4 | Z80DocumentFormatter, 5 | Z80DocumentRangeFormatter, 6 | Z80TypingFormatter 7 | } from './formatProcessor'; 8 | import { SymbolProcessor } from './symbolProcessor'; 9 | import { Z80CompletionProvider } from './completionProvider'; 10 | import { Z80DefinitionProvider } from './definitionProvider'; 11 | import { Z80HoverProvider } from './hover'; 12 | import { Z80RenameProvider } from './renameProvider'; 13 | import { Z80DocumentSymbolProvider, Z80WorkspaceSymbolProvider } from './symbolProvider'; 14 | 15 | export const EXTENSION_LANGUAGE_ID = 'z80-macroasm'; 16 | 17 | let changeConfigSubscription: vscode.Disposable | undefined; 18 | let symbolProcessor: SymbolProcessor | undefined; 19 | let formatProcessor: FormatProcessor | undefined; 20 | 21 | export function activate(ctx: vscode.ExtensionContext) { 22 | configure(ctx); 23 | 24 | // subscribe for every configuration change 25 | changeConfigSubscription = vscode.workspace.onDidChangeConfiguration(event => { 26 | configure(ctx, event); 27 | }); 28 | } 29 | 30 | export function deactivate() { 31 | if (symbolProcessor) { 32 | symbolProcessor.destroy(); 33 | symbolProcessor = undefined; 34 | } 35 | if (changeConfigSubscription) { 36 | changeConfigSubscription.dispose(); 37 | changeConfigSubscription = undefined; 38 | } 39 | } 40 | 41 | function configure(ctx: vscode.ExtensionContext, event?: vscode.ConfigurationChangeEvent) { 42 | const settings = vscode.workspace.getConfiguration(EXTENSION_LANGUAGE_ID); 43 | const languageSelector: vscode.DocumentFilter = { language: EXTENSION_LANGUAGE_ID, scheme: 'file' }; 44 | 45 | // test if changing specific configuration 46 | let formatterEnableDidChange = false; 47 | if (event && event.affectsConfiguration(EXTENSION_LANGUAGE_ID)) { 48 | formatterEnableDidChange = settings.format.enabled !== (formatProcessor?.settings.format.enabled || false); 49 | 50 | if (symbolProcessor) { 51 | symbolProcessor.settings = settings; 52 | } 53 | if (formatProcessor) { 54 | formatProcessor.settings = settings; 55 | } 56 | 57 | if (!formatterEnableDidChange) { 58 | return; 59 | } 60 | } 61 | 62 | // dispose previously created providers 63 | let provider: vscode.Disposable | undefined; 64 | while ((provider = ctx.subscriptions.pop()) != null) { 65 | provider.dispose(); 66 | } 67 | 68 | if (!formatterEnableDidChange || !symbolProcessor) { 69 | // dispose previously created symbol processor 70 | if (symbolProcessor) { 71 | symbolProcessor.destroy(); 72 | } 73 | symbolProcessor = new SymbolProcessor(settings); 74 | } 75 | 76 | if (settings.format.enabled) { 77 | // create format processor if not exists 78 | if (!formatProcessor) { 79 | formatProcessor = new FormatProcessor(settings); 80 | } 81 | 82 | ctx.subscriptions.push( 83 | vscode.languages.registerDocumentFormattingEditProvider(languageSelector, new Z80DocumentFormatter(formatProcessor)), 84 | vscode.languages.registerDocumentRangeFormattingEditProvider(languageSelector, new Z80DocumentRangeFormatter(formatProcessor)), 85 | vscode.languages.registerOnTypeFormattingEditProvider(languageSelector, new Z80TypingFormatter(formatProcessor), ' ', ',', ';', ':', '\n'), 86 | ); 87 | } 88 | 89 | // create subscriptions for all providers 90 | ctx.subscriptions.push( 91 | vscode.languages.registerCompletionItemProvider(languageSelector, new Z80CompletionProvider(symbolProcessor), ',', '.', ' '), 92 | vscode.languages.registerDefinitionProvider(languageSelector, new Z80DefinitionProvider(symbolProcessor)), 93 | vscode.languages.registerDocumentSymbolProvider(languageSelector, new Z80DocumentSymbolProvider(symbolProcessor)), 94 | vscode.languages.registerHoverProvider(languageSelector, new Z80HoverProvider(symbolProcessor)), 95 | vscode.languages.registerRenameProvider(languageSelector, new Z80RenameProvider(symbolProcessor)), 96 | vscode.languages.registerWorkspaceSymbolProvider(new Z80WorkspaceSymbolProvider(symbolProcessor)), 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /src/formatProcessor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ConfigProps, ConfigPropsProvider } from './configProperties'; 3 | import regex from './defs_regex'; 4 | import { safeSplitStringByChar } from './utils'; 5 | 6 | interface LinePartFrag { 7 | keyword: string; 8 | fillSpace?: boolean; 9 | firstParam?: string; 10 | args?: string[]; 11 | } 12 | 13 | interface LineParts extends LinePartFrag { 14 | label?: string; 15 | colonAfterLabel?: boolean; 16 | fragments?: LinePartFrag[]; 17 | } 18 | 19 | interface GenerateIndentProps extends ConfigProps { 20 | level: number; 21 | snippet?: string; 22 | keepAligned?: boolean; 23 | } 24 | 25 | interface AdjustKeywordCaseProps extends ConfigProps { 26 | keyword: string; 27 | checkRegsOrConds?: boolean; 28 | } 29 | 30 | type FormatProcessorOutput = vscode.ProviderResult; 31 | type EvalSpecificRegExpExecArray = RegExpExecArray & { notIndented?: boolean } | null; 32 | 33 | export class FormatProcessor extends ConfigPropsProvider { 34 | private _adjustKeywordCase({ keyword, checkRegsOrConds, ...opt }: AdjustKeywordCaseProps): string { 35 | if (opt.uppercaseKeywords !== 'auto' && ( 36 | regex.keyword.test(keyword) || 37 | (checkRegsOrConds && regex.regsOrConds.test(keyword)) 38 | )) { 39 | if (opt.uppercaseKeywords) { 40 | return keyword.toUpperCase(); 41 | } 42 | else { 43 | return keyword.toLowerCase(); 44 | } 45 | } 46 | 47 | return keyword; 48 | } 49 | 50 | private _generateIndent({ level, snippet, keepAligned, ...opt }: GenerateIndentProps) { 51 | const tabsSize = opt.indentSize * level; 52 | 53 | let prepend = ''; 54 | let fillSpacesAfterSnippet = tabsSize; 55 | if (snippet) { 56 | prepend += snippet; 57 | if (keepAligned && snippet.length >= tabsSize) { 58 | prepend += opt.eol; 59 | } 60 | else { 61 | while (snippet.length >= fillSpacesAfterSnippet) { 62 | fillSpacesAfterSnippet += opt.indentSize; 63 | } 64 | 65 | fillSpacesAfterSnippet -= snippet.length; 66 | } 67 | } 68 | 69 | if (opt.indentSpaces) { 70 | return prepend + ' '.repeat(fillSpacesAfterSnippet); 71 | } 72 | else { 73 | return prepend + '\t'.repeat( 74 | Math.ceil(fillSpacesAfterSnippet / opt.indentSize) 75 | ); 76 | } 77 | } 78 | 79 | private _processFragment(frag: string): LinePartFrag { 80 | const [, keyword = frag, rest ] = frag.match(/^(\S+)\s+(.*)$/) || []; 81 | const args: string[] = []; 82 | if (typeof rest === 'string') { 83 | if (rest.includes(',')) { 84 | safeSplitStringByChar(rest, ',').forEach((arg, idx) => { 85 | args.push(idx ? arg.trimStart() : arg); 86 | }); 87 | } 88 | else { 89 | args.push(rest); 90 | } 91 | } 92 | return { keyword, args }; 93 | } 94 | 95 | //--------------------------------------------------------------------------------------- 96 | format( 97 | document: vscode.TextDocument, 98 | range: vscode.Range, 99 | isOnType: boolean = false 100 | ): FormatProcessorOutput { 101 | const configProps = this.getConfigProps(document); 102 | const startLineNumber = document.lineAt(range.start).lineNumber; 103 | const endLineNumber = document.lineAt(range.end).lineNumber; 104 | const commaAfterArgument = ',' + (configProps.spaceAfterArgument ? ' ' : ''); 105 | const fragmentSeparator = `${configProps.spaceAfterInstruction ? ' ' : ''}: `; 106 | 107 | const output: vscode.TextEdit[] = []; 108 | 109 | for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; ++lineNumber) { 110 | const line = document.lineAt(lineNumber); 111 | 112 | if (line.range.isEmpty) { 113 | continue; 114 | } 115 | else if (line.isEmptyOrWhitespace) { 116 | // trim whitespace-filled lines 117 | output.push(new vscode.TextEdit(line.range, '')); 118 | continue; 119 | } 120 | 121 | let range = new vscode.Range(line.range.start, line.range.end); 122 | 123 | let text = line.text; 124 | let indentLevel = -1; 125 | const lineParts: LineParts = {} as any; 126 | 127 | const commentLineMatch = regex.commentLine.exec(text); 128 | if (commentLineMatch) { 129 | continue; 130 | } 131 | 132 | const endCommentMatch = regex.endComment.exec(text); 133 | if (endCommentMatch) { 134 | let idx = endCommentMatch.index; 135 | while (/\s/.test(text[idx - 1])) { 136 | idx--; 137 | } 138 | 139 | range = new vscode.Range(range.start, range.end.translate(0, idx - text.length)); 140 | text = text.substring(0, idx); 141 | } 142 | 143 | const evalMatch: EvalSpecificRegExpExecArray = regex.evalExpression.exec(text); 144 | if (evalMatch) { 145 | const [ fullMatch, label, keyword, argument ] = evalMatch; 146 | 147 | indentLevel = configProps.baseIndent; 148 | lineParts.args = [ argument ]; 149 | lineParts.label = `${fullMatch[0] === '@' ? '@' : ''}${label}`; 150 | 151 | if (keyword[0] === ':') { 152 | lineParts.colonAfterLabel = true; 153 | lineParts.keyword = keyword.slice(1).trim(); 154 | } 155 | else { 156 | lineParts.keyword = keyword.trim(); 157 | } 158 | 159 | if (lineParts.keyword === '=') { 160 | evalMatch.notIndented = !/(\t| {2,})=/.test(fullMatch); 161 | } 162 | 163 | text = text.replace(fullMatch, '').trim(); 164 | } 165 | 166 | const labelMatch = regex.labelDefinition.exec(text); 167 | if (labelMatch) { 168 | const [ fullMatch, label,, colon ] = labelMatch; 169 | 170 | indentLevel = configProps.baseIndent; 171 | lineParts.label = `${fullMatch[0] === '@' ? '@' : ''}${label}`; 172 | lineParts.colonAfterLabel = (colon === ':'); 173 | 174 | text = text.replace(fullMatch, '').trim(); 175 | } 176 | 177 | const trimmedText = isOnType ? text.trimStart() : text.trim(); 178 | const moduleLineMatch = regex.moduleLine.exec(trimmedText); 179 | const macroLineMatch = regex.macroLine.exec(trimmedText); 180 | const controlKeywordMatch = regex.controlKeywordLine.exec(trimmedText); 181 | const trailingWhitespaceMatch = /\S(\s*)$/.exec(text); 182 | 183 | if (moduleLineMatch?.index === 0) { 184 | const [, keyword ] = moduleLineMatch; 185 | 186 | indentLevel = configProps.controlIndent; 187 | lineParts.keyword = keyword.trim(); 188 | lineParts.args = [ text.replace(keyword, '').trim() ]; 189 | 190 | text = ''; 191 | } 192 | else if (macroLineMatch?.index === 0) { 193 | const [, keyword, firstParam, rest ] = macroLineMatch; 194 | 195 | indentLevel = configProps.controlIndent; 196 | lineParts.keyword = keyword.trim(); 197 | lineParts.firstParam = firstParam; 198 | lineParts.args = []; 199 | if (typeof rest === 'string') { 200 | if (rest.includes(',')) { 201 | safeSplitStringByChar(rest, ',').forEach((arg, idx) => { 202 | const ret = arg.trimEnd(); 203 | lineParts.args?.push(idx ? ret.trimStart() : ret); 204 | }); 205 | } 206 | else { 207 | lineParts.args.push(rest); 208 | } 209 | } 210 | 211 | text = ''; 212 | } 213 | else if (controlKeywordMatch?.index === 0) { 214 | indentLevel = configProps.controlIndent; 215 | } 216 | 217 | if (text.trim()) { 218 | if (indentLevel < 0) { 219 | indentLevel = configProps.baseIndent; 220 | } 221 | 222 | if (text.includes(':')) { 223 | // stop processing multi-instruction lines on type when it should be splitted 224 | if (configProps.splitInstructionsByColon && isOnType) { 225 | continue; 226 | } 227 | 228 | const splitLine = safeSplitStringByChar(text, ':'); 229 | if (splitLine.length > 1) { 230 | lineParts.fragments = splitLine.map( 231 | frag => this._processFragment(frag.trim()) 232 | ); 233 | 234 | // test if user just not starting to type an instruction at the end of line 235 | const trailingColonMatch = /:\s*$/.exec(text); 236 | if (!configProps.splitInstructionsByColon && isOnType && trailingColonMatch) { 237 | const lastFragmentIndex = lineParts.fragments.length - 1; 238 | lineParts.fragments[lastFragmentIndex].fillSpace = true; 239 | } 240 | } 241 | } 242 | 243 | if (!lineParts.fragments) { 244 | const trimmedText = isOnType ? text.trimStart() : text.trim(); 245 | const { keyword, args } = this._processFragment(trimmedText); 246 | lineParts.keyword = keyword; 247 | lineParts.args = args; 248 | } 249 | } 250 | 251 | const newText: string[] = []; 252 | if (lineParts.label) { 253 | const label = `${lineParts.label}${( 254 | (configProps.colonAfterLabels === 'no-change' && lineParts.colonAfterLabel) || 255 | configProps.colonAfterLabels === true) ? ':' : ''}`; 256 | 257 | if (evalMatch?.notIndented) { 258 | newText.push(`${label} `); 259 | } 260 | else { 261 | newText.push( 262 | this._generateIndent({ 263 | ...configProps, 264 | level: indentLevel, 265 | snippet: label, 266 | keepAligned: true, 267 | }) 268 | ); 269 | } 270 | } 271 | else { 272 | if (indentLevel < 0) { 273 | indentLevel = configProps.indentDetector 274 | .exec(line.text)?.filter(Boolean).slice(1).length || 0; 275 | } 276 | 277 | newText.push( 278 | this._generateIndent({ 279 | ...configProps, 280 | level: indentLevel, 281 | }) 282 | ); 283 | } 284 | 285 | (lineParts.fragments || [{ ...lineParts }]).forEach( 286 | ({ keyword, firstParam, fillSpace, args = [] }, index) => { 287 | if (!keyword && fillSpace == null) { 288 | return; 289 | } 290 | 291 | if (index) { 292 | const lastFrag = newText.pop(); 293 | if (lastFrag) { 294 | newText.push(lastFrag.trimEnd()); 295 | } 296 | 297 | if (configProps.splitInstructionsByColon) { 298 | newText.push( 299 | (configProps.eol + this._generateIndent({ 300 | ...configProps, 301 | level: indentLevel, 302 | })) 303 | ); 304 | } 305 | else if (fillSpace) { 306 | newText.push(fragmentSeparator.trimEnd()); 307 | return; 308 | } 309 | else { 310 | newText.push(fragmentSeparator); 311 | } 312 | 313 | newText.push(`${this._adjustKeywordCase({ ...configProps, keyword })} `); 314 | } 315 | else { 316 | if (configProps.whitespaceAfterInstruction === 'single-space' || 317 | evalMatch?.notIndented) { 318 | 319 | newText.push(`${this._adjustKeywordCase({ ...configProps, keyword })} `); 320 | } 321 | else if (configProps.whitespaceAfterInstruction === 'tab') { 322 | newText.push(`${this._adjustKeywordCase({ ...configProps, keyword })}\t`); 323 | } 324 | else { 325 | newText.push( 326 | this._generateIndent({ 327 | ...configProps, 328 | level: 1, 329 | snippet: this._adjustKeywordCase({ ...configProps, keyword }) 330 | }) 331 | ); 332 | } 333 | } 334 | 335 | if (firstParam) { 336 | newText.push(`${firstParam} `); 337 | } 338 | 339 | let wasOperator = false; 340 | args.flatMap(arg => { 341 | const stringsMatches: string[] = []; 342 | const safeArg = arg.replaceAll(regex.stringBounds, (match) => { 343 | const i = stringsMatches.push(match); 344 | return `【${i.toString().padStart(3, '0')}】`; 345 | }); 346 | 347 | const results: string[] = []; 348 | const matches = safeArg.matchAll(regex.operators); 349 | let beginIndex = 0; 350 | for (const match of matches) { 351 | const { [1]: operator, input, index } = match; 352 | if (input && index) { 353 | results.push(input.slice(beginIndex, index), `⨂${operator.trim()}`); 354 | beginIndex = index + operator.length; 355 | } 356 | } 357 | results.push(safeArg.slice(beginIndex)); 358 | 359 | return results.map((fragment) => 360 | fragment.replaceAll(/【(\d+)】/g, (_, counter) => { 361 | return stringsMatches[parseInt(counter) - 1]; 362 | }) 363 | ); 364 | 365 | }).forEach((value, idx) => { 366 | if (value[0] === '⨂') { 367 | const operator = value.slice(1).trim(); 368 | const space = ( 369 | configProps.spacesAroundOperators || 370 | /[a-z]+/i.test(operator) ? ' ' : '' 371 | ); 372 | 373 | newText.push(space + value.slice(1) + space); 374 | wasOperator = true; 375 | return; 376 | } 377 | 378 | let result, matcher; 379 | if (configProps.bracketType !== 'no-change' && 380 | (matcher = regex.bracketsBounds.exec(value))) { 381 | 382 | const content = matcher[2] || matcher[1] || ''; 383 | result = `${ 384 | configProps.bracketType === 'round' ? '(' : '['}${ 385 | this._adjustKeywordCase({ 386 | ...configProps, 387 | keyword: content, 388 | checkRegsOrConds: true 389 | })}${ 390 | configProps.bracketType === 'round' ? ')' : ']'}`; 391 | } 392 | if (configProps.hexaNumberStyle !== 'no-change' && 393 | (matcher = regex.numerals.exec(value))) { 394 | 395 | let hexa = (matcher[5] || matcher[4]); 396 | if (hexa) { 397 | const [, sign, reparsed ] = /^(\-?)(?:0x|[\$#])?(\w+)h?$/i.exec(hexa) || [ '', '', hexa ]; 398 | hexa = (configProps.hexaNumberCase === 'no-change') ? reparsed : 399 | reparsed[configProps.hexaNumberCase ? 'toUpperCase' : 'toLowerCase'](); 400 | switch (configProps.hexaNumberStyle) { 401 | case 'hash': 402 | result = `${sign}#${hexa}`; break; 403 | case 'motorola': 404 | result = `${sign}$${hexa}`; break; 405 | case 'intel': 406 | result = `${sign}${hexa.charCodeAt(0) > 64 ? '0' : ''}${hexa}h`; break; 407 | case 'intel-uppercase': 408 | result = `${sign}${hexa.charCodeAt(0) > 64 ? '0' : ''}${hexa}h`; break; 409 | case 'c-style': 410 | result = `${sign}0x${hexa}`; break; 411 | } 412 | } 413 | } 414 | 415 | if (idx && !wasOperator) { 416 | newText.push(commaAfterArgument); 417 | } 418 | newText.push( 419 | (result || this._adjustKeywordCase({ 420 | ...configProps, 421 | keyword: value, 422 | checkRegsOrConds: true 423 | })) 424 | ); 425 | 426 | wasOperator = false; 427 | }); 428 | } 429 | ); 430 | 431 | let result = newText.join('').trimEnd(); 432 | if (isOnType && trailingWhitespaceMatch) { 433 | result += trailingWhitespaceMatch[1]; 434 | } 435 | 436 | output.push(new vscode.TextEdit(range, result)); 437 | } 438 | 439 | return output; 440 | } 441 | } 442 | 443 | export class Z80DocumentFormatter implements vscode.DocumentFormattingEditProvider { 444 | constructor(public formatter: FormatProcessor) {} 445 | 446 | provideDocumentFormattingEdits(document: vscode.TextDocument): FormatProcessorOutput { 447 | const range = new vscode.Range(document.positionAt(0), document.positionAt(document.getText().length - 1)); 448 | return this.formatter.format(document, range); 449 | } 450 | } 451 | 452 | export class Z80DocumentRangeFormatter implements vscode.DocumentRangeFormattingEditProvider { 453 | constructor(public formatter: FormatProcessor) {} 454 | 455 | provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range): FormatProcessorOutput { 456 | return this.formatter.format(document, range); 457 | } 458 | } 459 | 460 | export class Z80TypingFormatter implements vscode.OnTypeFormattingEditProvider { 461 | constructor(public formatter: FormatProcessor) {} 462 | 463 | provideOnTypeFormattingEdits(document: vscode.TextDocument, position: vscode.Position, ch: string): FormatProcessorOutput { 464 | // if enter is pressed, format line that was being edited before, not the new line 465 | let line = position.line; 466 | if (ch === '\n' && line > 0) { 467 | line--; 468 | } 469 | 470 | return this.formatter.format(document, document.lineAt(line).range, ch !== '\n'); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/hover.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ProcessorResult, SymbolProcessor } from './symbolProcessor'; 3 | 4 | export class Z80HoverProvider implements vscode.HoverProvider { 5 | constructor(public symbolProcessor: SymbolProcessor) {} 6 | 7 | provideHover( 8 | document: vscode.TextDocument, 9 | position: vscode.Position, 10 | token: vscode.CancellationToken 11 | ): Thenable { 12 | return this.symbolProcessor.getFullSymbolAtDocPosition( 13 | document, position, token, ProcessorResult.HOVER 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/renameProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import regex from './defs_regex'; 3 | import { 4 | ProcessorResult, 5 | SymbolProcessor, 6 | SymbolDescriptorExt 7 | } from './symbolProcessor'; 8 | 9 | export class Z80RenameProvider implements vscode.RenameProvider { 10 | constructor(public symbolProcessor: SymbolProcessor) {} 11 | 12 | prepareRename( 13 | document: vscode.TextDocument, 14 | position: vscode.Position 15 | ): vscode.ProviderResult { 16 | 17 | const lineText = document.lineAt(position.line).text; 18 | const includeLineMatch = regex.includeLine.exec(lineText); 19 | if (includeLineMatch) { 20 | throw 'You cannot rename a includes.'; 21 | } 22 | 23 | const commentLineMatch = regex.endComment.exec(lineText); 24 | if (commentLineMatch && position.character >= commentLineMatch.index) { 25 | throw 'You cannot rename a comments.'; 26 | } 27 | 28 | let range = document.getWordRangeAtPosition(position, regex.stringBounds); 29 | if (range && !range.isEmpty) { 30 | throw 'You cannot rename a strings.'; 31 | } 32 | 33 | range = document.getWordRangeAtPosition(position, regex.numerals); 34 | if (range && !range.isEmpty) { 35 | throw 'You cannot rename a numerals.'; 36 | } 37 | 38 | range = document.getWordRangeAtPosition(position); 39 | if (!range) { 40 | throw null; 41 | } 42 | 43 | const activeLinePart = lineText.substr(0, range.end.character); 44 | const keywordMatch = regex.shouldSuggestInstruction.exec(activeLinePart); 45 | 46 | if (keywordMatch && regex.keyword.test(keywordMatch[4])) { 47 | throw 'You cannot rename a keywords.'; 48 | } 49 | 50 | const arg1RegisterMatch = regex.shouldSuggest1ArgRegister.exec(activeLinePart); 51 | const arg2RegisterMatch = regex.shouldSuggest2ArgRegister.exec(activeLinePart); 52 | const condFlagsMatch = regex.condFlags.exec(activeLinePart); 53 | 54 | if ((condFlagsMatch && condFlagsMatch[2]) || 55 | (arg2RegisterMatch && arg2RegisterMatch[3] && 56 | regex.registers.test(arg2RegisterMatch[3])) || 57 | (arg1RegisterMatch && arg1RegisterMatch[5] && 58 | regex.registers.test(arg1RegisterMatch[5]))) { 59 | 60 | throw 'You cannot rename a registers or flags.'; 61 | } 62 | 63 | return range; 64 | } 65 | 66 | provideRenameEdits( 67 | document: vscode.TextDocument, 68 | position: vscode.Position, 69 | newName: string, 70 | token: vscode.CancellationToken 71 | ): Thenable { 72 | 73 | return this._processSymbolAtDocPosition(document, position, newName, token); 74 | } 75 | 76 | 77 | /** 78 | * Provide defined symbol for 'Go to Definition' or symbol hovering. 79 | * @param document Text document object. 80 | * @param position Cursor position. 81 | * @param newName Target rename string. 82 | * @param token Cancellation token object. 83 | * @returns Promise. 84 | */ 85 | private async _processSymbolAtDocPosition( 86 | document: vscode.TextDocument, 87 | position: vscode.Position, 88 | newName: string, 89 | token: vscode.CancellationToken): Promise { 90 | 91 | const wsEdit = new vscode.WorkspaceEdit(); 92 | 93 | const range = document.getWordRangeAtPosition(position, regex.partialLabel); 94 | if (!range || (range && (range.isEmpty || !range.isSingleLine))) { 95 | return wsEdit; 96 | } 97 | 98 | const partialLabelMatch = regex.partialLabel.exec(document.getText(range)); 99 | if (!partialLabelMatch) { 100 | return wsEdit; 101 | } 102 | 103 | const oldName = partialLabelMatch[1]; 104 | 105 | let wasLocalLabel = (partialLabelMatch[0][0] === '.'); 106 | let symbol: SymbolDescriptorExt | null = null; 107 | 108 | if (wasLocalLabel) { 109 | symbol = await this.symbolProcessor.getFullSymbolAtDocPosition( 110 | document, position, token, ProcessorResult.SYMBOL_FULL 111 | ); 112 | 113 | if (token.isCancellationRequested) { 114 | return wsEdit; 115 | } 116 | } 117 | 118 | if (symbol == null) { 119 | symbol = await this.symbolProcessor.getFullSymbolAtDocPosition( 120 | document, position, token, ProcessorResult.SYMBOL 121 | ); 122 | 123 | if (token.isCancellationRequested) { 124 | return wsEdit; 125 | } 126 | } 127 | 128 | if (symbol == null && !wasLocalLabel) { 129 | symbol = await this.symbolProcessor.getFullSymbolAtDocPosition( 130 | document, position, token, ProcessorResult.SYMBOL_FULL 131 | ); 132 | 133 | if (token.isCancellationRequested) { 134 | return wsEdit; 135 | } 136 | } 137 | 138 | if (symbol != null && symbol.labelFull) { 139 | wasLocalLabel = symbol.localLabel; 140 | 141 | const files = this.symbolProcessor.filesWithIncludes(document); 142 | for (const uri in files) { 143 | const doc = await vscode.workspace.openTextDocument(uri); 144 | 145 | if (token.isCancellationRequested) { 146 | return wsEdit; 147 | } 148 | 149 | const moduleStack: string[] = []; 150 | let lastFullLabel: string | null = files[uri].labelPath[0]; 151 | 152 | for (let lineNumber = 0; lineNumber < doc.lineCount; lineNumber++) { 153 | const line = doc.lineAt(lineNumber); 154 | if (!line || line.isEmptyOrWhitespace) { 155 | continue; 156 | } 157 | 158 | let lineText = line.text; 159 | 160 | const commentLineMatch = regex.endComment.exec(lineText); 161 | const includeLineMatch = regex.includeLine.exec(lineText); 162 | const moduleLineMatch = regex.moduleLine.exec(lineText); 163 | const macroLineMatch = regex.macroLine.exec(lineText); 164 | const labelMatch = regex.labelDefinition.exec(lineText); 165 | 166 | if (commentLineMatch) { 167 | // remove comment from line to prevent false match 168 | lineText = lineText.replace(commentLineMatch[0], ''); 169 | } 170 | else if (includeLineMatch) { 171 | // remove include from line to prevent false match 172 | lineText = lineText.replace(includeLineMatch[0], ''); 173 | } 174 | else if (macroLineMatch) { 175 | const macroName = macroLineMatch[2]; 176 | 177 | // test if we renaming specific macro 178 | if (symbol.kind === vscode.SymbolKind.Function && macroName === oldName) { 179 | const start = macroLineMatch.index + macroLineMatch[1].length; 180 | const range = new vscode.Range( 181 | symbol.line, start, 182 | symbol.line, start + macroName.length 183 | ); 184 | 185 | wsEdit.replace(symbol.location.uri, range, newName); 186 | } 187 | 188 | // remove macro definition to prevent false match 189 | lineText = lineText.replace(macroLineMatch[0], ''); 190 | } 191 | 192 | const labelPath = []; 193 | if (labelMatch) { 194 | labelPath.push(labelMatch[1]); 195 | 196 | let declaration = labelMatch[1]; 197 | if (declaration[0] === '.') { 198 | if (lastFullLabel) { 199 | labelPath.unshift(lastFullLabel); 200 | declaration = lastFullLabel + declaration; 201 | } 202 | } 203 | else if (declaration.indexOf('$$') < 0) { 204 | lastFullLabel = declaration; 205 | } 206 | 207 | if (moduleStack.length && labelMatch[0][0] !== '@') { 208 | labelPath.unshift(moduleStack[0]); 209 | declaration = `${moduleStack[0]}.${declaration}`; 210 | } 211 | } 212 | 213 | if (moduleLineMatch) { 214 | const declaration = moduleLineMatch[2]; 215 | 216 | if (symbol.kind === vscode.SymbolKind.Module && declaration === oldName) { 217 | const start = line.firstNonWhitespaceCharacterIndex + moduleLineMatch[1].length; 218 | const range = new vscode.Range( 219 | lineNumber, start, 220 | lineNumber, start + declaration.length 221 | ); 222 | 223 | wsEdit.replace(doc.uri, range, newName); 224 | } 225 | 226 | moduleStack.unshift(declaration); 227 | 228 | // remove module definition to prevent false match 229 | lineText = lineText.replace(moduleLineMatch[0], ''); 230 | } 231 | else if (regex.endmoduleLine.test(lineText)) { 232 | moduleStack.shift(); 233 | continue; 234 | } 235 | 236 | // remove strings to prevent false match 237 | lineText = lineText.replace(regex.stringBounds, ''); 238 | 239 | // nothing left on the line... 240 | if (!lineText.trim()) { 241 | continue; 242 | } 243 | 244 | let replacementPhrase = 245 | wasLocalLabel ? '(?:(\\b\\w+)\\.|\\.)' : '(?:(\\b\\w+)\\.)?'; 246 | replacementPhrase += oldName + '\\b'; 247 | 248 | const matches = lineText.matchAll(RegExp(replacementPhrase, 'g')); 249 | for (const match of matches) { 250 | const [ phrase, prefix ] = match; 251 | 252 | const localLabel = (phrase[0] === '.'); 253 | if (( 254 | localLabel && symbol.labelFull && 255 | !symbol.labelFull.endsWith(lastFullLabel + phrase) 256 | ) || 257 | ( 258 | !localLabel && !prefix && 259 | symbol.labelPart && symbol.path.length > 1 && 260 | !(phrase === symbol.labelPart && 261 | moduleStack[0] === symbol.path[0]) 262 | )) { 263 | 264 | continue; 265 | } 266 | 267 | let start = match.index || 0; 268 | if (prefix) { 269 | if (phrase !== symbol.labelFull && prefix !== symbol.path[0]) { 270 | continue; 271 | } 272 | 273 | start += prefix.length + 1; 274 | } 275 | else if (localLabel) { 276 | start++; 277 | } 278 | 279 | const range = new vscode.Range( 280 | lineNumber, start, 281 | lineNumber, start + oldName.length 282 | ); 283 | 284 | wsEdit.replace(doc.uri, range, newName); 285 | } 286 | } 287 | } 288 | } 289 | 290 | return wsEdit; 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/symbolProcessor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import regex from './defs_regex'; 4 | 5 | export const enum ProcessorResult { 6 | DEFINITION, HOVER, SYMBOL, SYMBOL_FULL 7 | } 8 | 9 | export interface SymbolDescriptorExt { 10 | kind: vscode.SymbolKind; 11 | declaration: string; 12 | path: string[]; 13 | location: vscode.Location; 14 | line: number; 15 | documentation?: string; 16 | localLabel: boolean; 17 | labelPart?: string; 18 | labelFull?: string; 19 | context?: vscode.TextDocument; 20 | } 21 | 22 | export class SymbolDescriptor implements SymbolDescriptorExt { 23 | constructor( 24 | public declaration: string, 25 | public path: string[], 26 | public location: vscode.Location, 27 | public line = location.range.start.line, 28 | public documentation?: string, 29 | public kind: vscode.SymbolKind = vscode.SymbolKind.Variable, 30 | public localLabel: boolean = false) {} 31 | } 32 | 33 | class IncludeDescriptor { 34 | constructor( 35 | public declaration: string, 36 | public labelPath: string[], 37 | public fullPath: string, 38 | public location: vscode.Location, 39 | public line = location.range.start.line) {} 40 | } 41 | 42 | class FileTable { 43 | includes: IncludeDescriptor[] = []; 44 | symbols: SymbolDescriptor[] = []; 45 | } 46 | 47 | type SymbolProcessorMultitype = 48 | vscode.Definition | vscode.Hover | SymbolDescriptorExt; 49 | 50 | export type SymbolMap = { [name: string]: SymbolDescriptor }; 51 | 52 | export class SymbolProcessor { 53 | private _watcher: vscode.FileSystemWatcher; 54 | 55 | files: { [name: string]: FileTable } = {}; 56 | 57 | 58 | constructor(public settings: vscode.WorkspaceConfiguration) { 59 | const fileUriHandler = ((uri: vscode.Uri) => { 60 | vscode.workspace.openTextDocument(uri).then(doc => this._document(doc)); 61 | }); 62 | 63 | vscode.workspace 64 | .findFiles(settings.files.include, settings.files.exclude) 65 | .then(files => files.forEach(fileUriHandler)); 66 | 67 | vscode.workspace.onDidChangeTextDocument( 68 | (event: vscode.TextDocumentChangeEvent) => this._document(event.document) 69 | ); 70 | 71 | this._watcher = vscode.workspace.createFileSystemWatcher(settings.files.include); 72 | this._watcher.onDidChange(fileUriHandler); 73 | this._watcher.onDidCreate(fileUriHandler); 74 | this._watcher.onDidDelete((uri) => { 75 | delete this.files[uri.fsPath]; 76 | }); 77 | } 78 | 79 | destroy() { 80 | this._watcher.dispose(); 81 | } 82 | 83 | /** 84 | * Seeks symbols for use by Intellisense in the file at `fsPath`. 85 | * @param fsPath The path of the file to seek in. 86 | * @param output The collection of discovered symbols. 87 | * @param prependPath Path fragments which should be prepended. 88 | * @param searched Paths of files that have already been searched. 89 | */ 90 | private async _seekSymbols( 91 | fsPath: string, 92 | output: SymbolMap, 93 | prependPath: string[] = [], 94 | searched: string[] = [] 95 | ) { 96 | try { 97 | // Open missing document and process... 98 | const doc = await vscode.workspace.openTextDocument(fsPath); 99 | if (doc.isClosed) { 100 | // This shouldn't happen, but just in case... 101 | searched.push(fsPath); 102 | return; 103 | } 104 | if (fsPath !== doc.uri.fsPath) { 105 | console.warn(`Fixing file path: "${fsPath}" => "${doc.uri.fsPath}"`); 106 | if (this.files[fsPath]) { 107 | // Something went wrong, remove invalid indexed entry 108 | delete this.files[fsPath]; 109 | } 110 | fsPath = doc.uri.fsPath; 111 | } 112 | if (!this.files[fsPath]) { 113 | this._document(doc); 114 | } 115 | } 116 | catch(e) { 117 | // File not found, probably non-existent include 118 | searched.push(fsPath); 119 | return; 120 | } 121 | 122 | const table = this.files[fsPath]; 123 | searched.push(fsPath); 124 | 125 | for (const symbol of table.symbols) { 126 | const fullPath = [ ...prependPath, ...symbol.path ]; 127 | 128 | for (let i = fullPath.length - 1; i >= 0; i--) { 129 | const name = fullPath.slice(i).join('.').replace('..', '.'); 130 | 131 | if (!(name in output)) { 132 | output[name] = symbol; 133 | } 134 | } 135 | } 136 | 137 | await Promise.all(table.includes.map(async include => { 138 | if (searched.indexOf(include.fullPath) < 0) { 139 | await this._seekSymbols(include.fullPath, output, include.labelPath, searched); 140 | } 141 | })); 142 | } 143 | 144 | /** 145 | * Returns an include descriptor within scope of `context` on given `line`. 146 | * @param context The document to find symbols for. 147 | * @param declaration Include relative path 148 | * @param line Line number of including in file 149 | */ 150 | private _getInclude(context: vscode.TextDocument, declaration: string, line: number) { 151 | const fsPath = context.uri.fsPath; 152 | const table = this.files[fsPath]; 153 | 154 | if (table) { 155 | const include = table.includes.find( 156 | i => (i.declaration === declaration && i.line === line) 157 | ); 158 | 159 | if (include) { 160 | const doc = vscode.Uri.file(include.fullPath); 161 | const range = new vscode.Range(0, 0, 0, 0); 162 | 163 | return { 164 | ...include, 165 | destLocation: new vscode.Location(doc, range) 166 | }; 167 | } 168 | } 169 | 170 | return null; 171 | } 172 | 173 | /** 174 | * Get file uri list for given document including all its 175 | * included files recursively. 176 | * @param {vscode.TextDocument} doc 177 | */ 178 | filesWithIncludes(context: vscode.TextDocument) { 179 | const fsPath = context.uri.fsPath; 180 | const table = this.files[fsPath]; 181 | 182 | const result: { [name: string]: IncludeDescriptor } = {}; 183 | const putAllToResults = (t: FileTable): any => t?.includes 184 | .filter(i => (!result[i.fullPath]) && (result[i.fullPath] = i) && true) 185 | .forEach(i => putAllToResults(this.files[i.fullPath])); 186 | 187 | if (table) { 188 | result[fsPath] = { 189 | declaration: context.fileName, 190 | fullPath: fsPath, 191 | labelPath: [] 192 | }; 193 | 194 | putAllToResults(table); 195 | } 196 | 197 | return result; 198 | } 199 | 200 | /** 201 | * Returns a set of symbols possibly within scope of `context`. 202 | * @param context The document to find symbols for. 203 | */ 204 | async symbols(context: vscode.TextDocument): Promise { 205 | const output: SymbolMap = {}; 206 | const searched: string[] = []; 207 | 208 | await this._seekSymbols(context.uri.fsPath, output, [], searched); 209 | 210 | if (this.settings.seekSymbolsThroughWorkspace) { 211 | for (const filepath in this.files) { 212 | if (searched.length && !searched.includes(filepath)) { 213 | await this._seekSymbols(filepath, output, [], searched); 214 | } 215 | } 216 | } 217 | 218 | return output; 219 | } 220 | 221 | /** 222 | * Provide a list of symbols for both 'Go to Symbol in...' functions. 223 | * @param fileFilter (optional) URI of specific file in workspace. 224 | * @param query (optional) Part of string to find in symbol name. 225 | * @param token Cancellation token object. 226 | * @returns Promise of SymbolInformation objects array. 227 | */ 228 | async provideSymbols( 229 | fileFilter: string | null, 230 | query: string | null, 231 | token: vscode.CancellationToken 232 | ): Promise { 233 | 234 | const symbols: SymbolMap = {}; 235 | const searched: string[] = []; 236 | const output: vscode.SymbolInformation[] = []; 237 | 238 | if (fileFilter) { 239 | await this._seekSymbols(fileFilter, symbols, [], searched); 240 | } 241 | else { 242 | for (const filepath in this.files) { 243 | if (searched.length && !searched.includes(filepath) && 244 | !token.isCancellationRequested) { 245 | 246 | await this._seekSymbols(filepath, symbols, [], searched); 247 | } 248 | } 249 | } 250 | 251 | if (token.isCancellationRequested) { 252 | return []; 253 | } 254 | 255 | const alreadyProcessedDeclarations: string[] = []; 256 | Object.values(symbols).reverse().forEach(symbol => { 257 | if (!alreadyProcessedDeclarations.includes(symbol.declaration) && 258 | (!query || symbol.declaration.includes(query)) && 259 | (!fileFilter || symbol.location.uri.fsPath.endsWith(fileFilter))) { 260 | 261 | output.push(new vscode.SymbolInformation( 262 | symbol.declaration, symbol.kind, '', symbol.location 263 | )); 264 | 265 | alreadyProcessedDeclarations.push(symbol.declaration); 266 | } 267 | }); 268 | 269 | return output; 270 | } 271 | 272 | /** 273 | * Provide defined symbol for 'Go to Definition' or symbol hovering. 274 | * @param context Text document object. 275 | * @param position Cursor position. 276 | * @param token Cancellation token object. 277 | * @param hoverDocumentation Provide a hover object. 278 | * @returns Promise of T. 279 | */ 280 | getFullSymbolAtDocPosition( 281 | context: vscode.TextDocument, 282 | position: vscode.Position, 283 | token: vscode.CancellationToken, 284 | resultType: ProcessorResult = ProcessorResult.DEFINITION): Promise { 285 | 286 | return (async () => { 287 | const lineText = context.lineAt(position.line).text; 288 | const includeLineMatch = regex.includeLine.exec(lineText); 289 | 290 | if (resultType < ProcessorResult.SYMBOL && includeLineMatch) { 291 | const include = this._getInclude(context, includeLineMatch[4], position.line); 292 | 293 | if (include) { 294 | if (position.character >= include.location.range.start.character && 295 | position.character <= include.location.range.end.character) { 296 | 297 | if (resultType === ProcessorResult.HOVER) { 298 | return new vscode.Hover(include.fullPath, include.location.range); 299 | } 300 | else { 301 | return include.destLocation; 302 | } 303 | } 304 | } 305 | else { 306 | return; 307 | } 308 | } 309 | 310 | const commentLineMatch = regex.endComment.exec(lineText); 311 | if (commentLineMatch && position.character >= commentLineMatch.index) { 312 | return; 313 | } 314 | 315 | let range = context.getWordRangeAtPosition(position, regex.stringBounds); 316 | if (range && !range.isEmpty) { 317 | return; 318 | } 319 | 320 | range = context.getWordRangeAtPosition(position, regex.numerals); 321 | if (range && !range.isEmpty) { 322 | return; 323 | } 324 | 325 | if (resultType === ProcessorResult.SYMBOL) { 326 | range = context.getWordRangeAtPosition(position); 327 | } 328 | else { 329 | range = context.getWordRangeAtPosition(position, regex.fullLabel); 330 | } 331 | 332 | if (!range || (range && (range.isEmpty || !range.isSingleLine))) { 333 | return; 334 | } 335 | 336 | if (resultType < ProcessorResult.SYMBOL) { 337 | const activeLinePart = lineText.substr(0, range.end.character); 338 | const keywordMatch = regex.shouldSuggestInstruction.exec(activeLinePart); 339 | 340 | const arg1RegisterMatch = regex.shouldSuggest1ArgRegister.exec(activeLinePart); 341 | const arg2RegisterMatch = regex.shouldSuggest2ArgRegister.exec(activeLinePart); 342 | const condFlagsMatch = regex.condFlags.exec(activeLinePart); 343 | 344 | if ((condFlagsMatch && condFlagsMatch[2]) || 345 | (keywordMatch && keywordMatch[4] && 346 | regex.keyword.test(keywordMatch[4])) || 347 | (arg2RegisterMatch && arg2RegisterMatch[3] && 348 | regex.registers.test(arg2RegisterMatch[3])) || 349 | (arg1RegisterMatch && arg1RegisterMatch[5] && 350 | regex.registers.test(arg1RegisterMatch[5]))) { 351 | 352 | return; 353 | } 354 | } 355 | 356 | const symbols = await this.symbols(context); 357 | if (token.isCancellationRequested) { 358 | return; 359 | } 360 | 361 | let lbPart = context.getText(range); 362 | let lbFull = lbPart; 363 | let lbParent: string | undefined = undefined; 364 | let lbModule: string | undefined = undefined; 365 | 366 | if (resultType === ProcessorResult.SYMBOL_FULL && lbPart[0] !== '.') { 367 | const lbSplitted = lbFull.split('.'); 368 | if (lbSplitted.length >= 2) { 369 | const testLb = symbols[lbSplitted[0]]; 370 | if (testLb) { 371 | if (testLb.kind === vscode.SymbolKind.Module) { 372 | lbModule = lbSplitted.shift(); 373 | if (lbSplitted.length > 1) { 374 | lbParent = lbSplitted.shift(); 375 | } 376 | 377 | lbFull = lbPart = lbSplitted[0]; 378 | } 379 | else { 380 | lbParent = lbSplitted.shift(); 381 | lbFull = lbPart = `.${lbSplitted[0]}`; 382 | } 383 | } 384 | } 385 | } 386 | 387 | if (!lbParent && !lbModule) { 388 | for (let lineNumber = position.line - 1; lineNumber >= 0; lineNumber--) { 389 | const line = context.lineAt(lineNumber); 390 | if (line.isEmptyOrWhitespace) { 391 | continue; 392 | } 393 | if (lbPart[0] === '.' && !lbParent) { 394 | const parentLabelMatch = line.text.match(regex.parentLabel); 395 | if (parentLabelMatch) { 396 | lbParent = parentLabelMatch[1]; 397 | } 398 | } 399 | const moduleLineMatch = line.text.match(regex.moduleLine); 400 | if (moduleLineMatch) { 401 | lbModule = moduleLineMatch[2]; 402 | break; 403 | } 404 | else if (regex.endmoduleLine.test(line.text)) { 405 | break; 406 | } 407 | } 408 | } 409 | 410 | lbFull = this._enlargeLabel(this._enlargeLabel(lbFull, lbParent), lbModule); 411 | 412 | const symbol: any = symbols[lbFull] || symbols[lbPart]; 413 | if (!symbol) { 414 | return; 415 | } 416 | 417 | if (resultType === ProcessorResult.HOVER) { 418 | return new vscode.Hover(new vscode.MarkdownString(symbol.documentation), range); 419 | } 420 | else if (resultType === ProcessorResult.DEFINITION) { 421 | return symbol.location; 422 | } 423 | else if (resultType >= ProcessorResult.SYMBOL) { 424 | symbol.context = context; 425 | symbol.labelPart = lbPart; 426 | symbol.labelFull = lbFull; 427 | 428 | return symbol; 429 | } 430 | })(); 431 | } 432 | 433 | private _enlargeLabel(base: string, prepend?: string): string { 434 | if (prepend && base.indexOf(prepend) < 0) { 435 | if (base[0] === '.') { 436 | base = prepend + base; 437 | } 438 | else { 439 | base = `${prepend}.${base}`; 440 | } 441 | } 442 | return base; 443 | } 444 | 445 | private _document(document: vscode.TextDocument) { 446 | const table = new FileTable(); 447 | this.files[document.uri.fsPath] = table; 448 | 449 | const moduleStack: string[] = []; 450 | let commentBuffer: string[] = []; 451 | let lastFullLabel: string | null = null; 452 | 453 | for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) { 454 | const line = document.lineAt(lineNumber); 455 | if (line.isEmptyOrWhitespace) { 456 | continue; 457 | } 458 | 459 | const commentLineMatch = regex.commentLine.exec(line.text); 460 | if (commentLineMatch) { 461 | const baseLine = commentLineMatch[1].trim(); 462 | 463 | if (regex.horizontalRule.test(baseLine)) { 464 | continue; 465 | } 466 | 467 | commentBuffer.push(baseLine); 468 | } 469 | else { 470 | const includeLineMatch = regex.includeLine.exec(line.text); 471 | const moduleLineMatch = regex.moduleLine.exec(line.text); 472 | const macroLineMatch = regex.macroLine.exec(line.text); 473 | const labelMatch = regex.labelDefinition.exec(line.text); 474 | const labelPath = []; 475 | 476 | let symbolKind = vscode.SymbolKind.Function; 477 | 478 | if (labelMatch && !parseInt(labelMatch[1])) { 479 | labelPath.push(labelMatch[1]); 480 | 481 | let localLabel: boolean = false; 482 | let declaration = labelMatch[1]; 483 | if (declaration[0] === '.') { 484 | localLabel = true; 485 | 486 | if (lastFullLabel) { 487 | labelPath.unshift(lastFullLabel); 488 | declaration = lastFullLabel + declaration; 489 | } 490 | } 491 | else if (declaration.indexOf('$$') < 0) { 492 | lastFullLabel = declaration; 493 | } 494 | 495 | if (moduleStack.length && labelMatch[0][0] !== '@') { 496 | labelPath.unshift(moduleStack[0]); 497 | declaration = `${moduleStack[0]}.${declaration}`; 498 | } 499 | 500 | const location = new vscode.Location(document.uri, line.range.start); 501 | const defineExpressionMatch = regex.defineExpression.exec(line.text); 502 | const evalExpressionMatch = regex.evalExpression.exec(line.text); 503 | const endCommentMatch = regex.endComment.exec(line.text); 504 | 505 | if (defineExpressionMatch) { 506 | const instruction = (defineExpressionMatch[2] + ' '.repeat(8)).substr(0, 8); 507 | commentBuffer.push('\n```\n' + instruction + defineExpressionMatch[3].trim() + '\n```'); 508 | symbolKind = regex.defineFileExpression.exec(defineExpressionMatch[2]) ? 509 | vscode.SymbolKind.File : 510 | vscode.SymbolKind.Variable; 511 | } 512 | else if (evalExpressionMatch) { 513 | commentBuffer.push('\n`' + evalExpressionMatch[3].trim() + '`'); 514 | symbolKind = vscode.SymbolKind.Constant; 515 | } 516 | 517 | if (endCommentMatch) { 518 | commentBuffer.unshift(endCommentMatch[1].trim()); 519 | } 520 | 521 | table.symbols.push(new SymbolDescriptor( 522 | declaration, 523 | labelPath, 524 | location, 525 | lineNumber, 526 | commentBuffer.join('\n').trim() || undefined, 527 | symbolKind, 528 | localLabel 529 | )); 530 | } 531 | 532 | const pathFragment = [ ...moduleStack.slice(0, 1), ...labelPath.slice(-1) ]; 533 | 534 | if (includeLineMatch) { 535 | const filename = includeLineMatch[4]; 536 | const documentDirname = path.dirname(document.uri.fsPath); 537 | const includeName = path.join(documentDirname, filename); 538 | 539 | let pos = includeLineMatch.index + includeLineMatch[1].length; 540 | const linePos1 = new vscode.Position(lineNumber, pos); 541 | 542 | pos += includeLineMatch[2].length; 543 | const linePos2 = new vscode.Position(lineNumber, pos); 544 | 545 | const range = new vscode.Range(linePos1, linePos2); 546 | const location = new vscode.Location(document.uri, range); 547 | 548 | table.includes.push(new IncludeDescriptor( 549 | filename, 550 | pathFragment, 551 | includeName, 552 | location, 553 | lineNumber 554 | )); 555 | } 556 | else if (macroLineMatch) { 557 | symbolKind = vscode.SymbolKind.Event; 558 | 559 | const declaration = macroLineMatch[2]; 560 | const start = line.firstNonWhitespaceCharacterIndex + macroLineMatch[1].length; 561 | const location = new vscode.Location(document.uri, new vscode.Range( 562 | line.lineNumber, start, 563 | line.lineNumber, start + declaration.length 564 | )); 565 | 566 | const endCommentMatch = regex.endComment.exec(line.text); 567 | if (endCommentMatch) { 568 | commentBuffer.unshift(endCommentMatch[1].trim()); 569 | } 570 | 571 | if (macroLineMatch[3]) { 572 | commentBuffer.unshift('\`' + macroLineMatch[3].trim() + '`\n'); 573 | } 574 | 575 | commentBuffer.unshift(`**macro ${declaration}**`); 576 | pathFragment.push(declaration); 577 | 578 | table.symbols.push(new SymbolDescriptor( 579 | declaration, 580 | pathFragment, 581 | location, 582 | lineNumber, 583 | commentBuffer.join('\n').trim(), 584 | symbolKind 585 | )); 586 | } 587 | else if (moduleLineMatch) { 588 | symbolKind = vscode.SymbolKind.Module; 589 | 590 | const declaration = moduleLineMatch[2]; 591 | const start = line.firstNonWhitespaceCharacterIndex + moduleLineMatch[1].length; 592 | const location = new vscode.Location(document.uri, new vscode.Range( 593 | line.lineNumber, start, 594 | line.lineNumber, start + declaration.length 595 | )); 596 | 597 | moduleStack.unshift(declaration); 598 | commentBuffer.unshift(`**module ${declaration}**\n`); 599 | pathFragment.push(declaration); 600 | 601 | table.symbols.push(new SymbolDescriptor( 602 | declaration, 603 | pathFragment, 604 | location, 605 | lineNumber, 606 | commentBuffer.join('\n').trim(), 607 | symbolKind 608 | )); 609 | } 610 | else if (regex.endmoduleLine.test(line.text)) { 611 | moduleStack.shift(); 612 | } 613 | 614 | commentBuffer = []; 615 | } 616 | } 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /src/symbolProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { SymbolProcessor } from './symbolProcessor'; 3 | 4 | export class Z80DocumentSymbolProvider implements vscode.DocumentSymbolProvider { 5 | constructor(public symbolProcessor: SymbolProcessor) {} 6 | 7 | provideDocumentSymbols( 8 | document: vscode.TextDocument, 9 | token: vscode.CancellationToken 10 | ): Thenable { 11 | 12 | return this.symbolProcessor.provideSymbols(document.fileName, null, token); 13 | } 14 | } 15 | 16 | export class Z80WorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider { 17 | constructor(public symbolProcessor: SymbolProcessor) {} 18 | 19 | provideWorkspaceSymbols( 20 | query: string, 21 | token: vscode.CancellationToken 22 | ): vscode.ProviderResult { 23 | 24 | return this.symbolProcessor.provideSymbols(null, query, token); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import regex from './defs_regex'; 2 | 3 | const utils = { 4 | safeSplitStringByChar: 5 | (input: string, splitter: string) => { 6 | const stringsMatches: string[] = []; 7 | return input 8 | .replaceAll(regex.stringBounds, (match) => { 9 | const i = stringsMatches.push(match); 10 | return `【${i.toString().padStart(3, '0')}】`; 11 | }) 12 | .split(splitter) 13 | .map((fragment) => 14 | fragment.replaceAll(/【(\d+)】/g, (_, counter) => { 15 | return stringsMatches[parseInt(counter) - 1]; 16 | }) 17 | ); 18 | }, 19 | 20 | isFirstLetterUppercase: 21 | (input: string) => (!!input && input[0] >= 'A' && input[0] <= 'Z'), 22 | 23 | uppercaseIfNeeded: 24 | (input: string, ucase: boolean) => (ucase ? input.toUpperCase() : input), 25 | 26 | pad: 27 | (num: number, width: number = 2) => { 28 | const a = num.toString(16); 29 | return ('0000000000' + a).substr(-Math.max(width, a.length)).toUpperCase(); 30 | } 31 | }; 32 | 33 | export = utils; 34 | -------------------------------------------------------------------------------- /syntaxes/z80-macroasm.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "z80-macroasm", 4 | "patterns": [ 5 | { 6 | "include": "#comments" 7 | }, 8 | { 9 | "include": "#keywords" 10 | }, 11 | { 12 | "include": "#strings" 13 | }, 14 | { 15 | "include": "#mnemonics" 16 | }, 17 | { 18 | "include": "#numbers" 19 | }, 20 | { 21 | "include": "#includes" 22 | }, 23 | { 24 | "include": "#labels" 25 | } 26 | ], 27 | "repository": { 28 | "comments": { 29 | "patterns": [ 30 | { 31 | "name": "comment.block.z80asm", 32 | "begin": "/\\*", 33 | "end": "\\*/" 34 | }, 35 | { 36 | "name": "comment.block.z80asm", 37 | "begin": "{", 38 | "end": "}" 39 | }, 40 | { 41 | "name": "comment.line.z80asm", 42 | "begin": ";", 43 | "end": "\\n" 44 | }, 45 | { 46 | "name": "comment.line.z80asm", 47 | "begin": "//", 48 | "end": "\\n" 49 | } 50 | ] 51 | }, 52 | "keywords": { 53 | "patterns": [ 54 | { 55 | "name": "keyword.control.z80asm", 56 | "match": "(?i:(?<=\\s)(?:equ|eval|[fr]?org|end?t|align|(?:de|un)?phase|shift|save(?:bin|dev|hob|nex|sna|tap|trd)|empty(?:tap|trd)|inc(?:bin|hob|trd)|binclude|includelua|insert|binary|end|out(?:put|end)|tap(?:out|end)|fpos|fname|slot|size|opt)\\b)" 57 | }, 58 | { 59 | "name": "keyword.control.z80asm", 60 | "match": "(?i:(?<=\\s)(?:cpu|device|proc|macro|label|local|global|shared|public|forward|rept|e?dup|block|end[mprsw]|exitm|module|endmod(?:ule)?|(?:un)?define|export|disp|textarea|map|mmu|field|defarray|segment|restore|pushv|popv|enum|enumconf|nextenum)\\b)" 61 | }, 62 | { 63 | "name": "keyword.control.z80asm", 64 | "match": "(?i:(?<=\\s)(?:assert|fatal|error|warning|message|display|print|title|prtit|prtexit|fail|shellexec|def[bdghlmswir]|d[bcghmswz]|abyte[cz]?|byte|d?word|hex)\\b)" 65 | }, 66 | { 67 | "name": "keyword.control.z80asm", 68 | "match": "(?i:(?<=\\s)(?:page|newpage|radix|outradix|encoding|charset|codepage|macexp_(?:dft|ovr)|listing|(?:end)?struct|(?:end)?section|maxnest)\\b)" 69 | }, 70 | { 71 | "name": "keyword.control.z80asm", 72 | "match": "(?i:(?<=\\s)(?:if|ifn?def|ifn?used|ifn?exist|else|elseif|endif)\\b)" 73 | }, 74 | { 75 | "name": "keyword.control.z80asm", 76 | "match": "(?i:(?<=\\s)(?:bank|bankset|limit|protect|write\\s+direct|str)\\b)" 77 | }, 78 | { 79 | "name": "keyword.control.z80asm", 80 | "match": "(?i:(?<=\\s)(?:inc(?:l4[89]|lz4|zx7|exo)|lz(?:4[89]?|w7|exo|close)|read)\\b)" 81 | }, 82 | { 83 | "name": "keyword.control.z80asm", 84 | "match": "(?i:(?<=\\s)(?:repeat|while|[rw]end|until|(?:end)?switch|(?:else|end)?case|default|break|stop|function)\\b)" 85 | }, 86 | { 87 | "name": "keyword.control.z80asm", 88 | "match": "(?i:(?<=\\s)(?:amsdos|breakpoint|buildcpr|buildsna|run|save|setcpc|setcrtc)\\b)" 89 | }, 90 | { 91 | "name": "keyword.control.z80asm", 92 | "match": "(?i:(?<=\\s)(?:list|nolist|let|labelslist|bplist|setbp|setbreakpoint|cspectmap)\\b)" 93 | }, 94 | { 95 | "name": "string.other.lua.z80asm", 96 | "begin": "(?i:(?<=\\s)(lua)\\b)", 97 | "beginCaptures": { 98 | "1": { 99 | "name": "keyword.control.z80asm" 100 | } 101 | }, 102 | "end": "(?i:(?<=\\s)(endlua)\\b)", 103 | "endCaptures": { 104 | "1": { 105 | "name": "keyword.control.z80asm" 106 | } 107 | } 108 | } 109 | ] 110 | }, 111 | "strings": { 112 | "patterns": [ 113 | { 114 | "match": "(?i:(af'))", 115 | "captures": { 116 | "1": { 117 | "name": "support.type.register.z80asm" 118 | } 119 | } 120 | }, 121 | { 122 | "name": "string.quoted.single.z80asm", 123 | "begin": "'", 124 | "beginCaptures": { 125 | "0": { 126 | "name": "punctuation.definition.string.begin.z80asm" 127 | } 128 | }, 129 | "end": "(\\')|((?:[^\\\\\\n])$)", 130 | "endCaptures": { 131 | "1": { 132 | "name": "punctuation.definition.string.end.z80asm" 133 | }, 134 | "2": { 135 | "name": "invalid.illegal.newline.z80asm" 136 | } 137 | }, 138 | "patterns": [ 139 | { 140 | "include": "#string-character-escape" 141 | } 142 | ] 143 | }, 144 | { 145 | "name": "string.quoted.double.z80asm", 146 | "begin": "\"", 147 | "beginCaptures": { 148 | "0": { 149 | "name": "punctuation.definition.string.begin.z80asm" 150 | } 151 | }, 152 | "end": "(\")|((?:[^\\\\\\n])$)", 153 | "endCaptures": { 154 | "1": { 155 | "name": "punctuation.definition.string.end.z80asm" 156 | }, 157 | "2": { 158 | "name": "invalid.illegal.newline.z80asm" 159 | } 160 | }, 161 | "patterns": [ 162 | { 163 | "include": "#string-character-escape" 164 | } 165 | ] 166 | } 167 | ] 168 | }, 169 | "string-character-escape": { 170 | "patterns": [ 171 | { 172 | "name": "constant.character.escape.z80asm", 173 | "match": "\\\\(x[0-9A-Fa-f]{2}|[0-2][0-7]{0,2}|3[0-6][0-7]?|37[0-7]?|[4-7][0-7]?|.|$)" 174 | } 175 | ] 176 | }, 177 | "mnemonics": { 178 | "patterns": [ 179 | { 180 | "name": "support.type.flag.z80asm", 181 | "match": "\\b(?i:(j[pr]|call|ret))(?:\\s+(?i:([cmpz]|n[cz]|p[eo])))?\\b", 182 | "captures": { 183 | "1": { 184 | "name": "keyword.mnemonic.z80asm" 185 | }, 186 | "2": { 187 | "name": "meta.preprocessor.flag.z80asm" 188 | } 189 | } 190 | }, 191 | { 192 | "name": "support.type.flag.z80asm", 193 | "match": "\\b(?i:z80|r800|msx|gbz80|zxspectrum(?:48|128|256|512|1024)|(?:no)?(?:expand|expif|expmacro|export|intlabel|globalsymbols))\\b" 194 | }, 195 | { 196 | "name": "constant.numeric.operator.z80asm", 197 | "match": "\\b(?i:high|low|not|mod|sh[lr])\\b", 198 | "comment": "sjasm numeric operators" 199 | }, 200 | { 201 | "name": "constant.numeric.function.z80asm", 202 | "match": "\\b(?i:sqrt|a?sinh?|a?cosh?|a?tanh?|a?coth?|exp|alog|a?ld|ln|log|int|bitcnt|firstbit|lastbit|bitpos|sgn|abs|toupper|tolower|upstring|lowstring|strlen|substr|charfromstr|strstr|val|exprtype)\\b", 203 | "comment": "AS macroassembler functions" 204 | }, 205 | { 206 | "name": "keyword.mnemonic.z80asm", 207 | "match": "\\s(?i:ad[cd]|and|bit|ccf|cp|cp[di]r?|cpl|daa|dec|[de]i|djnz|ex[adx]?|halt|i[mn]|inc|in[di]r?|ld|ld[di]r?|neg|nop|or|ot[di]r|out|out[di]|pop|push|res|ret[in]|rla?|rlca?|r[lr]d|rra?|rrca?|rst|sbc|scf|set|s[lr]a|s[lr]l|slia|sl1|sub|x?or)\\s" 208 | }, 209 | { 210 | "name": "keyword.mnemonic.z80asm", 211 | "match": "\\s(?i:swap|ldir?x|ldws|lddr?x|ldpirx|outinb|swapnib|mul|mirror|nextreg|pixel(ad|dn)|setae|te?st|bs[lr]a|bsr[lf]|brlc)\\s" 212 | }, 213 | { 214 | "name": "support.type.register.z80asm", 215 | "match": "\\b(?i:[abcdefhlir]|ix|iy|af'?|bc|de|hl|pc|sp|ix[hlu]|iy[hlu]|[lh]x|x[lh]|[lh]y|y[lh])\\b" 216 | }, 217 | { 218 | "name": "constant.language.operator.z80asm", 219 | "match": "(?i:\\{(?:hex[248]?|bin(?:8|16|32)?|int|eval|r[bw])?\\})" 220 | } 221 | ] 222 | }, 223 | "numbers": { 224 | "patterns": [ 225 | { 226 | "name": "invalid", 227 | "match": "^\\{5}" 228 | }, 229 | { 230 | "name": "constant.numeric.integer.hexadecimal.z80asm", 231 | "match": "[\\$#][0-9a-fA-F]+" 232 | }, 233 | { 234 | "name": "constant.numeric.integer.hexadecimal.z80asm", 235 | "match": "(\\-?[0-9a-fA-F]+[hH])\\b" 236 | }, 237 | { 238 | "name": "constant.numeric.integer.hexadecimal.z80asm", 239 | "match": "(\\-?0x[0-9a-fA-F]+)\\b" 240 | }, 241 | { 242 | "name": "constant.numeric.integer.octal.z80asm", 243 | "match": "@[0-7]+" 244 | }, 245 | { 246 | "name": "constant.numeric.integer.octal.z80asm", 247 | "match": "\\-?[0-7]+[oO]\\b" 248 | }, 249 | { 250 | "name": "constant.numeric.integer.octal.z80asm", 251 | "match": "(\\-?0q?[0-7]+)\\b" 252 | }, 253 | { 254 | "name": "constant.numeric.integer.binary.z80asm", 255 | "match": "%[01]+\\b" 256 | }, 257 | { 258 | "name": "constant.numeric.integer.binary.z80asm", 259 | "match": "\\b[01]+[bB]\\b" 260 | }, 261 | { 262 | "name": "constant.numeric.integer.binary.z80asm", 263 | "match": "(\\-?0b[01]+)\\b" 264 | }, 265 | { 266 | "name": "constant.numeric.integer.decimal.z80asm", 267 | "match": "\\-?\\d+" 268 | } 269 | ] 270 | }, 271 | "includes": { 272 | "patterns": [ 273 | { 274 | "match": "(?i:(?<=\\s)(include)\\s+(([\"'])(?:[^\\3]+)\\3))", 275 | "captures": { 276 | "1": { 277 | "name": "keyword.control.z80asm" 278 | }, 279 | "2": { 280 | "name": "string.modifier.import.z80asm" 281 | } 282 | } 283 | } 284 | ] 285 | }, 286 | "labels": { 287 | "patterns": [ 288 | { 289 | "match": "^\\@?((\\$\\$(?!\\.))?[\\w\\.]+):?(?=\\s)", 290 | "captures": { 291 | "1": { 292 | "name": "entity.name.function.z80asm" 293 | } 294 | } 295 | }, 296 | { 297 | "match": "\\b([a-zA-Z][\\w\\.]+)\\b", 298 | "name": "variable.parameter.label.z80asm" 299 | } 300 | ] 301 | } 302 | }, 303 | "scopeName": "source.z80asm" 304 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "target": "esnext", 6 | "outDir": "out", 7 | "lib": [ "esnext" ], 8 | "sourceMap": true, 9 | "rootDir": ".", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noUnusedParameters": false 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension 5 | * `package.json` - this is the manifest file in which you declare your language support and define 6 | the location of the grammar file that has been copied into you extension. 7 | * `syntaxes/z80-macroasm.tmLanguage.json` - this is the Text mate grammar file that is used for tokenization 8 | * `language-configuration.json` - this the language configuration, defining the tokens that are used for 9 | comments and brackets. 10 | 11 | ## Get up and running straight away 12 | * Make sure the language configuration settings in `language-configuration.json` are accurate 13 | * press `F5` to open a new window with your extension loaded 14 | * create a new file with a file name suffix matching your language 15 | * verify that syntax highlight works and that the language configuration settings are working 16 | 17 | ## Make changes 18 | * you can relaunch the extension from the debug toolbar after making changes to the files listed above 19 | * you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes 20 | 21 | ## Add more language features 22 | * To add features such as intellisense, hovers and validators check out the VS Code extenders documentation at 23 | https://code.visualstudio.com/docs 24 | 25 | ## Install your extension 26 | * To start using your extension with Visual Studio Code copy it into the `/.vscode/extensions` folder and restart Code. 27 | * To share your extension with the world, read on https://code.visualstudio.com/docs about publishing an extension. --------------------------------------------------------------------------------