* Added doc lens links #227

* Test updates

* Fix test

* Update dependencies
This commit is contained in:
Bernie White 2022-04-10 12:32:37 +10:00 коммит произвёл GitHub
Родитель 87863de218
Коммит 2d06322c43
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 547 добавлений и 70 удалений

Просмотреть файл

@ -14,6 +14,18 @@ Continue reading to see the changes included in the latest version.
## Unreleased
What's changed since v2.0.0:
- New features:
- **Experimental:** CodeLens provides links to open or create rule documentation.
[#227](https://github.com/microsoft/PSRule-vscode/issues/227)
- Link from rules allows markdown documentation to be created or edited.
- When existing markdown documentation exists, file is opened in editor.
- When documentation for a rule does not exist, a new file is created from a snippet.
- Added settings to configure the location for storing documentation,
and the snippet used to create documentation.
- To use try this feature, install the preview channel with experimental features enabled.
## v2.0.0
What's changed since v1.7.0:

Просмотреть файл

@ -14,10 +14,24 @@ Channel | Description | Version/ downloads
## Features
### CodeLens
<p align="center">
<img src="./docs/images/codelens-doc-link.png" alt="CodeLens showing link to create documentation" width="640px" />
</p>
- CodeLens shows links to create or edit markdown documentation from rules in YAML, JSON, or PowerShell.
- **Open documentation** &mdash; Opens rule markdown documentation in the editor.
- The location for storing documentation is configurable in the extension settings.
- By default, a locale specific folder is created in the same directory as the rule file.
- **Create documentation** &mdash; Creates a new markdown file for the rule based on a snippet.
- New markdown documentation is created with the built-in _Rule Doc_ snippet.
- An alternative snippet can be specified by configuring extension settings.
### IntelliSense
<p align="center">
<img src="https://raw.githubusercontent.com/microsoft/PSRule-vscode/main/docs/images/options-schema-flyout.png" alt="Options suggestion context menu" />
<img src="./docs/images/options-schema-flyout.png" alt="Options suggestion context menu" width="640px" />
</p>
- Adds IntelliSense and validation support for configuring options and resources.
@ -26,7 +40,7 @@ Channel | Description | Version/ downloads
- **Create resources** &mdash; define _baselines_ and _selectors_ by using pre-built snippets and IntelliSense.
<p align="center">
<img src="https://raw.githubusercontent.com/microsoft/PSRule-vscode/main/docs/images/snippet-rule-type.png" alt="Rule definition snippet" />
<img src="./docs/images/snippet-rule-type.png" alt="Rule definition snippet" width="520px" />
</p>
- Adds snippets for defining new rules.
@ -35,7 +49,7 @@ Channel | Description | Version/ downloads
IntelliSense can also be triggered by using the shortcut `Ctrl+Space`.
<p align="center">
<img src="https://raw.githubusercontent.com/microsoft/PSRule-vscode/main/docs/images/snippet-markdown.png" alt="Rule markdown documentation snippet" />
<img src="./docs/images/snippet-markdown.png" alt="Rule markdown documentation snippet" width="640px" />
</p>
- Adds snippets for creating markdown documentation.
@ -46,7 +60,7 @@ Channel | Description | Version/ downloads
### Quick tasks
<p align="center">
<img src="./docs/images/tasks-provider.png" alt="Built-in tasks shown in task list" />
<img src="./docs/images/tasks-provider.png" alt="Built-in tasks shown in task list" width="640px" />
</p>
- Adds quick tasks for analysis directly from Visual Studio Code.
@ -59,13 +73,18 @@ Channel | Description | Version/ downloads
In addition to configuring the [ps-rule.yaml] options file, the following settings are available.
Name | Description
---- | -----------
`PSRule.execution.notProcessedWarning` | Warn when objects are not processed by any rule.
`PSRule.experimental.enabled` | Enables experimental features in the PSRule extension.
`PSRule.notifications.showChannelUpgrade` | Determines if a notification to switch to the stable channel is shown on start up.
`PSRule.notifications.showPowerShellExtension` | Determines if a notification to install the PowerShell extension is shown on start up.
`PSRule.output.as` | Configures the output of analysis tasks, either summary or detailed.
Name | Description
---- | -----------
`PSRule.codeLens.ruleDocumentationLinks` | Enables Code Lens that displays links to rule documentation. This is an experimental feature that requires experimental features to be enabled.
`PSRule.documentation.path` | The path to look for rule documentation. When not set, the path containing rules will be used.
`PSRule.documentation.localePath` | The locale path to use for locating rule documentation. The VS Code locale will be used by default.
`PSRule.documentation.customSnippetPath` | The path to a file containing a rule documentation snippet. When not set, built-in PSRule snippets will be used.
`PSRule.documentation.snippet` | The name of a snippet to use when creating new rule documentation. By default, the built-in `Rule Doc` snippet will be used.
`PSRule.execution.notProcessedWarning` | Warn when objects are not processed by any rule.
`PSRule.experimental.enabled` | Enables experimental features in the PSRule extension.
`PSRule.notifications.showChannelUpgrade` | Determines if a notification to switch to the stable channel is shown on start up.
`PSRule.notifications.showPowerShellExtension` | Determines if a notification to install the PowerShell extension is shown on start up.
`PSRule.output.as` | Configures the output of analysis tasks, either summary or detailed.
## Support

Двоичные данные
docs/images/codelens-doc-link.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 56 KiB

153
package-lock.json сгенерированный
Просмотреть файл

@ -9,15 +9,17 @@
"version": "0.0.1",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"fs-extra": "^10.0.0",
"vscode-languageclient": "^7.0.0"
},
"devDependencies": {
"@types/fs-extra": "^9.0.13",
"@types/glob": "^7.2.0",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.23",
"@types/vscode": "1.66.0",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"@vscode/test-electron": "^2.1.3",
"ansi-regex": ">=6.0.1",
"esbuild": "^0.14.34",
@ -119,6 +121,15 @@
"node": ">= 6"
}
},
"node_modules/@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@ -1868,6 +1879,19 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"node_modules/fs-extra": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
"integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -2063,8 +2087,7 @@
"node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/growl": {
"version": "1.10.5",
@ -2135,6 +2158,18 @@
"node": ">=10"
}
},
"node_modules/hosted-git-info/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/htmlparser2": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
@ -2368,6 +2403,17 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/keytar": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
@ -2460,14 +2506,11 @@
}
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dependencies": {
"yallist": "^4.0.0"
},
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.3.tgz",
"integrity": "sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw==",
"engines": {
"node": ">=10"
"node": ">=12"
}
},
"node_modules/markdown-it": {
@ -3220,17 +3263,17 @@
"dev": true
},
"node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"dependencies": {
"lru-cache": "^6.0.0"
"lru-cache": "^7.4.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
"node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/serialize-javascript": {
@ -3627,6 +3670,14 @@
"integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
@ -3940,7 +3991,8 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yargs": {
"version": "16.2.0",
@ -4106,6 +4158,15 @@
"integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==",
"dev": true
},
"@types/fs-extra": {
"version": "9.0.13",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz",
"integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@ -5289,6 +5350,16 @@
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"fs-extra": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz",
"integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -5442,8 +5513,7 @@
"graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"growl": {
"version": "1.10.5",
@ -5491,6 +5561,17 @@
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
},
"dependencies": {
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
}
}
},
"htmlparser2": {
@ -5660,6 +5741,15 @@
"integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
"dev": true
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"keytar": {
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz",
@ -5733,12 +5823,9 @@
}
},
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"requires": {
"yallist": "^4.0.0"
}
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.3.tgz",
"integrity": "sha512-WY9wjJNQt9+PZilnLbuFKM+SwDull9+6IAguOrarOMoOHTcJ9GnXSO11+Gw6c7xtDkBkthR57OZMtZKYr+1CEw=="
},
"markdown-it": {
"version": "12.3.2",
@ -6288,11 +6375,11 @@
"dev": true
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"version": "7.3.6",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz",
"integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==",
"requires": {
"lru-cache": "^6.0.0"
"lru-cache": "^7.4.0"
}
},
"serialize-javascript": {
@ -6590,6 +6677,11 @@
"integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==",
"dev": true
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
},
"unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
@ -6847,7 +6939,8 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"yargs": {
"version": "16.2.0",

Просмотреть файл

@ -53,10 +53,48 @@
}
},
"contributes": {
"commands": [
{
"command": "PSRule.createOrEditDocumentation",
"title": "Create or edit documentation",
"shortTitle": "Edit documentation",
"category": "PSRule"
}
],
"configuration": [
{
"title": "PSRule",
"properties": {
"PSRule.codeLens.ruleDocumentationLinks": {
"type": "boolean",
"default": true,
"description": "Enables Code Lens that displays links to rule documentation. This is an experimental feature that requires experimental features to be enabled.",
"scope": "application"
},
"PSRule.documentation.path": {
"type": "string",
"default": null,
"description": "The path to look for rule documentation. When not set, the path containing rules will be used.",
"scope": "window"
},
"PSRule.documentation.localePath": {
"type": "string",
"default": null,
"description": "The locale path to use for locating rule documentation. The VS Code locale will be used by default.",
"scope": "window"
},
"PSRule.documentation.customSnippetPath": {
"type": "string",
"default": null,
"description": "The path to a file containing a rule documentation snippet. When not set, built-in PSRule snippets will be used.",
"scope": "window"
},
"PSRule.documentation.snippet": {
"type": "string",
"default": "Rule Doc",
"markdownDescription": "The name of a snippet to use when creating new rule documentation. By default, the built-in `Rule Doc` snippet will be used.",
"scope": "window"
},
"PSRule.execution.notProcessedWarning": {
"type": "boolean",
"default": false,
@ -277,7 +315,8 @@
"esbuild-watch": "npm run -S esbuild-base -- --sourcemap --watch"
},
"dependencies": {
"vscode-languageclient": "^7.0.0"
"vscode-languageclient": "^7.0.0",
"fs-extra": "^10.0.0"
},
"extensionDependencies": [
"vscode.powershell",
@ -288,8 +327,9 @@
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.23",
"@types/vscode": "1.66.0",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"@types/fs-extra": "^9.0.13",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"esbuild": "^0.14.34",
"eslint": "^8.12.0",
"glob": "^7.2.0",

Просмотреть файл

@ -1,5 +1,5 @@
{
"rule-doc": {
"Rule Doc": {
"prefix": "rule",
"description": "Rule documentation",
"body": [
@ -19,7 +19,7 @@
""
]
},
"rule-doc-extended": {
"Rule Doc Extended": {
"prefix": "rule",
"description": "Extended rule documentation",
"body": [

Просмотреть файл

@ -0,0 +1,50 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as path from 'path';
import * as fse from 'fs-extra';
import { Position, TextDocument, Uri, window, workspace } from 'vscode';
import { logger } from '../logger';
import { getDocumentationPath, readDocumentationSnippet } from '../utils';
import { configuration } from '../configuration';
const validNameExpression =
/[^<>:/\\|?*"'`+@._\-\x00-\x1F][^<>:/\\|?*"'`+@\x00-\x1F]{1,126}[^<>:/\\|?*"'`+@._\-\x00-\x1F]+/g;
export async function createOrEditDocumentation(name: string | undefined): Promise<void> {
if (name === '' || name === undefined) {
name = await window.showInputBox({
prompt: 'Enter the name of the rule to create documentation for.',
validateInput: (value: string) => {
return validNameExpression.test(value) ? undefined : 'Must be a valid rule name.';
},
});
}
if (name === '' || name === undefined) return;
let uri = await getDocumentationPath(name);
if (uri) {
let parent = Uri.file(path.dirname(uri.fsPath));
logger.verbose(`Using documentation path ${uri.fsPath}`);
let exists = await fse.pathExists(uri.fsPath);
if (!exists) {
await fse.ensureDir(parent.fsPath);
await fse.writeFile(uri.fsPath, '', { encoding: 'utf-8' });
}
const document: TextDocument = await workspace.openTextDocument(uri);
const editor = await window.showTextDocument(document);
// Populate new documentation with a snippet
const snippetConfig = configuration.get().documentationSnippet;
const snippetPathConfig = configuration.get().documentationCustomSnippetPath;
if (!exists && snippetConfig !== '') {
let snippet = await readDocumentationSnippet(snippetPathConfig, snippetConfig);
if (snippet) {
editor.insertSnippet(snippet, new Position(0, 0));
}
}
}
}

Просмотреть файл

@ -3,21 +3,26 @@
'use strict';
import { ConfigurationChangeEvent, ExtensionContext, workspace } from 'vscode';
import { ConfigurationChangeEvent, ExtensionContext, env, workspace } from 'vscode';
import { configurationItemPrefix } from './consts';
/**
* The output of analysis tasks.
*/
export enum OutputAs {
Detail = 0,
Summary = 1,
Detail = 'Detail',
Summary = 'Summary',
}
/**
* PSRule extension settings.
*/
export interface ISetting {
codeLensRuleDocumentationLinks: boolean;
documentationCustomSnippetPath: string | undefined;
documentationSnippet: string;
documentationPath: string | undefined;
documentationLocalePath: string;
executionNotProcessedWarning: boolean;
experimentalEnabled: boolean;
outputAs: OutputAs;
@ -29,6 +34,11 @@ export interface ISetting {
* Default configuration for PSRule extension settings.
*/
const globalDefaults: ISetting = {
codeLensRuleDocumentationLinks: true,
documentationCustomSnippetPath: undefined,
documentationSnippet: 'Rule Doc',
documentationPath: undefined,
documentationLocalePath: env.language,
executionNotProcessedWarning: false,
experimentalEnabled: false,
outputAs: OutputAs.Summary,
@ -50,7 +60,7 @@ export class ConfigurationManager {
constructor(setting?: ISetting) {
this.default = setting ?? globalDefaults;
this.current = this.default;
this.current = { ...this.default };
this.loadSettings();
}
@ -82,23 +92,49 @@ export class ConfigurationManager {
private loadSettings(): void {
const config = workspace.getConfiguration(configurationItemPrefix);
// Experimental
let experimental = (this.current.experimentalEnabled = config.get<boolean>(
'experimental.enabled',
this.default.experimentalEnabled
));
// Read settings
this.current.executionNotProcessedWarning =
config.get<boolean>('execution.notProcessedWarning') ??
this.default.executionNotProcessedWarning;
this.current.documentationCustomSnippetPath =
config.get<string>('documentation.customSnippetPath') ??
this.default.documentationCustomSnippetPath;
this.current.experimentalEnabled =
config.get<boolean>('experimental.enabled') ?? this.default.experimentalEnabled;
this.current.documentationSnippet =
config.get<string>('documentation.snippet') ?? this.default.documentationSnippet;
this.current.outputAs = config.get<OutputAs>('output.as') ?? this.default.outputAs;
this.current.documentationPath =
config.get<string>('documentation.path') ?? this.default.documentationPath;
this.current.notificationsShowChannelUpgrade =
config.get<boolean>('notifications.showChannelUpgrade') ??
this.default.notificationsShowChannelUpgrade;
this.current.documentationLocalePath =
config.get<string>('documentation.localePath') ?? this.default.documentationLocalePath;
this.current.notificationsShowPowerShellExtension =
config.get<boolean>('notifications.showPowerShellExtension') ??
this.default.notificationsShowPowerShellExtension;
this.current.codeLensRuleDocumentationLinks = !experimental
? false
: config.get<boolean>(
'codeLens.ruleDocumentationLinks',
this.default.codeLensRuleDocumentationLinks
);
this.current.executionNotProcessedWarning = config.get<boolean>(
'execution.notProcessedWarning',
this.default.executionNotProcessedWarning
);
this.current.outputAs = config.get<OutputAs>('output.as', this.default.outputAs);
this.current.notificationsShowChannelUpgrade = config.get<boolean>(
'notifications.showChannelUpgrade',
this.default.notificationsShowChannelUpgrade
);
this.current.notificationsShowPowerShellExtension = config.get<boolean>(
'notifications.showPowerShellExtension',
this.default.notificationsShowPowerShellExtension
);
// Clear dirty settings flag
this.pendingLoad = false;

137
src/docLens.ts Normal file
Просмотреть файл

@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
'use strict';
import * as fse from 'fs-extra';
import {
CancellationToken,
CodeLens,
CodeLensProvider,
Command,
Disposable,
Event,
EventEmitter,
ExtensionContext,
Position,
Range,
TextDocument,
commands,
languages,
workspace,
} from 'vscode';
import { DocumentSelector } from 'vscode-languageclient';
import { createOrEditDocumentation } from './commands/createOrEditDocumentation';
import { configuration } from './configuration';
import { ILogger } from './logger';
import { getDocumentationPath } from './utils';
interface IContext {
readonly logger: ILogger;
readonly extensionContext: ExtensionContext;
}
export class DocumentationLensProvider implements CodeLensProvider, Disposable {
// Fields
private readonly context: IContext;
private codeLenses: CodeLens[] = [];
private regexPowerShell: RegExp;
private regexYaml: RegExp;
private regexJson: RegExp;
private _registration: Disposable | undefined;
private _onDidChangeCodeLenses: EventEmitter<void> = new EventEmitter<void>();
public readonly onDidChangeCodeLenses: Event<void> = this._onDidChangeCodeLenses.event;
constructor(logger: ILogger, extensionContext: ExtensionContext) {
this.context = { logger, extensionContext };
this.regexPowerShell =
/(?<rule>Rule )(?:-Name\s)?(?<name>['"]?[^<>:/\\|?*"'`+@._\-\x00-\x1F][^<>:/\\|?*"'`+@\x00-\x1F]{1,126}[^<>:/\\|?*"'`+@._\-\x00-\x1F]+['"]?)(?:.+)/gi;
this.regexYaml =
/(?<version>apiVersion: github\.com\/microsoft\/PSRule\/v1)(?:\s+kind: Rule\s+metadata:\s+)(?: name: ?)(?<name>['"]?[^<>:/\\|?*"'`+@._\-\x00-\x1F][^<>:/\\|?*"'`+@\x00-\x1F]{1,126}[^<>:/\\|?*"'`+@._\-\x00-\x1F]+['"]?)/gi;
this.regexJson =
/("apiVersion":\s*"github\.com\/microsoft\/PSRule\/v1",)(?:\s*"kind":\s*"Rule",\s*"metadata":\s*{\s*)(?:"name":\s)(?<name>"[^<>:/\\|?*"'`+@._\-\x00-\x1F][^<>:/\\|?*"'`+@\x00-\x1F]{1,126}[^<>:/\\|?*"'`+@._\-\x00-\x1F]+")/gi;
workspace.onDidChangeConfiguration((_) => {
this._onDidChangeCodeLenses.fire();
});
}
dispose() {
this._registration?.dispose();
}
public register(): void {
let filter: DocumentSelector = [
{ language: 'yaml', pattern: '**/*.Rule.yaml' },
{ language: 'json', pattern: '**/*.Rule.json' },
{ language: 'jsonc', pattern: '**/*.Rule.jsonc' },
{ language: 'powershell', pattern: '**/*.Rule.ps1' },
];
this._registration = languages.registerCodeLensProvider(filter, this);
commands.registerCommand('PSRule.createOrEditDocumentation', (name: string) => {
createOrEditDocumentation(name);
});
}
public async provideCodeLenses(
document: TextDocument,
token: CancellationToken
): Promise<CodeLens[]> {
if (configuration.get().codeLensRuleDocumentationLinks) {
const regex = this.getLanguageExpression(document.languageId);
if (regex === undefined) return [];
this.codeLenses = [];
const text = document.getText();
let matches;
while ((matches = regex.exec(text)) !== null) {
let name =
matches.groups !== undefined ? matches.groups['name'].replace(/\'/g, '') : '';
const line = document.lineAt(document.positionAt(matches.index).line);
const indexOf = line.text.indexOf(matches[1]);
const position = new Position(line.lineNumber, indexOf);
const range = document.getWordRangeAtPosition(position);
if (range && name) {
this.codeLenses.push(await this.createCodeLens(range, name));
}
}
return this.codeLenses;
}
return [];
}
private getLanguageExpression(language: string): RegExp | undefined {
if (language === 'powershell') return this.regexPowerShell;
if (language === 'yaml') return this.regexYaml;
if (language === 'json' || language === 'jsonc') return this.regexJson;
return undefined;
}
/**
* Create a rule code lens.
* @param range The range in the document the code lens applies to.
* @param name The name of the rule.
* @returns A code lens object.
*/
private async createCodeLens(range: Range, name: string): Promise<CodeLens> {
let uri = await getDocumentationPath(name);
let exists = uri !== undefined && (await fse.pathExists(uri.fsPath));
let title = exists ? 'Open documentation' : 'Create documentation';
let tooltip = exists ? 'Open documentation for rule' : 'Create documentation for rule';
return new CodeLens(range, {
title: title,
tooltip: tooltip,
command: 'PSRule.createOrEditDocumentation',
arguments: [name],
});
}
public async resolveCodeLens(
codeLens: CodeLens,
token: CancellationToken
): Promise<CodeLens | undefined> {
return undefined;
}
}

Просмотреть файл

@ -9,13 +9,16 @@ import { logger } from './logger';
import { PSRuleTaskProvider } from './tasks';
import { ConfigurationManager } from './configuration';
import { pwsh } from './powershell';
import { DocumentationLensProvider } from './docLens';
export let taskManager: PSRuleTaskProvider | undefined;
export let docLensProvider: DocumentationLensProvider | undefined;
export interface ExtensionInfo {
id: string;
version: string;
channel: string;
path: string;
}
export class ExtensionManager implements vscode.Disposable {
@ -52,6 +55,9 @@ export class ExtensionManager implements vscode.Disposable {
}
public dispose(): void {
if (docLensProvider) {
docLensProvider.dispose();
}
if (taskManager) {
taskManager.dispose();
}
@ -77,6 +83,11 @@ export class ExtensionManager implements vscode.Disposable {
private switchMode(): void {
ConfigurationManager.configure(this._context);
if (!docLensProvider) {
docLensProvider = new DocumentationLensProvider(logger, this._context);
docLensProvider.register();
}
if (this.isTrusted) {
pwsh.configure(this._info);
}
@ -160,6 +171,7 @@ export class ExtensionManager implements vscode.Disposable {
id: extensionId,
version: extensionVersion,
channel: extensionChannel,
path: context.extensionPath,
};
return result;
}

Просмотреть файл

@ -299,7 +299,7 @@ export class PSRuleTaskProvider implements vscode.TaskProvider {
shellArgs: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command'],
env: {
PSRULE_OUTPUT_STYLE: 'VisualStudioCode',
PSRULE_OUTPUT_AS: outputAs.toString(),
PSRULE_OUTPUT_AS: outputAs,
PSRULE_OUTPUT_CULTURE: vscode.env.language,
PSRULE_OUTPUT_BANNER: 'Minimal',
PSRULE_EXECUTION_NOTPROCESSEDWARNING: executionNotProcessedWarning

Просмотреть файл

@ -1,15 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as assert from 'assert/strict';
import { configuration, OutputAs } from '../../configuration';
import * as assert from 'assert';
import { configuration, ConfigurationManager, ISetting, OutputAs } from '../../configuration';
suite('ConfigurationManager tests', () => {
test('Defaults', () => {
assert.strictEqual(configuration.get().executionNotProcessedWarning, false);
assert.strictEqual(configuration.get().experimentalEnabled, false);
assert.strictEqual<OutputAs>(configuration.get().outputAs, OutputAs.Summary);
assert.strictEqual(configuration.get().notificationsShowChannelUpgrade, true);
assert.strictEqual(configuration.get().notificationsShowPowerShellExtension, true);
const config = new ConfigurationManager(undefined);
assert.equal(config.get().codeLensRuleDocumentationLinks, false);
assert.equal(config.get().documentationCustomSnippetPath, undefined);
assert.equal(config.get().documentationLocalePath, 'en');
assert.equal(config.get().documentationPath, undefined);
assert.equal(config.get().documentationSnippet, 'Rule Doc');
assert.equal(config.get().executionNotProcessedWarning, false);
assert.equal(config.get().experimentalEnabled, false);
assert.equal(config.get().outputAs, OutputAs.Summary);
assert.equal(config.get().notificationsShowChannelUpgrade, true);
assert.equal(config.get().notificationsShowPowerShellExtension, true);
});
});

71
src/utils.ts Normal file
Просмотреть файл

@ -0,0 +1,71 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
import { SnippetString, Uri, WorkspaceFolder, window, workspace } from 'vscode';
import { configuration } from './configuration';
import { ext } from './extension';
/**
* Calculates the file path of rule documentation for a specific rule based on settings.
* @param name The name of the rule.
* @returns The path where the rule markdown documentation should be created/ edited from.
*/
export async function getDocumentationPath(name: string): Promise<Uri | undefined> {
const workspaceRoot = getActiveOrFirstWorkspace()?.uri;
const docConfigPath = configuration.get().documentationPath;
let docRootPath =
docConfigPath && workspaceRoot ? path.join(workspaceRoot.fsPath, docConfigPath) : undefined;
let lang = configuration.get().documentationLocalePath;
if (!docRootPath && window.activeTextEditor?.document.uri) {
docRootPath = path.dirname(window.activeTextEditor.document.uri.path);
}
if (docRootPath) {
let uri = Uri.file(path.join(docRootPath, lang, `${name}.md`));
return uri;
}
return undefined;
}
/**
* Get a snippet from disk.
* @param file The path to a file containing a snippet. When not set, the default extension snippets will be used.
* @param name The name of the snippet to use. When name is an empty string no snippet is returned.
* @returns A possible matching snippet string.
*/
export async function readDocumentationSnippet(
file: string | undefined,
name: string
): Promise<SnippetString | undefined> {
if (name === '') return undefined;
// Try custom snippet file
const workspaceRoot = getActiveOrFirstWorkspace()?.uri;
let snippetFile = file && workspaceRoot ? path.join(workspaceRoot.fsPath, file) : undefined;
// Try built-in snippet file
if (!snippetFile) {
const info = await ext.info;
snippetFile = info ? path.join(info.path, 'snippets/markdown.json') : undefined;
}
if (snippetFile && (await fse.pathExists(snippetFile))) {
let json = await fse.readJson(snippetFile, { encoding: 'utf-8' });
if (json) {
let body: string[] = json[name].body;
return new SnippetString(body.join(os.EOL));
}
}
}
function getActiveOrFirstWorkspace(): WorkspaceFolder | undefined {
if (window.activeTextEditor) {
return workspace.getWorkspaceFolder(window.activeTextEditor.document.uri);
}
return workspace.workspaceFolders && workspace.workspaceFolders.length > 0
? workspace.workspaceFolders[0]
: undefined;
}