├── .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 | > 
93 |
94 | - Generated map of every symbol defined considers also modules or temporal labels:
95 | > 
96 |
97 | - Show symbol's value or specific definiton on hover:
98 | > 
99 |
100 | ### Completion:
101 | > 
102 |
103 | - Inteligent completion of directives, pseudo-instructions, Z80 instructions, registers, labels or symbols:
104 | > 
105 |
106 | ### Renaming:
107 | - Allow to rename labels, local labels, module names or macro indetifiers in InteliSense meaning.
108 | > 
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.
--------------------------------------------------------------------------------