├── .editorconfig ├── .gitignore ├── .husky └── pre-commit ├── .node-version ├── .nova ├── Configuration.json └── Tasks │ └── Development.json ├── .prettierignore ├── .prettierrc ├── DEVELOPMENT.md ├── LICENSE.md ├── README.md ├── XML.novaextension ├── CHANGELOG.md ├── Images │ └── extension │ │ ├── completions.png │ │ ├── hovers.png │ │ └── linting.png ├── README.md ├── Schemas │ ├── catalog.xml │ ├── completions.xsd │ └── syntax.xsd ├── Scripts │ └── main.dist.js ├── bin │ └── .gitkeep ├── extension.json ├── extension.png └── extension@2x.png ├── bin ├── build.sh ├── get-server.sh └── grab-examples.sh ├── examples ├── Inventory-catalog.xml ├── Inventory.xml ├── Inventory.xsd ├── KitchenSink.xml └── catalog.xml ├── package-lock.json ├── package.json ├── src └── Scripts │ ├── commands │ ├── format-command.ts │ ├── rename-command.ts │ └── restart-command.ts │ ├── main.ts │ ├── utils.ts │ └── xml-language-server.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # 2 | # Editor config, for sharing IDE preferences ~ https://editorconfig.org/ 3 | # 4 | 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | end_of_line = lf 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | # Files to ignore from git source control 3 | # 4 | 5 | node_modules 6 | *.env 7 | .DS_Store 8 | 9 | stdin.log 10 | stdout.log 11 | debug.log 12 | .vscode 13 | 14 | examples/syntaxes 15 | examples/completions 16 | 17 | XML.novaextension/bin/lemminx* 18 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npx --no lint-staged 2 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | lts-jod 2 | -------------------------------------------------------------------------------- /.nova/Configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.ignored_file_patterns" : [ 3 | "XML.novaextension\/Scripts" 4 | ], 5 | "robb-j.debugLogPath" : "\/Users\/rob\/Developer\/nova\/xml", 6 | "workspace.color" : 0, 7 | "workspace.name" : "xml", 8 | "xml.catalogs" : [ 9 | "examples\/catalog.xml" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.nova/Tasks/Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "build": { 4 | "enabled": true, 5 | "path": "bin/build.sh" 6 | } 7 | }, 8 | "buildBeforeRunning": true, 9 | "openLogOnRun": "fail" 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # 2 | # Files to ignore from prettier 3 | # 4 | 5 | XML.novaextension/Scripts 6 | coverage 7 | .nova 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Setup 4 | 5 | To work on the extension, you will need to have [Node.js](https://nodejs.org/en/) (version 16+) 6 | and [Nova](https://nova.app) installed on your development machine. Then run: 7 | 8 | ```sh 9 | # cd to/this/folder 10 | 11 | # install NPM dependencies 12 | npm install 13 | 14 | # opt-in to commit hooks 15 | npx husky 16 | ``` 17 | 18 | ## Regular use 19 | 20 | For development, use the `Development` task to build and run the extension locally. 21 | **Build** will compile the TypeScript into JavaScript into the extension folder. 22 | **Run** will do the build, install bundled dependencies and activate the extension in Nova. 23 | Nova will run the extension locally and restart when any file inside the `.novaextension` changes, 24 | i.e. by running the **Build** task. 25 | 26 | > Make sure to disable the extension if a published version is already installed. 27 | 28 | When in development mode, the extension outputs extra information to the Debug Pane, 29 | which can be shown with **View** → **Show Debug Pane**. 30 | 31 | Use the files in the [examples](/examples) folder to test out different features of the language server. 32 | 33 | ## Code formatting 34 | 35 | This repository uses [Prettier](https://prettier.io/), 36 | [yorkie](https://www.npmjs.com/package/yorkie) 37 | and [lint-staged](https://www.npmjs.com/package/lint-staged) to 38 | automatically format code when staging code git commits. 39 | 40 | You can manually run the formatter with `npm run format` if you want. 41 | 42 | Prettier ignores files using [.prettierignore](/.prettierignore) 43 | or specific lines after a `// prettier-ignore` comment. 44 | 45 | ## Links 46 | 47 | - https://github.com/eclipse/lemminx 48 | - https://github.com/redhat-developer/vscode-xml 49 | - https://github.com/redhat-developer/vscode-xml/blob/master/docs/Validation.md#xml-catalog-with-xsd 50 | - https://www.w3schools.com/xml/schema_elements_ref.asp 51 | - https://docs.nova.app/syntax-reference/ 52 | 53 | ## Notes 54 | 55 | Files are cached at `~/.lemminx` 56 | 57 | ## Release procedure 58 | 59 | - Ensure the Language Server is downloaded 60 | - Check the core language definitions are still valid 61 | - Generate new screenshots if needed 62 | - Ensure the `CHANGELOG.md` is up to date 63 | - Run the build 64 | - Bump the version in `extension.json` 65 | - Commit as `X.Y.Z` 66 | - Tag as `vX.Y.Z` 67 | - **Extensions** → **Submit to the Extension Library...** 68 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rob Anderson 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 | # XML Nova extension 2 | 3 | Please see: 4 | 5 | - [Get the extension →](https://extensions.panic.com/extensions/robb-j/robb-j.XML/) 6 | - [Extension README.md →](/XML.novaextension/README.md) 7 | - [Extension CHANGELOG.md →](/XML.novaextension/CHANGELOG.md) 8 | - [DEVELOPMENT.md →](/DEVELOPMENT.md) 9 | -------------------------------------------------------------------------------- /XML.novaextension/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Version 0.4.1 2 | 3 | - Restart the server if `xml.catalogs` is changed, the Language Server seems to need this 4 | - Reduce icon size for a smaller package 5 | 6 | ## Version 0.4.0 7 | 8 | - Added `Restart Server` command 9 | - Upgraded the language server from `0.24.0` to `0.29.0`, see it's [GitHub releases](https://github.com/redhat-developer/vscode-xml/releases) for info 10 | 11 | ## Version 0.3.0 12 | 13 | - Tree sitter support! Added support for `` syntax in `https://www.nova.app/syntax` xml files. 14 | - Update the language server from `0.21.0` to `0.24.0`, see it's [GitHub releases](https://github.com/redhat-developer/vscode-xml/releases) for info 15 | - Build tooling updates 16 | - Specify custom catalog files per-workspace, thanks [@greystate](https://github.com/greystate)! 17 | 18 | ## Version 0.2.1 19 | 20 | Update the language server from `0.18.0` to `0.21.0`, see it's [GitHub releases](https://github.com/redhat-developer/vscode-xml/releases) for info 21 | 22 | > [0.18.1](https://github.com/redhat-developer/vscode-xml/releases/tag/0.18.1), 23 | > [0.18.2](https://github.com/redhat-developer/vscode-xml/releases/tag/0.18.2), 24 | > [0.18.3](https://github.com/redhat-developer/vscode-xml/releases/tag/0.18.3), 25 | > [0.19.0](https://github.com/redhat-developer/vscode-xml/releases/tag/0.19.0), 26 | > [0.19.1](https://github.com/redhat-developer/vscode-xml/releases/tag/0.19.1), 27 | > [0.20.0](https://github.com/redhat-developer/vscode-xml/releases/tag/0.20.0), 28 | > [0.21.0](https://github.com/redhat-developer/vscode-xml/releases/tag/0.21.0) 29 | 30 | All compiled code is now Open Source, if you fancy diving in to see what Yaml Extension is up to, [you can](https://github.com/robb-j/nova-xml/tree/main/XML.novaextension/Scripts) 31 | 32 | ## Version 0.2 33 | 34 | Nova Syntax definitions improvements 35 | 36 | - Add support for `` definitions 37 | - Allow `subsyntax=true` on `` elements 38 | - Allow empty `` 39 | - Fix detectors so sub-elements can be re-used 40 | 41 | Thanks to [Martin Kopischke](https://github.com/kopischke) for the `` [bug report](https://github.com/robb-j/nova-xml/issues/2) 42 | 43 | ## Version 0.1.1 44 | 45 | - Fix the LanguageServer not starting up correctly 46 | 47 | ## Version 0.1 48 | 49 | - Everything is new 50 | -------------------------------------------------------------------------------- /XML.novaextension/Images/extension/completions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/Images/extension/completions.png -------------------------------------------------------------------------------- /XML.novaextension/Images/extension/hovers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/Images/extension/hovers.png -------------------------------------------------------------------------------- /XML.novaextension/Images/extension/linting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/Images/extension/linting.png -------------------------------------------------------------------------------- /XML.novaextension/README.md: -------------------------------------------------------------------------------- 1 | **XML Extension** provides a deeper integration with **XML** through the use of schemas, XML validation, code completion, linting and optional document formatting. XML Extension also provides a schema for Nova's own [Syntax definitions](https://docs.nova.app/syntax-reference/). 2 | 3 | XML Extension adds XML schema support to Nova 4 | 5 | ## Work in Progress 6 | 7 | This version of XML is a prerelease and not all functionality exists yet. 8 | Things that are still to be done are on 9 | [GitHub Issues](https://github.com/robb-j/nova-xml/issues/1). 10 | 11 | ## Requirements 12 | 13 | XML Extension runs the [eclipse/lemminx](https://github.com/eclipse/lemminx) XML language server as a native binary 14 | and has no external requirements. 15 | 16 | ## Entitlements 17 | 18 | XML Extension uses these entitlements for these purposes: 19 | 20 | - `process` is to run the Language Server itself 21 | - `network` is to download schemas and cache them to `~/.lemmix` 22 | - `filesystem` is to read in XML files, write formatted files and cache schemas 23 | 24 | ## Usage 25 | 26 | XML Extension runs any time you open a local project with XML files in it, automatically lints all open files, then reports errors and warnings in Nova's **Issues** sidebar and the editor gutter: 27 | 28 | XML Extension adds XML schema support to Nova 29 | 30 | XML Extension intelligently suggests completions for you as you write, based on the current documents [associated schema](#associating-schemas). 31 | 32 | See completion options as you write 33 | 34 | XML Extension displays relevant documentation when you hover over symbols: 35 | 36 | Get tooltips when writting XML files 37 | 38 | **Commands** 39 | 40 | - **Format** will format the current XML document. 41 | - **Rename** will rename the tag you have selected. 42 | 43 | ### Configuration 44 | 45 | To configure global preferences, open **Extensions → Extension Library...** then select XML's **Preferences** tab. 46 | 47 | You can also configure preferences on a per-project basis in **Project → Project Settings...** 48 | 49 | ### Associating Schemas 50 | 51 | There are two schema languages for XML, [DTD](https://www.w3schools.com/xml/xml_dtd_intro.asp) 52 | and [XSD](https://www.w3schools.com/xml/schema_intro.asp). DTD files are simpler and shorter to write whereas XSD schemas are more verbose but provide greater functionality. 53 | 54 | Schemas can be loaded relative to the XML file in question. 55 | 56 | **Nova Syntax Definitions** 57 | 58 | XML Extension uses a schema catalog to automatically register a schema for Nova [Syntax definitions](https://docs.nova.app/syntax-reference/). 59 | You can opt into it like this: 60 | 61 | ```xml 62 | 63 | 64 | 65 | 66 | ``` 67 | 68 | **Local Catalog** 69 | 70 | If your project uses its own catalog file(s) for mapping namespaces to schemas, you can point to them in the project's configuration settings for **XML**. 71 | 72 | This will allow XML Extension to load mappings from your local files as well. 73 | 74 | > You may need to run the **Restart XML Server** command for these changes to take effect 75 | 76 | ### Writing Schemas 77 | 78 | W3 Schools has good tutorials for creating both [DTD](https://www.w3schools.com/xml/xml_dtd_intro.asp) 79 | and [XSD](https://www.w3schools.com/xml/schema_intro.asp) schemas. 80 | 81 | For an example, check out 82 | [Inventory.xml](https://github.com/robb-j/nova-xml/blob/main/examples/Inventory.xml) 83 | and [Intentory.xsd](https://github.com/robb-j/nova-xml/blob/main/examples/Inventory.xsd) 84 | in the example repo. 85 | It shows how to structure a schema and associate it relatively. 86 | -------------------------------------------------------------------------------- /XML.novaextension/Schemas/catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /XML.novaextension/Schemas/completions.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /XML.novaextension/Schemas/syntax.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | A regular expression that is used as the basis for the resulting parse rule. 38 | https://docs.nova.app/syntax-reference/scopes/#match-scopes 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | A capture element defines the regular expression capture group number (starting at 1, with 0 being the entire regular expression match), and can be referenced by name in the same way as scopes for syntax highlighting. 48 | https://docs.nova.app/syntax-reference/scopes/#match-scopes 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | Aefine an expression using a set of strings. This is very useful when the possible expressions being matched are from a known set of words or expressions. 62 | https://docs.nova.app/syntax-reference/scopes/#string-expressions 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | https://docs.nova.app/syntax-reference/scopes/#back-referencing-between-start-and-end-expressions 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | https://docs.nova.app/syntax-reference/scopes/#start-end-scopes 102 | 103 | 104 | 105 | 106 | 107 | 108 | https://docs.nova.app/syntax-reference/scopes/#start-end-scopes 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | Start-End scopes can be used to define a fenced block of code, also known as a Subsyntax. When a subsyntax element is used in place of subscopes. https://docs.nova.app/syntax-reference/scopes/#subsyntaxes 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | https://docs.nova.app/syntax-reference/scopes/#cut-off-scopes 133 | 134 | 135 | 136 | 137 | 138 | 139 | https://docs.nova.app/syntax-reference/scopes/#include-scopes 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | https://docs.nova.app/syntax-reference/scopes/#start-end-scopes 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 170 | 171 | 172 | 173 | 174 | 175 | The user-readable name of the syntax, which is display in user interface elements, such as "HTML". 176 | https://docs.nova.app/syntax-reference/syntaxes/#user-readable-name 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | The category in which the syntax should be considered. This determines how documents using the syntax are presented to the user, including accent colors in the document’s tab. 186 | https://docs.nova.app/syntax-reference/syntaxes/#syntax-type 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | The file extension used by default for new documents using the syntax, such as html for HTML files. 205 | https://docs.nova.app/syntax-reference/syntaxes/#preferred-file-extension 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | Define that the syntax is a conceptual “child” of another syntax. This is not used in parsing, but for IDE features that may restrict certain items by syntax, such as Clips. 215 | https://docs.nova.app/syntax-reference/syntaxes/#parent 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | Declares that a syntax can be used for scripting in shells and executed externally. This enables the language to be used in the IDE’s Tasks UI 225 | https://docs.nova.app/syntax-reference/syntaxes/#scriptable 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | This element contains metadata about the syntax, including its identifying name, user-readable name, and language category. 238 | https://docs.nova.app/syntax-reference/syntaxes/#meta-properties 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 255 | 256 | 257 | 258 | 259 | 260 | File extension detectors will match a document based on one or more of its file extension components. 261 | https://docs.nova.app/syntax-reference/syntaxes/#file-extension-detectors 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | Filename detectors will match a document based on one or more predefined filenames. 274 | https://docs.nova.app/syntax-reference/syntaxes/#filename-detectors 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | Content match detectors will match the text of a document using a regular expression. 287 | https://docs.nova.app/syntax-reference/syntaxes/#content-match-detectors 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | Compound detectors can be used to combine multiple other detectors into a single rule. This is most often useful when you wish to restrict certain detectors together, such as only matching a content match detector when a specific filename or file extension also matches. 301 | https://docs.nova.app/syntax-reference/syntaxes/#compound-detectors 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | Sets of Detectors are used when files are opened in the editor, and determine which syntax is used for the document based on a set of definable rules that evaluate to a “score”. Each syntax’s detectors are evaluated against the document being opened, and whichever set scores the “highest” will determine the language used. 321 | https://docs.nova.app/syntax-reference/syntaxes/#detectors 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 338 | 339 | 340 | 341 | 342 | 343 | If this expression matches the current line just before the user presses Return, the succeeding line being inserted will be automatically indented one level. 344 | https://docs.nova.app/syntax-reference/syntaxes/#matching-increase 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | If this expression matches the current line when the user types text matching the expression, the current line will be automatically dedented one level. 359 | https://docs.nova.app/syntax-reference/syntaxes/#matching-decrease 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | Define rules for automatically adjusting indentation as the user types. 373 | https://docs.nova.app/syntax-reference/syntaxes/#indentation-rules 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 387 | 388 | 389 | 390 | https://docs.nova.app/syntax-reference/syntaxes/#comment-rules 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | https://docs.nova.app/syntax-reference/syntaxes/#comment-rules 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | Defines the rules for commenting and uncommenting text within a document. There are individual elements within to define rules for both single-line and multi-line commenting. The expression values are text that will be wrapped around text being commented, or detected and removed from text being uncommented. 417 | https://docs.nova.app/syntax-reference/syntaxes/#comment-rules 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 431 | 432 | 433 | 434 | 435 | https://docs.nova.app/syntax-reference/syntaxes/#brackets 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | Defines the set of characters that should be treated as brackets by the editor when performing bracket matching, bracket highlighting, and bracket auto-closing. 448 | https://docs.nova.app/syntax-reference/syntaxes/#brackets 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | Defines the set of characters, which are most often varying types of brackets and quotes, that should be treated as pairs by the editor when performing wrapping of selected text as well as inserting and consuming pairs during typing. 463 | https://docs.nova.app/syntax-reference/syntaxes/#surrounding-pairs 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 476 | 477 | 478 | 479 | https://docs.nova.app/syntax-reference/scopes/#types-of-scopes 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | Defines its first level of Scopes. When parsing of a document begins, these scopes are evaluated. As scopes match, they may cause the parser to enter a deeper state and reference other scopes. 518 | https://docs.nova.app/syntax-reference/syntaxes/#scopes 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | Template scopes are a special set of scopes which allow easy construction of template languages, like PHP and Jinja. 534 | otherwise are defined exactly the same way as the "scopes" element. 535 | https://docs.nova.app/syntax-reference/syntaxes/#template-scopes 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | To make building syntax grammars easier and cleaner, scopes may be grouped logically into Collections. 551 | A syntax’s top level "collections" element contains reference to one or more collections, which in turn contain scopes that may be referenced elsewhere using an "Include scope". 552 | https://docs.nova.app/syntax-reference/syntaxes/#collections 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | https://docs.nova.app/syntax-reference/syntaxes/#collections 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 583 | 584 | 585 | 586 | To control how the symbolication process defines the local scope, use the `local` element within the grammar’s `symbols` element, with a scope attribute. 587 | https://docs.nova.app/syntax-reference/symbols/#controlling-what-local-scope-means 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | Certain symbolication behaviors can be controlled for the entire grammar by using the optional "symbols" element as a direct child of the grammar’s "syntax" element. 606 | https://docs.nova.app/syntax-reference/symbols/#grammar-symbolication-options 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | https://docs.nova.app/syntax-reference/symbols/#controlling-symbol-redefinition-behaviors 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | https://docs.nova.app/syntax-reference/symbols/#symbolic-contexts 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | https://docs.nova.app/syntax-reference/symbols/#filtering-symbols 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | https://docs.nova.app/syntax-reference/symbols/#computing-a-symbols-display-name 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | https://docs.nova.app/syntax-reference/symbols/#computing-a-symbols-display-name 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | https://docs.nova.app/syntax-reference/symbols/#basic-symbols 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | https://docs.nova.app/api-reference/symbol/ 733 | 734 | 735 | 736 | 737 | https://docs.nova.app/syntax-reference/symbols/#defining-a-symbols-scope 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 756 | 757 | 758 | 759 | https://docs.nova.app/syntax-reference/completions/#static-completions 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | Behaviors are a conditional feature that allow completions to define how they are presented and inserted into the document based on the text around them. 770 | https://docs.nova.app/syntax-reference/completions/#behaviors 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | Declares the type of symbol icon displayed to the user for the item. 794 | https://docs.nova.app/syntax-reference/completions/#symbol 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | The `case-insensitive` attribute may be applied with a value of "true" to make the completion case-insensitive for matching. In this way, the completion will match text behind the cursor regardless of the case of that text. 803 | https://docs.nova.app/syntax-reference/completions/#case-insensitivity 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | The deprecated element may be applied with a value of "true" to mark the completion as deprecated in the completions list. Deprecated completions are deemphasized to the user, allowing them to be available but denoting that their use is discouraged. 812 | https://docs.nova.app/syntax-reference/completions/#case-insensitivity 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | A provider may define one or more "set" elements to declare which static completion sets are filtered and shown to the user when matched. The text of the element should be the name of a static completion set defined by any completions definition. 823 | https://docs.nova.app/syntax-reference/completions/#sets 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | A completion provider determines where specific completions should be offered based on logical rules about the document. 837 | https://docs.nova.app/syntax-reference/completions/#providers 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | Defines the name of a syntax in which the provider will be valid. The contents of the element should be the identifying name of a validly registered syntax. Most often, completions that are bundle alongside a syntax will use that syntax’s name. Multiple syntax elements may be provided, in which case the provider will be valid in more than one syntax. 847 | https://docs.nova.app/syntax-reference/completions/#syntax 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | Defines a CSS-style selector expression that can be used to finely determine in which syntax scopes a provider is valid. These expressions match based on the scope name components used for syntax highlighting. If the selector does not match the current position in the document, the provider will not be used. 857 | 858 | A provider may only define a single selector element. 859 | https://docs.nova.app/syntax-reference/completions/#selector 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | Defines a regular expression that is evaluated anchored to the end of the text preceding the cursor position. If the expression does not match, the provider will not be used. 869 | https://docs.nova.app/syntax-reference/completions/#expression 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 884 | 885 | 886 | 887 | By default, this instructs the syntax engine to look for a Tree-sitter parser alongside the XML file, named the same as the syntax’s name attribute. For a syntax named mylang, it will look for libtree-sitter-mylang.dylib exporting a function named tree_sitter_mylang(). 888 | 889 | https://docs.nova.app/syntax-reference/tree-sitter/#including-a-parser-in-extensions 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | The most common (and definitely most required) query of any language extension is support for syntax highlighting, or coloring of tokens in a document to indicate their meaning. 900 | 901 | https://docs.nova.app/syntax-reference/tree-sitter/#syntax-highlighting 902 | 903 | 904 | 905 | 906 | 907 | 908 | By default, specifying the highlights element without a path attribute will tell the syntax engine to look for a file named highlights.scm within your extension’s Queries/ folder. 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | Symbolication is the process of taking specific nodes from the syntax tree and building a list of user-visible “symbols” that are the major structural components of the file. For procedural languages, this will likely be things like types, functions, etc. For a language like HTML, this is likely important tags. 920 | 921 | https://docs.nova.app/syntax-reference/tree-sitter/#symbolication 922 | 923 | 924 | 925 | 926 | 927 | 928 | By default, specifying the symbols element without a "path" attribute will tell the syntax engine to look for a file named symbols.scm within your extension’s Queries/ folder. 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | Fold queries define the boundaries on which automatic code folding support is provided in Nova’s editor. 940 | 941 | https://docs.nova.app/syntax-reference/tree-sitter/#folds 942 | 943 | 944 | 945 | 946 | 947 | 948 | By default, specifying the folds element without a "path" attribute will tell the syntax engine to look for a file named folds.scm within your extension’s Queries/ folder. 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | Injections allow for a language to mark regions of a document which should be parsed as another language by the editor (also known as “code fences”). Examples of this include script and style tags in HTML and triple-backtick blocks in Markdown. 960 | 961 | https://docs.nova.app/syntax-reference/tree-sitter/#injections 962 | 963 | 964 | 965 | 966 | 967 | 968 | By default, specifying the injections element without a "path" attribute will tell the syntax engine to look for a file named injections.scm within your extension’s Queries/ folder. 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | Languages that make heavy use of prose may wish to include support for automatic text checking support, which performs operations like spell checking and automatic URL detection. This is often used in languages like HTML and Markdown for human-readable text as well as in many procedural languages for documentation comments. 980 | 981 | By default, Nova’s editor will scan any region syntax highlighted with the comment selector to be included in text checking. This means that most languages likely don’t have to do anything to support it. 982 | 983 | https://docs.nova.app/syntax-reference/tree-sitter/#text-checking 984 | 985 | 986 | 987 | 988 | 989 | 990 | By default, specifying the text-checking element without a "path" attribute will tell the syntax engine to look for a file named textChecking.scm within your extension’s Queries/ folder. 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | Extensions that make use of the Colors API can provide a query to automatically detect potential color values in the document to be passed to the extension for further processing. 1002 | 1003 | Any nodes matched by color queries will be collected by the editor and provided in the ColorInformationContext object’s candidates property when a color request is made. 1004 | 1005 | https://docs.nova.app/syntax-reference/tree-sitter/#colors 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | By default, specifying the colors element without a "path" attribute will tell the syntax engine to look for a file named colors.scm within your extension’s Queries/ folder. 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | -------------------------------------------------------------------------------- /XML.novaextension/Scripts/main.dist.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __hasOwnProp = Object.prototype.hasOwnProperty; 6 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 20 | var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 21 | var __async = (__this, __arguments, generator) => { 22 | return new Promise((resolve, reject) => { 23 | var fulfilled = (value) => { 24 | try { 25 | step(generator.next(value)); 26 | } catch (e) { 27 | reject(e); 28 | } 29 | }; 30 | var rejected = (value) => { 31 | try { 32 | step(generator.throw(value)); 33 | } catch (e) { 34 | reject(e); 35 | } 36 | }; 37 | var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); 38 | step((generator = generator.apply(__this, __arguments)).next()); 39 | }); 40 | }; 41 | 42 | // src/Scripts/main.ts 43 | var main_exports = {}; 44 | __export(main_exports, { 45 | activate: () => activate, 46 | deactivate: () => deactivate 47 | }); 48 | module.exports = __toCommonJS(main_exports); 49 | 50 | // src/Scripts/utils.ts 51 | function createDebug(namespace) { 52 | return (...args) => { 53 | if (!nova.inDevMode()) return; 54 | const humanArgs = args.map( 55 | (arg) => typeof arg === "object" ? JSON.stringify(arg) : arg 56 | ); 57 | console.info(`${namespace}:`, ...humanArgs); 58 | }; 59 | } 60 | function getEditorRange(document, range) { 61 | const fullContents = document.getTextInRange(new Range(0, document.length)); 62 | let rangeStart = 0; 63 | let rangeEnd = 0; 64 | let chars = 0; 65 | const lines = fullContents.split(document.eol); 66 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 67 | const lineLength = lines[lineIndex].length + document.eol.length; 68 | if (range.start.line === lineIndex) { 69 | rangeStart = chars + range.start.character; 70 | } 71 | if (range.end.line === lineIndex) { 72 | rangeEnd = chars + range.end.character; 73 | break; 74 | } 75 | chars += lineLength; 76 | } 77 | return new Range(rangeStart, rangeEnd); 78 | } 79 | function getLspRange(document, range) { 80 | const fullContents = document.getTextInRange(new Range(0, document.length)); 81 | let chars = 0; 82 | let startLspRange; 83 | const lines = fullContents.split(document.eol); 84 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 85 | const lineLength = lines[lineIndex].length + document.eol.length; 86 | if (!startLspRange && chars + lineLength >= range.start) { 87 | const character = range.start - chars; 88 | startLspRange = { line: lineIndex, character }; 89 | } 90 | if (startLspRange && chars + lineLength >= range.end) { 91 | const character = range.end - chars; 92 | return { start: startLspRange, end: { line: lineIndex, character } }; 93 | } 94 | chars += lineLength; 95 | } 96 | return null; 97 | } 98 | function logError(message, error) { 99 | console.error(message); 100 | if (error instanceof Error) { 101 | console.error(error.message); 102 | console.error(error.stack); 103 | } else { 104 | console.error("An non-error was thrown"); 105 | console.error(error); 106 | } 107 | } 108 | 109 | // src/Scripts/xml-language-server.ts 110 | var debug = createDebug("xml-language-server"); 111 | var XmlLanguageServer = class { 112 | constructor() { 113 | __publicField(this, "languageClient", null); 114 | debug("#new"); 115 | this.start(); 116 | } 117 | start() { 118 | return __async(this, null, function* () { 119 | var _a; 120 | if (this.languageClient) { 121 | this.languageClient.stop(); 122 | this.languageClient = null; 123 | } 124 | try { 125 | debug("#start"); 126 | const packageDir = nova.inDevMode() ? nova.extension.path : nova.extension.globalStoragePath; 127 | const catalogs = (_a = nova.workspace.config.get("xml.catalogs", "array")) != null ? _a : []; 128 | catalogs.push(nova.path.join(packageDir, "Schemas/catalog.xml")); 129 | for (let i = 0; i < catalogs.length; i++) { 130 | if (catalogs[i].startsWith("/")) continue; 131 | catalogs[i] = nova.path.join(nova.workspace.path, catalogs[i]); 132 | } 133 | const serverOptions = { 134 | type: "stdio", 135 | path: nova.path.join(packageDir, "bin/lemminx-osx-x86_64") 136 | }; 137 | const clientOptions = { 138 | syntaxes: ["xml"], 139 | initializationOptions: { 140 | settings: { 141 | xml: { catalogs } 142 | } 143 | } 144 | // debug: true, 145 | }; 146 | yield this.prepareBinary(serverOptions.path); 147 | debug("serverOptions", serverOptions); 148 | debug("clientOptions", clientOptions); 149 | const client = new LanguageClient( 150 | "robb-j.xml", 151 | "XML LanguageClient", 152 | serverOptions, 153 | clientOptions 154 | ); 155 | client.start(); 156 | this.languageClient = client; 157 | this.setupLanguageServer(client); 158 | } catch (error) { 159 | logError("LanguageServer Failed", error); 160 | } 161 | }); 162 | } 163 | dispose() { 164 | debug("#dispose"); 165 | this.stop(); 166 | } 167 | stop() { 168 | return __async(this, null, function* () { 169 | debug("#stop"); 170 | if (this.languageClient) { 171 | this.languageClient.stop(); 172 | this.languageClient = null; 173 | } 174 | }); 175 | } 176 | // Make sure the LSP server binary is executable 177 | prepareBinary(path) { 178 | return new Promise((resolve, reject) => { 179 | const process = new Process("/usr/bin/env", { 180 | args: ["chmod", "u+x", path] 181 | }); 182 | process.onDidExit((status) => status === 0 ? resolve() : reject()); 183 | process.start(); 184 | }); 185 | } 186 | setupLanguageServer(client) { 187 | client.onDidStop((err) => { 188 | debug("Language Server Stopped", err == null ? void 0 : err.message); 189 | }); 190 | } 191 | }; 192 | 193 | // src/Scripts/commands/format-command.ts 194 | var debug2 = createDebug("format"); 195 | function formatCommand(_0, _1) { 196 | return __async(this, arguments, function* (editor, { languageClient }) { 197 | if (!languageClient) { 198 | debug2("LanguageServer not running"); 199 | return; 200 | } 201 | const params = { 202 | textDocument: { 203 | uri: editor.document.uri 204 | }, 205 | options: { 206 | tabSize: editor.tabLength, 207 | insertSpaces: Boolean(editor.softTabs) 208 | } 209 | }; 210 | debug2("format", params); 211 | const result = yield languageClient.sendRequest( 212 | "textDocument/formatting", 213 | params 214 | ); 215 | if (!result) return; 216 | editor.edit((edit) => { 217 | for (const change of result.reverse()) { 218 | edit.replace( 219 | getEditorRange(editor.document, change.range), 220 | change.newText 221 | ); 222 | } 223 | }); 224 | }); 225 | } 226 | 227 | // src/Scripts/commands/rename-command.ts 228 | var debug3 = createDebug("format"); 229 | function renameCommand(_0, _1) { 230 | return __async(this, arguments, function* (editor, { languageClient }) { 231 | var _a; 232 | debug3("format", editor.document.uri); 233 | if (!languageClient) { 234 | debug3("LanguageServer not running"); 235 | return; 236 | } 237 | editor.selectWordsContainingCursors(); 238 | const selectedPosition = (_a = getLspRange( 239 | editor.document, 240 | editor.selectedRange 241 | )) == null ? void 0 : _a.start; 242 | if (!selectedPosition) return debug3("Nothing selected"); 243 | const newName = yield new Promise((resolve) => { 244 | nova.workspace.showInputPalette( 245 | "New name for symbol", 246 | { placeholder: editor.selectedText, value: editor.selectedText }, 247 | resolve 248 | ); 249 | }); 250 | if (!newName || newName == editor.selectedText) { 251 | return; 252 | } 253 | debug3("newName", newName); 254 | const params = { 255 | textDocument: { uri: editor.document.uri }, 256 | position: selectedPosition, 257 | newName 258 | }; 259 | const result = yield languageClient.sendRequest( 260 | "textDocument/rename", 261 | params 262 | ); 263 | debug3("result", result); 264 | if (!result) return; 265 | for (const uri in result.changes) { 266 | const editor2 = yield nova.workspace.openFile(uri); 267 | if (!editor2) { 268 | debug3("Failed to open", uri); 269 | continue; 270 | } 271 | const changes = result.changes[uri]; 272 | if (!changes) { 273 | debug3("TODO: add support for documentChanges", uri); 274 | continue; 275 | } 276 | editor2.edit((edit) => { 277 | for (const change of changes.reverse()) { 278 | edit.replace( 279 | getEditorRange(editor2.document, change.range), 280 | change.newText 281 | ); 282 | } 283 | }); 284 | } 285 | }); 286 | } 287 | 288 | // src/Scripts/commands/restart-command.ts 289 | var debug4 = createDebug("restart"); 290 | function restartCommand(langServer) { 291 | return __async(this, null, function* () { 292 | debug4("Restarting"); 293 | langServer.stop(); 294 | langServer.start(); 295 | }); 296 | } 297 | 298 | // src/Scripts/main.ts 299 | var debug5 = createDebug("main"); 300 | function activate() { 301 | debug5("#activate"); 302 | const langServer = new XmlLanguageServer(); 303 | nova.subscriptions.add(langServer); 304 | nova.workspace.onDidAddTextEditor((editor) => { 305 | editor.onWillSave(() => __async(null, null, function* () { 306 | var _a; 307 | if (editor.document.syntax !== "xml") return; 308 | if ((_a = nova.config.get("robb-j.xml.formatOnSave", "boolean")) != null ? _a : false) { 309 | yield nova.commands.invoke("robb-j.xml.format", editor); 310 | } 311 | })); 312 | }); 313 | nova.commands.register( 314 | "robb-j.xml.format", 315 | (editor) => formatCommand(editor, langServer) 316 | ); 317 | nova.commands.register( 318 | "robb-j.xml.rename", 319 | (editor) => renameCommand(editor, langServer) 320 | ); 321 | nova.commands.register("robb-j.xml.restart", () => restartCommand(langServer)); 322 | nova.workspace.config.observe("xml.catalogs", () => { 323 | restartCommand(langServer); 324 | }); 325 | } 326 | function deactivate() { 327 | debug5("#deactivate"); 328 | } 329 | -------------------------------------------------------------------------------- /XML.novaextension/bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/bin/.gitkeep -------------------------------------------------------------------------------- /XML.novaextension/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "robb-j.XML", 3 | "name": "XML", 4 | "organization": "robb-j", 5 | "description": "Adds XML linting, schema support and formatting to Nova", 6 | "version": "0.4.1", 7 | "categories": ["completions", "languages", "issues"], 8 | "license": "MIT", 9 | "repository": "https://github.com/robb-j/nova-xml", 10 | "bugs": "https://github.com/robb-j/nova-xml/issues", 11 | 12 | "main": "main.dist.js", 13 | 14 | "activationEvents": ["onLanguage:xml"], 15 | 16 | "entitlements": { 17 | "process": true, 18 | "filesystem": "readwrite" 19 | }, 20 | 21 | "config": [ 22 | { 23 | "title": "Format on Save", 24 | "key": "robb-j.xml.formatOnSave", 25 | "type": "boolean", 26 | "default": false 27 | } 28 | ], 29 | 30 | "configWorkspace": [ 31 | { 32 | "title": "XML Catalogs", 33 | "description": "Catalog files allows you to associate namespaces with a Schema files for validation custom validation.", 34 | "key": "xml.catalogs", 35 | "type": "pathArray", 36 | "link": "https://github.com/eclipse/lemminx/blob/main/docs/Configuration.md#catalog" 37 | } 38 | ], 39 | 40 | "commands": { 41 | "extensions": [ 42 | { 43 | "title": "Restart XML Server", 44 | "command": "robb-j.xml.restart" 45 | } 46 | ], 47 | "editor": [ 48 | { 49 | "title": "Format", 50 | "command": "robb-j.xml.format", 51 | "filters": { 52 | "syntaxes": ["xml"] 53 | } 54 | }, 55 | { 56 | "title": "Rename", 57 | "command": "robb-j.xml.rename", 58 | "filters": { 59 | "syntaxes": ["xml"] 60 | } 61 | } 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /XML.novaextension/extension.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/extension.png -------------------------------------------------------------------------------- /XML.novaextension/extension@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robb-j/nova-xml/c222b3036975a5b0ee07706c76927c12be584d1e/XML.novaextension/extension@2x.png -------------------------------------------------------------------------------- /bin/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Ensure the build fails if TypeScript fails 4 | set -e 5 | 6 | # Lint TypeScript source code 7 | npx tsc --noEmit --pretty 8 | 9 | # Bundle into JavaScript 10 | npx esbuild \ 11 | --bundle \ 12 | --format=cjs \ 13 | --target=es6 \ 14 | --platform=neutral \ 15 | --outfile=XML.novaextension/Scripts/main.dist.js \ 16 | src/Scripts/main.ts 17 | -------------------------------------------------------------------------------- /bin/get-server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | # URL=https://github.com/redhat-developer/vscode-xml/releases/download/0.29.0/lemminx-osx-aarch_64.zip 6 | URL=https://github.com/redhat-developer/vscode-xml/releases/download/0.29.0/lemminx-osx-x86_64.zip 7 | 8 | DIR=`dirname $0`/../XML.novaextension/bin 9 | 10 | echo "Fetching server..." 11 | curl -sfL $URL > $DIR/lemminx.zip 12 | 13 | echo "Unzipping..." 14 | unzip $DIR/lemminx.zip -d $DIR 15 | 16 | echo "Cleaning..." 17 | rm $DIR/lemminx.zip 18 | -------------------------------------------------------------------------------- /bin/grab-examples.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # A bit of a hack, but useful to test XML schemas, this script: 5 | # - looks for syntax and completion XML files from inside nova.app 6 | # - copies them into folders inside the examples directory 7 | # - adds xmlns attributes to and elements 8 | # 9 | 10 | mkdir -p examples/syntaxes 11 | mkdir -p examples/completions 12 | 13 | cp /Applications/Nova.app/Contents/SharedSupport/Extensions/*.novaextension/Syntaxes/*.xml examples/syntaxes 14 | cp /Applications/Nova.app/Contents/SharedSupport/Extensions/*.novaextension/Completions/*.xml examples/completions 15 | 16 | # for FILE in examples/syntaxes/*.xml 17 | # do 18 | # sed -i '' -E 's///' $FILE 19 | # done 20 | 21 | # Add a the syntax namespace to syntax files 22 | sed -i '' -E 's///' examples/syntaxes/*.xml 23 | 24 | # Add a the completions namespace to completions files 25 | sed -i '' -E 's///' examples/completions/*.xml 26 | -------------------------------------------------------------------------------- /examples/Inventory-catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/Inventory.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/Inventory.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | An inventory of things that a person might own 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | A collection of vehicles that a person owns 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | A motorcar, you know, with wheels 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/KitchenSink.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kitchen Sink 5 | script 6 | sink 7 | markdown 8 | 9 | 10 | 11 | 12 | sink,md.sink 13 | KitchenSink 14 | ^#!\/.+(sink)$ 15 | 16 | 17 | sink 18 | kitchen 19 | everything-in-the-sink 20 | 21 | 22 | 23 | 24 | 25 | 26 | (\{[^}\"']*$)|(\[[^\]\"']*$)|(\([^)\"']*$) 27 | 28 | 29 | ^\s*(\s*/\*.*\*/\s*)*[\}\]\)\\] 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | // 40 | 41 | 42 | 43 | /* 44 | 45 | 46 | */ 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | \/\/\s*(\w) 70 | 71 | 72 | 73 | 74 | \/* 75 | 76 | 77 | *\\ 78 | 79 | 80 | 81 | 82 | 83 | 84 | \b(let)\s+([a-zA-ZÀ-ÖØ-öø-ÿ_][A-Za-zÀ-ÖØ-öø-ÿ0-9_]*) 85 | 86 | 87 | 88 | 89 | 90 | 91 | (\[) 92 | 93 | 94 | 95 | (\]) 96 | 97 | 98 | 99 | 100 | \d+ 101 | 102 | 103 | true|false 104 | 105 | 106 | "[^"]*" 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | h1 127 | h2 128 | h3 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | kitchen 141 | sink 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /examples/catalog.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xml", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "xml", 8 | "devDependencies": { 9 | "@types/nova-editor-node": "^5.1.4", 10 | "esbuild": "^0.25.4", 11 | "husky": "^9.1.7", 12 | "lint-staged": "^16.2.4", 13 | "prettier": "^3.5.3", 14 | "typescript": "^5.8.3", 15 | "vscode-languageserver-protocol": "^3.16.0" 16 | } 17 | }, 18 | "node_modules/@esbuild/aix-ppc64": { 19 | "version": "0.25.11", 20 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", 21 | "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", 22 | "cpu": [ 23 | "ppc64" 24 | ], 25 | "dev": true, 26 | "optional": true, 27 | "os": [ 28 | "aix" 29 | ], 30 | "engines": { 31 | "node": ">=18" 32 | } 33 | }, 34 | "node_modules/@esbuild/android-arm": { 35 | "version": "0.25.11", 36 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", 37 | "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", 38 | "cpu": [ 39 | "arm" 40 | ], 41 | "dev": true, 42 | "optional": true, 43 | "os": [ 44 | "android" 45 | ], 46 | "engines": { 47 | "node": ">=18" 48 | } 49 | }, 50 | "node_modules/@esbuild/android-arm64": { 51 | "version": "0.25.11", 52 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", 53 | "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", 54 | "cpu": [ 55 | "arm64" 56 | ], 57 | "dev": true, 58 | "optional": true, 59 | "os": [ 60 | "android" 61 | ], 62 | "engines": { 63 | "node": ">=18" 64 | } 65 | }, 66 | "node_modules/@esbuild/android-x64": { 67 | "version": "0.25.11", 68 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", 69 | "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", 70 | "cpu": [ 71 | "x64" 72 | ], 73 | "dev": true, 74 | "optional": true, 75 | "os": [ 76 | "android" 77 | ], 78 | "engines": { 79 | "node": ">=18" 80 | } 81 | }, 82 | "node_modules/@esbuild/darwin-arm64": { 83 | "version": "0.25.11", 84 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", 85 | "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", 86 | "cpu": [ 87 | "arm64" 88 | ], 89 | "dev": true, 90 | "optional": true, 91 | "os": [ 92 | "darwin" 93 | ], 94 | "engines": { 95 | "node": ">=18" 96 | } 97 | }, 98 | "node_modules/@esbuild/darwin-x64": { 99 | "version": "0.25.11", 100 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", 101 | "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", 102 | "cpu": [ 103 | "x64" 104 | ], 105 | "dev": true, 106 | "optional": true, 107 | "os": [ 108 | "darwin" 109 | ], 110 | "engines": { 111 | "node": ">=18" 112 | } 113 | }, 114 | "node_modules/@esbuild/freebsd-arm64": { 115 | "version": "0.25.11", 116 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", 117 | "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", 118 | "cpu": [ 119 | "arm64" 120 | ], 121 | "dev": true, 122 | "optional": true, 123 | "os": [ 124 | "freebsd" 125 | ], 126 | "engines": { 127 | "node": ">=18" 128 | } 129 | }, 130 | "node_modules/@esbuild/freebsd-x64": { 131 | "version": "0.25.11", 132 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", 133 | "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", 134 | "cpu": [ 135 | "x64" 136 | ], 137 | "dev": true, 138 | "optional": true, 139 | "os": [ 140 | "freebsd" 141 | ], 142 | "engines": { 143 | "node": ">=18" 144 | } 145 | }, 146 | "node_modules/@esbuild/linux-arm": { 147 | "version": "0.25.11", 148 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", 149 | "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", 150 | "cpu": [ 151 | "arm" 152 | ], 153 | "dev": true, 154 | "optional": true, 155 | "os": [ 156 | "linux" 157 | ], 158 | "engines": { 159 | "node": ">=18" 160 | } 161 | }, 162 | "node_modules/@esbuild/linux-arm64": { 163 | "version": "0.25.11", 164 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", 165 | "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", 166 | "cpu": [ 167 | "arm64" 168 | ], 169 | "dev": true, 170 | "optional": true, 171 | "os": [ 172 | "linux" 173 | ], 174 | "engines": { 175 | "node": ">=18" 176 | } 177 | }, 178 | "node_modules/@esbuild/linux-ia32": { 179 | "version": "0.25.11", 180 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", 181 | "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", 182 | "cpu": [ 183 | "ia32" 184 | ], 185 | "dev": true, 186 | "optional": true, 187 | "os": [ 188 | "linux" 189 | ], 190 | "engines": { 191 | "node": ">=18" 192 | } 193 | }, 194 | "node_modules/@esbuild/linux-loong64": { 195 | "version": "0.25.11", 196 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", 197 | "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", 198 | "cpu": [ 199 | "loong64" 200 | ], 201 | "dev": true, 202 | "optional": true, 203 | "os": [ 204 | "linux" 205 | ], 206 | "engines": { 207 | "node": ">=18" 208 | } 209 | }, 210 | "node_modules/@esbuild/linux-mips64el": { 211 | "version": "0.25.11", 212 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", 213 | "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", 214 | "cpu": [ 215 | "mips64el" 216 | ], 217 | "dev": true, 218 | "optional": true, 219 | "os": [ 220 | "linux" 221 | ], 222 | "engines": { 223 | "node": ">=18" 224 | } 225 | }, 226 | "node_modules/@esbuild/linux-ppc64": { 227 | "version": "0.25.11", 228 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", 229 | "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", 230 | "cpu": [ 231 | "ppc64" 232 | ], 233 | "dev": true, 234 | "optional": true, 235 | "os": [ 236 | "linux" 237 | ], 238 | "engines": { 239 | "node": ">=18" 240 | } 241 | }, 242 | "node_modules/@esbuild/linux-riscv64": { 243 | "version": "0.25.11", 244 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", 245 | "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", 246 | "cpu": [ 247 | "riscv64" 248 | ], 249 | "dev": true, 250 | "optional": true, 251 | "os": [ 252 | "linux" 253 | ], 254 | "engines": { 255 | "node": ">=18" 256 | } 257 | }, 258 | "node_modules/@esbuild/linux-s390x": { 259 | "version": "0.25.11", 260 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", 261 | "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", 262 | "cpu": [ 263 | "s390x" 264 | ], 265 | "dev": true, 266 | "optional": true, 267 | "os": [ 268 | "linux" 269 | ], 270 | "engines": { 271 | "node": ">=18" 272 | } 273 | }, 274 | "node_modules/@esbuild/linux-x64": { 275 | "version": "0.25.11", 276 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", 277 | "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", 278 | "cpu": [ 279 | "x64" 280 | ], 281 | "dev": true, 282 | "optional": true, 283 | "os": [ 284 | "linux" 285 | ], 286 | "engines": { 287 | "node": ">=18" 288 | } 289 | }, 290 | "node_modules/@esbuild/netbsd-arm64": { 291 | "version": "0.25.11", 292 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", 293 | "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", 294 | "cpu": [ 295 | "arm64" 296 | ], 297 | "dev": true, 298 | "optional": true, 299 | "os": [ 300 | "netbsd" 301 | ], 302 | "engines": { 303 | "node": ">=18" 304 | } 305 | }, 306 | "node_modules/@esbuild/netbsd-x64": { 307 | "version": "0.25.11", 308 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", 309 | "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", 310 | "cpu": [ 311 | "x64" 312 | ], 313 | "dev": true, 314 | "optional": true, 315 | "os": [ 316 | "netbsd" 317 | ], 318 | "engines": { 319 | "node": ">=18" 320 | } 321 | }, 322 | "node_modules/@esbuild/openbsd-arm64": { 323 | "version": "0.25.11", 324 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", 325 | "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", 326 | "cpu": [ 327 | "arm64" 328 | ], 329 | "dev": true, 330 | "optional": true, 331 | "os": [ 332 | "openbsd" 333 | ], 334 | "engines": { 335 | "node": ">=18" 336 | } 337 | }, 338 | "node_modules/@esbuild/openbsd-x64": { 339 | "version": "0.25.11", 340 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", 341 | "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", 342 | "cpu": [ 343 | "x64" 344 | ], 345 | "dev": true, 346 | "optional": true, 347 | "os": [ 348 | "openbsd" 349 | ], 350 | "engines": { 351 | "node": ">=18" 352 | } 353 | }, 354 | "node_modules/@esbuild/openharmony-arm64": { 355 | "version": "0.25.11", 356 | "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", 357 | "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", 358 | "cpu": [ 359 | "arm64" 360 | ], 361 | "dev": true, 362 | "optional": true, 363 | "os": [ 364 | "openharmony" 365 | ], 366 | "engines": { 367 | "node": ">=18" 368 | } 369 | }, 370 | "node_modules/@esbuild/sunos-x64": { 371 | "version": "0.25.11", 372 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", 373 | "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", 374 | "cpu": [ 375 | "x64" 376 | ], 377 | "dev": true, 378 | "optional": true, 379 | "os": [ 380 | "sunos" 381 | ], 382 | "engines": { 383 | "node": ">=18" 384 | } 385 | }, 386 | "node_modules/@esbuild/win32-arm64": { 387 | "version": "0.25.11", 388 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", 389 | "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", 390 | "cpu": [ 391 | "arm64" 392 | ], 393 | "dev": true, 394 | "optional": true, 395 | "os": [ 396 | "win32" 397 | ], 398 | "engines": { 399 | "node": ">=18" 400 | } 401 | }, 402 | "node_modules/@esbuild/win32-ia32": { 403 | "version": "0.25.11", 404 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", 405 | "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", 406 | "cpu": [ 407 | "ia32" 408 | ], 409 | "dev": true, 410 | "optional": true, 411 | "os": [ 412 | "win32" 413 | ], 414 | "engines": { 415 | "node": ">=18" 416 | } 417 | }, 418 | "node_modules/@esbuild/win32-x64": { 419 | "version": "0.25.11", 420 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", 421 | "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", 422 | "cpu": [ 423 | "x64" 424 | ], 425 | "dev": true, 426 | "optional": true, 427 | "os": [ 428 | "win32" 429 | ], 430 | "engines": { 431 | "node": ">=18" 432 | } 433 | }, 434 | "node_modules/@types/nova-editor-node": { 435 | "version": "5.1.5", 436 | "resolved": "https://registry.npmjs.org/@types/nova-editor-node/-/nova-editor-node-5.1.5.tgz", 437 | "integrity": "sha512-QOgjOWVvsqGbI3yyWDMkrCLY4BYbfjve/NV18DK9ADobX4KK/Pv7qDDvBNZoOT311rhceh33X7f0VL4jPYVQgw==", 438 | "dev": true 439 | }, 440 | "node_modules/ansi-escapes": { 441 | "version": "7.1.1", 442 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", 443 | "integrity": "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q==", 444 | "dev": true, 445 | "dependencies": { 446 | "environment": "^1.0.0" 447 | }, 448 | "engines": { 449 | "node": ">=18" 450 | }, 451 | "funding": { 452 | "url": "https://github.com/sponsors/sindresorhus" 453 | } 454 | }, 455 | "node_modules/ansi-regex": { 456 | "version": "6.2.2", 457 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", 458 | "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", 459 | "dev": true, 460 | "engines": { 461 | "node": ">=12" 462 | }, 463 | "funding": { 464 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 465 | } 466 | }, 467 | "node_modules/ansi-styles": { 468 | "version": "6.2.3", 469 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", 470 | "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", 471 | "dev": true, 472 | "engines": { 473 | "node": ">=12" 474 | }, 475 | "funding": { 476 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 477 | } 478 | }, 479 | "node_modules/braces": { 480 | "version": "3.0.3", 481 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", 482 | "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", 483 | "dev": true, 484 | "dependencies": { 485 | "fill-range": "^7.1.1" 486 | }, 487 | "engines": { 488 | "node": ">=8" 489 | } 490 | }, 491 | "node_modules/cli-cursor": { 492 | "version": "5.0.0", 493 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", 494 | "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", 495 | "dev": true, 496 | "dependencies": { 497 | "restore-cursor": "^5.0.0" 498 | }, 499 | "engines": { 500 | "node": ">=18" 501 | }, 502 | "funding": { 503 | "url": "https://github.com/sponsors/sindresorhus" 504 | } 505 | }, 506 | "node_modules/cli-truncate": { 507 | "version": "5.1.0", 508 | "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.0.tgz", 509 | "integrity": "sha512-7JDGG+4Zp0CsknDCedl0DYdaeOhc46QNpXi3NLQblkZpXXgA6LncLDUUyvrjSvZeF3VRQa+KiMGomazQrC1V8g==", 510 | "dev": true, 511 | "dependencies": { 512 | "slice-ansi": "^7.1.0", 513 | "string-width": "^8.0.0" 514 | }, 515 | "engines": { 516 | "node": ">=20" 517 | }, 518 | "funding": { 519 | "url": "https://github.com/sponsors/sindresorhus" 520 | } 521 | }, 522 | "node_modules/colorette": { 523 | "version": "2.0.20", 524 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", 525 | "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", 526 | "dev": true 527 | }, 528 | "node_modules/commander": { 529 | "version": "14.0.1", 530 | "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz", 531 | "integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==", 532 | "dev": true, 533 | "engines": { 534 | "node": ">=20" 535 | } 536 | }, 537 | "node_modules/emoji-regex": { 538 | "version": "10.6.0", 539 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", 540 | "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", 541 | "dev": true 542 | }, 543 | "node_modules/environment": { 544 | "version": "1.1.0", 545 | "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", 546 | "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", 547 | "dev": true, 548 | "engines": { 549 | "node": ">=18" 550 | }, 551 | "funding": { 552 | "url": "https://github.com/sponsors/sindresorhus" 553 | } 554 | }, 555 | "node_modules/esbuild": { 556 | "version": "0.25.11", 557 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", 558 | "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", 559 | "dev": true, 560 | "hasInstallScript": true, 561 | "bin": { 562 | "esbuild": "bin/esbuild" 563 | }, 564 | "engines": { 565 | "node": ">=18" 566 | }, 567 | "optionalDependencies": { 568 | "@esbuild/aix-ppc64": "0.25.11", 569 | "@esbuild/android-arm": "0.25.11", 570 | "@esbuild/android-arm64": "0.25.11", 571 | "@esbuild/android-x64": "0.25.11", 572 | "@esbuild/darwin-arm64": "0.25.11", 573 | "@esbuild/darwin-x64": "0.25.11", 574 | "@esbuild/freebsd-arm64": "0.25.11", 575 | "@esbuild/freebsd-x64": "0.25.11", 576 | "@esbuild/linux-arm": "0.25.11", 577 | "@esbuild/linux-arm64": "0.25.11", 578 | "@esbuild/linux-ia32": "0.25.11", 579 | "@esbuild/linux-loong64": "0.25.11", 580 | "@esbuild/linux-mips64el": "0.25.11", 581 | "@esbuild/linux-ppc64": "0.25.11", 582 | "@esbuild/linux-riscv64": "0.25.11", 583 | "@esbuild/linux-s390x": "0.25.11", 584 | "@esbuild/linux-x64": "0.25.11", 585 | "@esbuild/netbsd-arm64": "0.25.11", 586 | "@esbuild/netbsd-x64": "0.25.11", 587 | "@esbuild/openbsd-arm64": "0.25.11", 588 | "@esbuild/openbsd-x64": "0.25.11", 589 | "@esbuild/openharmony-arm64": "0.25.11", 590 | "@esbuild/sunos-x64": "0.25.11", 591 | "@esbuild/win32-arm64": "0.25.11", 592 | "@esbuild/win32-ia32": "0.25.11", 593 | "@esbuild/win32-x64": "0.25.11" 594 | } 595 | }, 596 | "node_modules/eventemitter3": { 597 | "version": "5.0.1", 598 | "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", 599 | "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", 600 | "dev": true 601 | }, 602 | "node_modules/fill-range": { 603 | "version": "7.1.1", 604 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", 605 | "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", 606 | "dev": true, 607 | "dependencies": { 608 | "to-regex-range": "^5.0.1" 609 | }, 610 | "engines": { 611 | "node": ">=8" 612 | } 613 | }, 614 | "node_modules/get-east-asian-width": { 615 | "version": "1.4.0", 616 | "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", 617 | "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", 618 | "dev": true, 619 | "engines": { 620 | "node": ">=18" 621 | }, 622 | "funding": { 623 | "url": "https://github.com/sponsors/sindresorhus" 624 | } 625 | }, 626 | "node_modules/husky": { 627 | "version": "9.1.7", 628 | "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", 629 | "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", 630 | "dev": true, 631 | "license": "MIT", 632 | "bin": { 633 | "husky": "bin.js" 634 | }, 635 | "engines": { 636 | "node": ">=18" 637 | }, 638 | "funding": { 639 | "url": "https://github.com/sponsors/typicode" 640 | } 641 | }, 642 | "node_modules/is-fullwidth-code-point": { 643 | "version": "5.1.0", 644 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", 645 | "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", 646 | "dev": true, 647 | "dependencies": { 648 | "get-east-asian-width": "^1.3.1" 649 | }, 650 | "engines": { 651 | "node": ">=18" 652 | }, 653 | "funding": { 654 | "url": "https://github.com/sponsors/sindresorhus" 655 | } 656 | }, 657 | "node_modules/is-number": { 658 | "version": "7.0.0", 659 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 660 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 661 | "dev": true, 662 | "engines": { 663 | "node": ">=0.12.0" 664 | } 665 | }, 666 | "node_modules/lint-staged": { 667 | "version": "16.2.4", 668 | "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.2.4.tgz", 669 | "integrity": "sha512-Pkyr/wd90oAyXk98i/2KwfkIhoYQUMtss769FIT9hFM5ogYZwrk+GRE46yKXSg2ZGhcJ1p38Gf5gmI5Ohjg2yg==", 670 | "dev": true, 671 | "dependencies": { 672 | "commander": "^14.0.1", 673 | "listr2": "^9.0.4", 674 | "micromatch": "^4.0.8", 675 | "nano-spawn": "^2.0.0", 676 | "pidtree": "^0.6.0", 677 | "string-argv": "^0.3.2", 678 | "yaml": "^2.8.1" 679 | }, 680 | "bin": { 681 | "lint-staged": "bin/lint-staged.js" 682 | }, 683 | "engines": { 684 | "node": ">=20.17" 685 | }, 686 | "funding": { 687 | "url": "https://opencollective.com/lint-staged" 688 | } 689 | }, 690 | "node_modules/listr2": { 691 | "version": "9.0.4", 692 | "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.4.tgz", 693 | "integrity": "sha512-1wd/kpAdKRLwv7/3OKC8zZ5U8e/fajCfWMxacUvB79S5nLrYGPtUI/8chMQhn3LQjsRVErTb9i1ECAwW0ZIHnQ==", 694 | "dev": true, 695 | "dependencies": { 696 | "cli-truncate": "^5.0.0", 697 | "colorette": "^2.0.20", 698 | "eventemitter3": "^5.0.1", 699 | "log-update": "^6.1.0", 700 | "rfdc": "^1.4.1", 701 | "wrap-ansi": "^9.0.0" 702 | }, 703 | "engines": { 704 | "node": ">=20.0.0" 705 | } 706 | }, 707 | "node_modules/log-update": { 708 | "version": "6.1.0", 709 | "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", 710 | "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", 711 | "dev": true, 712 | "dependencies": { 713 | "ansi-escapes": "^7.0.0", 714 | "cli-cursor": "^5.0.0", 715 | "slice-ansi": "^7.1.0", 716 | "strip-ansi": "^7.1.0", 717 | "wrap-ansi": "^9.0.0" 718 | }, 719 | "engines": { 720 | "node": ">=18" 721 | }, 722 | "funding": { 723 | "url": "https://github.com/sponsors/sindresorhus" 724 | } 725 | }, 726 | "node_modules/micromatch": { 727 | "version": "4.0.8", 728 | "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", 729 | "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", 730 | "dev": true, 731 | "dependencies": { 732 | "braces": "^3.0.3", 733 | "picomatch": "^2.3.1" 734 | }, 735 | "engines": { 736 | "node": ">=8.6" 737 | } 738 | }, 739 | "node_modules/mimic-function": { 740 | "version": "5.0.1", 741 | "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", 742 | "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", 743 | "dev": true, 744 | "engines": { 745 | "node": ">=18" 746 | }, 747 | "funding": { 748 | "url": "https://github.com/sponsors/sindresorhus" 749 | } 750 | }, 751 | "node_modules/nano-spawn": { 752 | "version": "2.0.0", 753 | "resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz", 754 | "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", 755 | "dev": true, 756 | "engines": { 757 | "node": ">=20.17" 758 | }, 759 | "funding": { 760 | "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" 761 | } 762 | }, 763 | "node_modules/onetime": { 764 | "version": "7.0.0", 765 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", 766 | "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", 767 | "dev": true, 768 | "dependencies": { 769 | "mimic-function": "^5.0.0" 770 | }, 771 | "engines": { 772 | "node": ">=18" 773 | }, 774 | "funding": { 775 | "url": "https://github.com/sponsors/sindresorhus" 776 | } 777 | }, 778 | "node_modules/picomatch": { 779 | "version": "2.3.1", 780 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 781 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 782 | "dev": true, 783 | "engines": { 784 | "node": ">=8.6" 785 | }, 786 | "funding": { 787 | "url": "https://github.com/sponsors/jonschlinkert" 788 | } 789 | }, 790 | "node_modules/pidtree": { 791 | "version": "0.6.0", 792 | "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", 793 | "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", 794 | "dev": true, 795 | "bin": { 796 | "pidtree": "bin/pidtree.js" 797 | }, 798 | "engines": { 799 | "node": ">=0.10" 800 | } 801 | }, 802 | "node_modules/prettier": { 803 | "version": "3.6.2", 804 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", 805 | "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", 806 | "dev": true, 807 | "bin": { 808 | "prettier": "bin/prettier.cjs" 809 | }, 810 | "engines": { 811 | "node": ">=14" 812 | }, 813 | "funding": { 814 | "url": "https://github.com/prettier/prettier?sponsor=1" 815 | } 816 | }, 817 | "node_modules/restore-cursor": { 818 | "version": "5.1.0", 819 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", 820 | "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", 821 | "dev": true, 822 | "dependencies": { 823 | "onetime": "^7.0.0", 824 | "signal-exit": "^4.1.0" 825 | }, 826 | "engines": { 827 | "node": ">=18" 828 | }, 829 | "funding": { 830 | "url": "https://github.com/sponsors/sindresorhus" 831 | } 832 | }, 833 | "node_modules/restore-cursor/node_modules/signal-exit": { 834 | "version": "4.1.0", 835 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 836 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 837 | "dev": true, 838 | "engines": { 839 | "node": ">=14" 840 | }, 841 | "funding": { 842 | "url": "https://github.com/sponsors/isaacs" 843 | } 844 | }, 845 | "node_modules/rfdc": { 846 | "version": "1.4.1", 847 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", 848 | "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", 849 | "dev": true 850 | }, 851 | "node_modules/slice-ansi": { 852 | "version": "7.1.2", 853 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", 854 | "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", 855 | "dev": true, 856 | "dependencies": { 857 | "ansi-styles": "^6.2.1", 858 | "is-fullwidth-code-point": "^5.0.0" 859 | }, 860 | "engines": { 861 | "node": ">=18" 862 | }, 863 | "funding": { 864 | "url": "https://github.com/chalk/slice-ansi?sponsor=1" 865 | } 866 | }, 867 | "node_modules/string-argv": { 868 | "version": "0.3.2", 869 | "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", 870 | "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", 871 | "dev": true, 872 | "engines": { 873 | "node": ">=0.6.19" 874 | } 875 | }, 876 | "node_modules/string-width": { 877 | "version": "8.1.0", 878 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.0.tgz", 879 | "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", 880 | "dev": true, 881 | "dependencies": { 882 | "get-east-asian-width": "^1.3.0", 883 | "strip-ansi": "^7.1.0" 884 | }, 885 | "engines": { 886 | "node": ">=20" 887 | }, 888 | "funding": { 889 | "url": "https://github.com/sponsors/sindresorhus" 890 | } 891 | }, 892 | "node_modules/strip-ansi": { 893 | "version": "7.1.2", 894 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", 895 | "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", 896 | "dev": true, 897 | "dependencies": { 898 | "ansi-regex": "^6.0.1" 899 | }, 900 | "engines": { 901 | "node": ">=12" 902 | }, 903 | "funding": { 904 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 905 | } 906 | }, 907 | "node_modules/to-regex-range": { 908 | "version": "5.0.1", 909 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 910 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 911 | "dev": true, 912 | "dependencies": { 913 | "is-number": "^7.0.0" 914 | }, 915 | "engines": { 916 | "node": ">=8.0" 917 | } 918 | }, 919 | "node_modules/typescript": { 920 | "version": "5.9.3", 921 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 922 | "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 923 | "dev": true, 924 | "bin": { 925 | "tsc": "bin/tsc", 926 | "tsserver": "bin/tsserver" 927 | }, 928 | "engines": { 929 | "node": ">=14.17" 930 | } 931 | }, 932 | "node_modules/vscode-jsonrpc": { 933 | "version": "8.2.0", 934 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 935 | "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 936 | "dev": true, 937 | "engines": { 938 | "node": ">=14.0.0" 939 | } 940 | }, 941 | "node_modules/vscode-languageserver-protocol": { 942 | "version": "3.17.5", 943 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 944 | "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 945 | "dev": true, 946 | "dependencies": { 947 | "vscode-jsonrpc": "8.2.0", 948 | "vscode-languageserver-types": "3.17.5" 949 | } 950 | }, 951 | "node_modules/vscode-languageserver-types": { 952 | "version": "3.17.5", 953 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 954 | "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", 955 | "dev": true 956 | }, 957 | "node_modules/wrap-ansi": { 958 | "version": "9.0.2", 959 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", 960 | "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", 961 | "dev": true, 962 | "dependencies": { 963 | "ansi-styles": "^6.2.1", 964 | "string-width": "^7.0.0", 965 | "strip-ansi": "^7.1.0" 966 | }, 967 | "engines": { 968 | "node": ">=18" 969 | }, 970 | "funding": { 971 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 972 | } 973 | }, 974 | "node_modules/wrap-ansi/node_modules/string-width": { 975 | "version": "7.2.0", 976 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", 977 | "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", 978 | "dev": true, 979 | "dependencies": { 980 | "emoji-regex": "^10.3.0", 981 | "get-east-asian-width": "^1.0.0", 982 | "strip-ansi": "^7.1.0" 983 | }, 984 | "engines": { 985 | "node": ">=18" 986 | }, 987 | "funding": { 988 | "url": "https://github.com/sponsors/sindresorhus" 989 | } 990 | }, 991 | "node_modules/yaml": { 992 | "version": "2.8.1", 993 | "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", 994 | "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", 995 | "dev": true, 996 | "bin": { 997 | "yaml": "bin.mjs" 998 | }, 999 | "engines": { 1000 | "node": ">= 14.6" 1001 | } 1002 | } 1003 | } 1004 | } 1005 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xml", 3 | "private": true, 4 | "scripts": { 5 | "format": "prettier --write '**/*.{js,ts,tsx,json,css,md}'" 6 | }, 7 | "devDependencies": { 8 | "@types/nova-editor-node": "^5.1.4", 9 | "esbuild": "^0.25.4", 10 | "husky": "^9.1.7", 11 | "lint-staged": "^16.2.4", 12 | "prettier": "^3.5.3", 13 | "typescript": "^5.8.3", 14 | "vscode-languageserver-protocol": "^3.16.0" 15 | }, 16 | "lint-staged": { 17 | "*.{js,ts,tsx,json,css,md}": [ 18 | "npx prettier --write" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Scripts/commands/format-command.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DocumentFormattingParams, 3 | TextEdit, 4 | } from 'vscode-languageserver-protocol' 5 | 6 | import { createDebug, getEditorRange } from '../utils' 7 | import { XmlLanguageServer } from '../xml-language-server' 8 | 9 | const debug = createDebug('format') 10 | 11 | export async function formatCommand( 12 | editor: TextEditor, 13 | { languageClient }: XmlLanguageServer, 14 | ) { 15 | if (!languageClient) { 16 | debug('LanguageServer not running') 17 | return 18 | } 19 | 20 | const params: DocumentFormattingParams = { 21 | textDocument: { 22 | uri: editor.document.uri, 23 | }, 24 | options: { 25 | tabSize: editor.tabLength, 26 | insertSpaces: Boolean(editor.softTabs), 27 | }, 28 | } 29 | 30 | debug('format', params) 31 | 32 | const result = (await languageClient.sendRequest( 33 | 'textDocument/formatting', 34 | params, 35 | )) as TextEdit[] | null 36 | 37 | if (!result) return 38 | 39 | editor.edit((edit) => { 40 | for (const change of result.reverse()) { 41 | edit.replace( 42 | getEditorRange(editor.document, change.range), 43 | change.newText, 44 | ) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/Scripts/commands/rename-command.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | RenameParams, 3 | WorkspaceEdit, 4 | } from 'vscode-languageserver-protocol' 5 | import { createDebug, getEditorRange, getLspRange } from '../utils' 6 | import { XmlLanguageServer } from '../xml-language-server' 7 | 8 | const debug = createDebug('format') 9 | 10 | // Shamelessly stolen from 11 | // https://github.com/apexskier/nova-typescript/blob/main/src/commands/rename.ts 12 | 13 | export async function renameCommand( 14 | editor: TextEditor, 15 | { languageClient }: XmlLanguageServer, 16 | ) { 17 | debug('format', editor.document.uri) 18 | 19 | if (!languageClient) { 20 | debug('LanguageServer not running') 21 | return 22 | } 23 | 24 | // Select the whole of a word 25 | editor.selectWordsContainingCursors() 26 | 27 | const selectedPosition = getLspRange( 28 | editor.document, 29 | editor.selectedRange, 30 | )?.start 31 | if (!selectedPosition) return debug('Nothing selected') 32 | 33 | const newName = await new Promise((resolve) => { 34 | nova.workspace.showInputPalette( 35 | 'New name for symbol', 36 | { placeholder: editor.selectedText, value: editor.selectedText }, 37 | resolve, 38 | ) 39 | }) 40 | if (!newName || newName == editor.selectedText) { 41 | return 42 | } 43 | 44 | debug('newName', newName) 45 | 46 | const params: RenameParams = { 47 | textDocument: { uri: editor.document.uri }, 48 | position: selectedPosition, 49 | newName, 50 | } 51 | 52 | const result = (await languageClient.sendRequest( 53 | 'textDocument/rename', 54 | params, 55 | )) as WorkspaceEdit | null 56 | 57 | debug('result', result) 58 | if (!result) return 59 | 60 | for (const uri in result.changes) { 61 | const editor = await nova.workspace.openFile(uri) 62 | if (!editor) { 63 | debug('Failed to open', uri) 64 | continue 65 | } 66 | const changes = result.changes[uri] 67 | if (!changes) { 68 | debug('TODO: add support for documentChanges', uri) 69 | continue 70 | } 71 | 72 | editor.edit((edit) => { 73 | for (const change of changes.reverse()) { 74 | edit.replace( 75 | getEditorRange(editor.document, change.range), 76 | change.newText, 77 | ) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Scripts/commands/restart-command.ts: -------------------------------------------------------------------------------- 1 | import { XmlLanguageServer } from '../xml-language-server' 2 | import { createDebug } from '../utils' 3 | 4 | const debug = createDebug('restart') 5 | 6 | export async function restartCommand(langServer: XmlLanguageServer) { 7 | debug('Restarting') 8 | 9 | langServer.stop() 10 | langServer.start() 11 | } 12 | -------------------------------------------------------------------------------- /src/Scripts/main.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Extension entry point 3 | // 4 | 5 | import { createDebug } from './utils' 6 | import { XmlLanguageServer } from './xml-language-server' 7 | 8 | import { formatCommand } from './commands/format-command' 9 | import { renameCommand } from './commands/rename-command' 10 | import { restartCommand } from './commands/restart-command' 11 | 12 | const debug = createDebug('main') 13 | 14 | export function activate() { 15 | debug('#activate') 16 | 17 | const langServer = new XmlLanguageServer() 18 | nova.subscriptions.add(langServer) 19 | 20 | nova.workspace.onDidAddTextEditor((editor) => { 21 | editor.onWillSave(async () => { 22 | if (editor.document.syntax !== 'xml') return 23 | 24 | if (nova.config.get('robb-j.xml.formatOnSave', 'boolean') ?? false) { 25 | await nova.commands.invoke('robb-j.xml.format', editor) 26 | } 27 | }) 28 | }) 29 | nova.commands.register('robb-j.xml.format', (editor) => 30 | formatCommand(editor, langServer), 31 | ) 32 | nova.commands.register('robb-j.xml.rename', (editor) => 33 | renameCommand(editor, langServer), 34 | ) 35 | nova.commands.register('robb-j.xml.restart', () => restartCommand(langServer)) 36 | 37 | nova.workspace.config.observe('xml.catalogs', () => { 38 | restartCommand(langServer) 39 | }) 40 | } 41 | 42 | export function deactivate() { 43 | debug('#deactivate') 44 | } 45 | -------------------------------------------------------------------------------- /src/Scripts/utils.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Utility files to help out and make code more readable 3 | // 4 | 5 | import type { Range as LspRange } from 'vscode-languageserver-protocol' 6 | 7 | /** 8 | * Generate a method for namespaced debug-only logging, 9 | * inspired by https://github.com/visionmedia/debug. 10 | * 11 | * - prints messages under a namespace 12 | * - only outputs logs when nova.inDevMode() 13 | * - converts object arguments to json 14 | */ 15 | export function createDebug(namespace: string) { 16 | return (...args: any[]) => { 17 | if (!nova.inDevMode()) return 18 | 19 | const humanArgs = args.map((arg) => 20 | typeof arg === 'object' ? JSON.stringify(arg) : arg, 21 | ) 22 | console.info(`${namespace}:`, ...humanArgs) 23 | } 24 | } 25 | 26 | /** 27 | * Shamelessly stolen from 28 | * https://github.com/apexskier/nova-typescript/blob/2d4c1d8e61ca4afba6ee9ad1977a765e8cd0f037/src/lspNovaConversions.ts#L29 29 | */ 30 | export function getEditorRange(document: TextDocument, range: LspRange): Range { 31 | const fullContents = document.getTextInRange(new Range(0, document.length)) 32 | let rangeStart = 0 33 | let rangeEnd = 0 34 | let chars = 0 35 | const lines = fullContents.split(document.eol) 36 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 37 | const lineLength = lines[lineIndex].length + document.eol.length 38 | if (range.start.line === lineIndex) { 39 | rangeStart = chars + range.start.character 40 | } 41 | if (range.end.line === lineIndex) { 42 | rangeEnd = chars + range.end.character 43 | break 44 | } 45 | chars += lineLength 46 | } 47 | return new Range(rangeStart, rangeEnd) 48 | } 49 | 50 | export function getLspRange( 51 | document: TextDocument, 52 | range: Range, 53 | ): LspRange | null { 54 | const fullContents = document.getTextInRange(new Range(0, document.length)) 55 | let chars = 0 56 | let startLspRange: LspRange['start'] | undefined 57 | const lines = fullContents.split(document.eol) 58 | for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) { 59 | const lineLength = lines[lineIndex].length + document.eol.length 60 | if (!startLspRange && chars + lineLength >= range.start) { 61 | const character = range.start - chars 62 | startLspRange = { line: lineIndex, character } 63 | } 64 | if (startLspRange && chars + lineLength >= range.end) { 65 | const character = range.end - chars 66 | return { start: startLspRange, end: { line: lineIndex, character } } 67 | } 68 | chars += lineLength 69 | } 70 | return null 71 | } 72 | 73 | /** 74 | * Output put a potentially unknown error 75 | */ 76 | export function logError(message: string, error: unknown) { 77 | console.error(message) 78 | 79 | if (error instanceof Error) { 80 | console.error(error.message) 81 | console.error(error.stack) 82 | } else { 83 | console.error('An non-error was thrown') 84 | console.error(error) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Scripts/xml-language-server.ts: -------------------------------------------------------------------------------- 1 | import { createDebug, logError } from './utils' 2 | 3 | type ServerOptions = ConstructorParameters[2] 4 | 5 | const debug = createDebug('xml-language-server') 6 | 7 | export class XmlLanguageServer { 8 | languageClient: LanguageClient | null = null 9 | 10 | constructor() { 11 | debug('#new') 12 | 13 | this.start() 14 | } 15 | 16 | async start() { 17 | if (this.languageClient) { 18 | this.languageClient.stop() 19 | this.languageClient = null 20 | } 21 | 22 | try { 23 | debug('#start') 24 | 25 | const packageDir = nova.inDevMode() 26 | ? nova.extension.path 27 | : nova.extension.globalStoragePath 28 | 29 | const catalogs = nova.workspace.config.get('xml.catalogs', 'array') ?? [] 30 | catalogs.push(nova.path.join(packageDir, 'Schemas/catalog.xml')) 31 | 32 | for (let i = 0; i < catalogs.length; i++) { 33 | if (catalogs[i].startsWith('/')) continue 34 | catalogs[i] = nova.path.join(nova.workspace.path!, catalogs[i]) 35 | } 36 | 37 | const serverOptions = { 38 | type: 'stdio' as const, 39 | path: nova.path.join(packageDir, 'bin/lemminx-osx-x86_64'), 40 | } 41 | 42 | const clientOptions = { 43 | syntaxes: ['xml'], 44 | initializationOptions: { 45 | settings: { 46 | xml: { catalogs }, 47 | }, 48 | }, 49 | // debug: true, 50 | } 51 | 52 | await this.prepareBinary(serverOptions.path) 53 | 54 | debug('serverOptions', serverOptions) 55 | debug('clientOptions', clientOptions) 56 | 57 | const client = new LanguageClient( 58 | 'robb-j.xml', 59 | 'XML LanguageClient', 60 | serverOptions, 61 | clientOptions, 62 | ) 63 | 64 | client.start() 65 | 66 | this.languageClient = client 67 | 68 | this.setupLanguageServer(client) 69 | } catch (error) { 70 | logError('LanguageServer Failed', error) 71 | } 72 | } 73 | 74 | dispose() { 75 | debug('#dispose') 76 | this.stop() 77 | } 78 | 79 | async stop() { 80 | debug('#stop') 81 | if (this.languageClient) { 82 | this.languageClient.stop() 83 | this.languageClient = null 84 | } 85 | } 86 | 87 | // Make sure the LSP server binary is executable 88 | prepareBinary(path: string) { 89 | // Ensure the binary has execute permissions 90 | return new Promise((resolve, reject) => { 91 | const process = new Process('/usr/bin/env', { 92 | args: ['chmod', 'u+x', path], 93 | }) 94 | process.onDidExit((status) => (status === 0 ? resolve() : reject())) 95 | process.start() 96 | }) 97 | } 98 | 99 | setupLanguageServer(client: LanguageClient) { 100 | client.onDidStop((err) => { 101 | debug('Language Server Stopped', err?.message) 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Nova", 4 | 5 | "compilerOptions": { 6 | "target": "esnext", 7 | "module": "ES2020", 8 | "moduleResolution": "node", 9 | "newLine": "lf", 10 | "strict": true, 11 | "noEmitOnError": true, 12 | "skipLibCheck": true, 13 | 14 | "lib": [], 15 | 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "forceConsistentCasingInFileNames": true 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------