@ansible/ansible-language-server
Advanced tools
Comparing version 0.2.6 to 0.3.0
@@ -9,2 +9,16 @@ <!-- markdownlint-disable no-duplicate-heading --> | ||
## [0.3.0] - 2021-11-18 | ||
### Minor Changes | ||
* Added support for nested module options (suboptions) (#116) @tomaciazek | ||
* Adopted use of `creator-ee` execution environment (#132) @ssbarnea | ||
* Updated container cleanup logic for execution environment (#111) @ganeshrn | ||
### Bugfixes | ||
* Updated plugin doc cache validate logic for execution environment (#109) | ||
@ganeshrn | ||
* Fixed issue with container copy command (#110) @ganeshrn | ||
## [0.2.6] - 2021-10-29 | ||
@@ -14,3 +28,4 @@ | ||
* Fix autocompletion of the builtin modules with EE (#94) @ganeshrn | ||
* Fixed autocompletion to account for the builtin modules when used | ||
with EE (#94) @ganeshrn | ||
@@ -17,0 +32,0 @@ ## [0.2.5] - 2021-10-23 |
@@ -37,3 +37,3 @@ import { YAMLError } from 'yaml/util'; | ||
versionAdded?: string; | ||
suboptions?: unknown; | ||
suboptions?: Map<string, IOption>; | ||
} |
@@ -117,75 +117,60 @@ "use strict"; | ||
} | ||
// Finally, check if we're looking for module options | ||
// In that case, the module name is a key of a map | ||
const parentKeyPath = new yaml_1.AncestryBuilder(path) | ||
.parentOfKey() | ||
.parent(types_1.YAMLMap) | ||
.getKeyPath(); | ||
if (parentKeyPath && (0, yaml_1.isTaskParam)(parentKeyPath)) { | ||
const parentKeyNode = parentKeyPath[parentKeyPath.length - 1]; | ||
if (parentKeyNode instanceof types_1.Scalar) { | ||
let module; | ||
if (parentKeyNode.value === 'args') { | ||
module = yield (0, yaml_1.findProvidedModule)(parentKeyPath, document, docsLibrary); | ||
// Finally, check if we're looking for module options or sub-options | ||
const options = yield (0, yaml_1.getPossibleOptionsForPath)(path, document, docsLibrary); | ||
if (options) { | ||
const optionMap = new yaml_1.AncestryBuilder(path) | ||
.parentOfKey() | ||
.get(); | ||
// find options that have been already provided by the user | ||
const providedOptions = new Set((0, yaml_1.getYamlMapKeys)(optionMap)); | ||
const remainingOptions = [...options.entries()].filter(([, specs]) => !providedOptions.has(specs.name)); | ||
const nodeRange = getNodeRange(node, document); | ||
return remainingOptions | ||
.map(([option, specs]) => { | ||
return { | ||
name: option, | ||
specs: specs, | ||
}; | ||
}) | ||
.map((option, index) => { | ||
// translate option documentation to CompletionItem | ||
const details = (0, docsFormatter_1.getDetails)(option.specs); | ||
let priority; | ||
if (isAlias(option)) { | ||
priority = priorityMap.aliasOption; | ||
} | ||
else if (option.specs.required) { | ||
priority = priorityMap.requiredOption; | ||
} | ||
else { | ||
[module] = yield docsLibrary.findModule(parentKeyNode.value, parentKeyPath, document.uri); | ||
priority = priorityMap.option; | ||
} | ||
if (module && module.documentation) { | ||
const moduleOptions = module.documentation.options; | ||
const optionMap = new yaml_1.AncestryBuilder(parentKeyPath).parent(types_1.Pair).get().value; | ||
// find options that have been already provided by the user | ||
const providedOptions = new Set((0, yaml_1.getYamlMapKeys)(optionMap)); | ||
const remainingOptions = [...moduleOptions.entries()].filter(([, specs]) => !providedOptions.has(specs.name)); | ||
const nodeRange = getNodeRange(node, document); | ||
return remainingOptions | ||
.map(([option, specs]) => { | ||
return { | ||
name: option, | ||
specs: specs, | ||
}; | ||
}) | ||
.map((option, index) => { | ||
// translate option documentation to CompletionItem | ||
const details = (0, docsFormatter_1.getDetails)(option.specs); | ||
let priority; | ||
if (isAlias(option)) { | ||
priority = priorityMap.aliasOption; | ||
} | ||
else if (option.specs.required) { | ||
priority = priorityMap.requiredOption; | ||
} | ||
else { | ||
priority = priorityMap.option; | ||
} | ||
const completionItem = { | ||
label: option.name, | ||
detail: details, | ||
// using index preserves order from the specification | ||
// except when overridden by the priority | ||
sortText: priority.toString() + index.toString().padStart(3), | ||
kind: isAlias(option) | ||
? vscode_languageserver_1.CompletionItemKind.Reference | ||
: vscode_languageserver_1.CompletionItemKind.Property, | ||
documentation: (0, docsFormatter_1.formatOption)(option.specs), | ||
insertText: atEndOfLine(document, position) | ||
? `${option.name}:` | ||
: undefined, | ||
}; | ||
const insertText = atEndOfLine(document, position) | ||
? `${option.name}:` | ||
: option.name; | ||
if (nodeRange) { | ||
completionItem.textEdit = { | ||
range: nodeRange, | ||
newText: insertText, | ||
}; | ||
} | ||
else { | ||
completionItem.insertText = insertText; | ||
} | ||
return completionItem; | ||
}); | ||
const completionItem = { | ||
label: option.name, | ||
detail: details, | ||
// using index preserves order from the specification | ||
// except when overridden by the priority | ||
sortText: priority.toString() + index.toString().padStart(3), | ||
kind: isAlias(option) | ||
? vscode_languageserver_1.CompletionItemKind.Reference | ||
: vscode_languageserver_1.CompletionItemKind.Property, | ||
documentation: (0, docsFormatter_1.formatOption)(option.specs), | ||
insertText: atEndOfLine(document, position) | ||
? `${option.name}:` | ||
: undefined, | ||
}; | ||
const insertText = atEndOfLine(document, position) | ||
? `${option.name}:` | ||
: option.name; | ||
if (nodeRange) { | ||
completionItem.textEdit = { | ||
range: nodeRange, | ||
newText: insertText, | ||
}; | ||
} | ||
} | ||
else { | ||
completionItem.insertText = insertText; | ||
} | ||
return completionItem; | ||
}); | ||
} | ||
@@ -192,0 +177,0 @@ } |
@@ -62,26 +62,10 @@ "use strict"; | ||
} | ||
// hovering over a module parameter | ||
// can either be directly under module or in 'args' | ||
const parentKeyPath = new yaml_1.AncestryBuilder(path) | ||
.parentOfKey() | ||
.parent(types_1.YAMLMap) | ||
.getKeyPath(); | ||
if (parentKeyPath && (0, yaml_1.isTaskParam)(parentKeyPath)) { | ||
const parentKeyNode = parentKeyPath[parentKeyPath.length - 1]; | ||
if (parentKeyNode instanceof types_1.Scalar) { | ||
let module; | ||
if (parentKeyNode.value === 'args') { | ||
module = yield (0, yaml_1.findProvidedModule)(parentKeyPath, document, docsLibrary); | ||
} | ||
else { | ||
[module] = yield docsLibrary.findModule(parentKeyNode.value, parentKeyPath, document.uri); | ||
} | ||
if (module && module.documentation) { | ||
const option = module.documentation.options.get(node.value); | ||
if (option) { | ||
return { | ||
contents: (0, docsFormatter_1.formatOption)(option, true), | ||
}; | ||
} | ||
} | ||
// hovering over a module option or sub-option | ||
const options = yield (0, yaml_1.getPossibleOptionsForPath)(path, document, docsLibrary); | ||
if (options) { | ||
const option = options.get(node.value); | ||
if (option) { | ||
return { | ||
contents: (0, docsFormatter_1.formatOption)(option, true), | ||
}; | ||
} | ||
@@ -88,0 +72,0 @@ } |
@@ -40,2 +40,3 @@ "use strict"; | ||
function markSemanticTokens(path, builder, document, docsLibrary) { | ||
var _a, _b; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
@@ -72,3 +73,3 @@ const node = path[path.length - 1]; | ||
// highlight module parameters | ||
markModuleParameters(pair.value, module, builder, document); | ||
markModuleParameters(pair.value, (_a = module.documentation) === null || _a === void 0 ? void 0 : _a.options, builder, document); | ||
} | ||
@@ -84,3 +85,3 @@ } | ||
// highlight module parameters | ||
markModuleParameters(pair.value, module, builder, document); | ||
markModuleParameters(pair.value, (_b = module.documentation) === null || _b === void 0 ? void 0 : _b.options, builder, document); | ||
} | ||
@@ -118,15 +119,34 @@ } | ||
} | ||
function markModuleParameters(moduleParamMap, module, builder, document) { | ||
var _a; | ||
function markModuleParameters(moduleParamMap, options, builder, document) { | ||
for (const moduleParamPair of moduleParamMap.items) { | ||
if (moduleParamPair.key instanceof types_1.Scalar) { | ||
const option = (_a = module.documentation) === null || _a === void 0 ? void 0 : _a.options.get(moduleParamPair.key.value); | ||
const option = options === null || options === void 0 ? void 0 : options.get(moduleParamPair.key.value); | ||
if (option) { | ||
markNode(moduleParamPair.key, vscode_languageserver_1.SemanticTokenTypes.method, [], builder, document); | ||
if (option.type === 'dict' && | ||
moduleParamPair.value instanceof types_1.YAMLMap) { | ||
// highlight sub-parameters | ||
markModuleParameters(moduleParamPair.value, option.suboptions, builder, document); | ||
} | ||
else if (option.type === 'list' && | ||
moduleParamPair.value instanceof types_1.YAMLSeq) { | ||
// highlight list of sub-parameters | ||
for (const item of moduleParamPair.value.items) { | ||
if (item instanceof types_1.YAMLMap) { | ||
markModuleParameters(item, option.suboptions, builder, document); | ||
} | ||
else { | ||
markAllNestedKeysAsOrdinary(item, builder, document); | ||
} | ||
} | ||
} | ||
else { | ||
markAllNestedKeysAsOrdinary(moduleParamPair.value, builder, document); | ||
} | ||
} | ||
else { | ||
markOrdinaryKey(moduleParamPair.key, builder, document); | ||
markAllNestedKeysAsOrdinary(moduleParamPair.value, builder, document); | ||
} | ||
} | ||
if (moduleParamPair.value instanceof types_1.Node) { | ||
else if (moduleParamPair.value instanceof types_1.Node) { | ||
markAllNestedKeysAsOrdinary(moduleParamPair.value, builder, document); | ||
@@ -133,0 +153,0 @@ } |
@@ -7,2 +7,3 @@ import { Connection } from 'vscode-languageserver'; | ||
private useProgressTracker; | ||
private successFileMarker; | ||
private _container_engine; | ||
@@ -16,2 +17,3 @@ private _container_image; | ||
cleanUpContainer(containerName: string): void; | ||
doesContainerNameExist(containerName: string): boolean; | ||
private isPluginInPath; | ||
@@ -21,2 +23,3 @@ private runContainer; | ||
private updateCachePaths; | ||
private isPluginDocCacheValid; | ||
} |
@@ -24,2 +24,3 @@ "use strict"; | ||
this.useProgressTracker = false; | ||
this.successFileMarker = 'SUCCESS'; | ||
this.connection = connection; | ||
@@ -107,3 +108,3 @@ this.context = context; | ||
} | ||
if (fs.existsSync(hostCacheBasePath)) { | ||
if (this.isPluginDocCacheValid(hostCacheBasePath)) { | ||
ansibleConfig.collections_paths = this.updateCachePaths(ansibleConfig.collections_paths, hostCacheBasePath); | ||
@@ -138,2 +139,4 @@ ansibleConfig.module_locations = this.updateCachePaths(ansibleConfig.module_locations, hostCacheBasePath); | ||
} | ||
// plugin cache successfully created | ||
fs.closeSync(fs.openSync(path.join(hostCacheBasePath, this.successFileMarker), 'w')); | ||
} | ||
@@ -182,6 +185,11 @@ catch (error) { | ||
cleanUpContainer(containerName) { | ||
[ | ||
const cleanUpCommands = [ | ||
`${this._container_engine} stop ${containerName}`, | ||
`${this._container_engine} rm ${containerName}`, | ||
].forEach((command) => { | ||
]; | ||
if (!this.doesContainerNameExist(containerName)) { | ||
console.log(`clean up container not required as container with name ${containerName} does not exist`); | ||
return; | ||
} | ||
for (const command of cleanUpCommands) { | ||
try { | ||
@@ -194,5 +202,17 @@ child_process.execSync(command, { | ||
// container already stopped and/or removed | ||
break; | ||
} | ||
}); | ||
} | ||
} | ||
doesContainerNameExist(containerName) { | ||
let containerNameExist = false; | ||
try { | ||
child_process.execSync(`${this._container_engine} inspect ${containerName}`); | ||
containerNameExist = true; | ||
} | ||
catch (error) { | ||
containerNameExist = false; | ||
} | ||
return containerNameExist; | ||
} | ||
isPluginInPath(containerName, searchPath, pluginFolderPath) { | ||
@@ -248,3 +268,3 @@ const command = `${this._container_engine} exec ${containerName} find ${searchPath} -path '${pluginFolderPath}'`; | ||
fs.mkdirSync(destPath, { recursive: true }); | ||
const copyCommand = `docker cp ${containerName}:${srcPath} ${destPathFolder}`; | ||
const copyCommand = `${this._container_engine} cp ${containerName}:${srcPath} ${destPathFolder}`; | ||
this.connection.console.log(`Copying plugins from container to local cache path ${copyCommand}`); | ||
@@ -270,4 +290,8 @@ (0, misc_1.asyncExec)(copyCommand, { | ||
} | ||
isPluginDocCacheValid(hostCacheBasePath) { | ||
const markerFilePath = path.join(hostCacheBasePath, this.successFileMarker); | ||
return true ? fs.existsSync(markerFilePath) : false; | ||
} | ||
} | ||
exports.ExecutionEnvironment = ExecutionEnvironment; | ||
//# sourceMappingURL=executionEnvironment.js.map |
@@ -26,3 +26,3 @@ "use strict"; | ||
enabled: false, | ||
image: 'quay.io/ansible/ansible-navigator-demo-ee:0.6.0', | ||
image: 'quay.io/ansible/creator-ee:latest', | ||
pullPolicy: 'missing' | ||
@@ -29,0 +29,0 @@ } |
@@ -84,3 +84,3 @@ "use strict"; | ||
default: rawOption.default, | ||
suboptions: rawOption.suboptions, | ||
suboptions: processRawOptions(rawOption.suboptions), | ||
}; | ||
@@ -87,0 +87,0 @@ if (isIDescription(rawOption.description)) |
@@ -8,7 +8,7 @@ /// <reference types="node" /> | ||
export declare function toLspRange(range: [number, number], textDocument: TextDocument): Range; | ||
export declare function hasOwnProperty<X extends unknown, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown>; | ||
export declare function hasOwnProperty<X, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown>; | ||
/** | ||
* Checks whether `obj` is a non-null object. | ||
*/ | ||
export declare function isObject<X extends unknown>(obj: X): obj is X & Record<PropertyKey, unknown>; | ||
export declare function isObject<X>(obj: X): obj is X & Record<PropertyKey, unknown>; | ||
export declare function insert(str: string, index: number, val: string): string; | ||
@@ -15,0 +15,0 @@ /** |
import { Position, TextDocument } from 'vscode-languageserver-textdocument'; | ||
import { Document, Options } from 'yaml'; | ||
import { Node, Pair, YAMLMap } from 'yaml/types'; | ||
import { IModuleMetadata } from '../interfaces/module'; | ||
import { IModuleMetadata, IOption } from '../interfaces/module'; | ||
import { DocsLibrary } from '../services/docsLibrary'; | ||
@@ -83,2 +83,15 @@ /** | ||
/** | ||
* If the path points at a parameter or sub-parameter provided for a module, it | ||
* will return the list of all possible options or sub-options at that | ||
* level/indentation. | ||
*/ | ||
export declare function getPossibleOptionsForPath(path: Node[], document: TextDocument, docsLibrary: DocsLibrary): Promise<Map<string, IOption> | null>; | ||
/** | ||
* For a given path, it searches up that path until a path to the task parameter | ||
* (typically a module name) is found. The trace of keys with indication whether | ||
* the values hold a 'list' or a 'dict' is preserved along the way and returned | ||
* alongside. | ||
*/ | ||
export declare function getTaskParamPathWithTrace(path: Node[]): [Node[], [string, 'list' | 'dict'][]]; | ||
/** | ||
* For a given Ansible task parameter path, find the module if it has been | ||
@@ -85,0 +98,0 @@ * provided for the task. |
@@ -12,3 +12,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.isPlaybook = exports.parseAllDocuments = exports.getOrigRange = exports.getYamlMapKeys = exports.findProvidedModule = exports.isRoleParam = exports.isBlockParam = exports.isPlayParam = exports.getDeclaredCollections = exports.isTaskParam = exports.tasksKey = exports.getPathAtOffset = exports.contains = exports.getPathAt = exports.AncestryBuilder = void 0; | ||
exports.isPlaybook = exports.parseAllDocuments = exports.getOrigRange = exports.getYamlMapKeys = exports.findProvidedModule = exports.getTaskParamPathWithTrace = exports.getPossibleOptionsForPath = exports.isRoleParam = exports.isBlockParam = exports.isPlayParam = exports.getDeclaredCollections = exports.isTaskParam = exports.tasksKey = exports.getPathAtOffset = exports.contains = exports.getPathAt = exports.AncestryBuilder = void 0; | ||
const _ = require("lodash"); | ||
@@ -318,2 +318,88 @@ const yaml_1 = require("yaml"); | ||
/** | ||
* If the path points at a parameter or sub-parameter provided for a module, it | ||
* will return the list of all possible options or sub-options at that | ||
* level/indentation. | ||
*/ | ||
function getPossibleOptionsForPath(path, document, docsLibrary) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const [taskParamPath, suboptionTrace] = getTaskParamPathWithTrace(path); | ||
if (!taskParamPath) | ||
return null; | ||
const optionTraceElement = suboptionTrace.pop(); | ||
if (!optionTraceElement || optionTraceElement[1] !== 'dict') { | ||
// that element must always be a `dict` | ||
// (unlike for sub-options, which can also be a 'list') | ||
return null; | ||
} | ||
// The module name is a key of the task parameters map | ||
const taskParamNode = taskParamPath[taskParamPath.length - 1]; | ||
if (!(taskParamNode instanceof types_1.Scalar)) | ||
return null; | ||
let module; | ||
// Module options can either be directly under module or in 'args' | ||
if (taskParamNode.value === 'args') { | ||
module = yield findProvidedModule(taskParamPath, document, docsLibrary); | ||
} | ||
else { | ||
[module] = yield docsLibrary.findModule(taskParamNode.value, taskParamPath, document.uri); | ||
} | ||
if (!module || !module.documentation) | ||
return null; | ||
let options = module.documentation.options; | ||
suboptionTrace.reverse(); // now going down the path | ||
for (const [optionName, optionType] of suboptionTrace) { | ||
const option = options.get(optionName); | ||
if (optionName && (option === null || option === void 0 ? void 0 : option.type) === optionType && option.suboptions) { | ||
options = option.suboptions; | ||
} | ||
else { | ||
return null; // suboption structure mismatch | ||
} | ||
} | ||
return options; | ||
}); | ||
} | ||
exports.getPossibleOptionsForPath = getPossibleOptionsForPath; | ||
/** | ||
* For a given path, it searches up that path until a path to the task parameter | ||
* (typically a module name) is found. The trace of keys with indication whether | ||
* the values hold a 'list' or a 'dict' is preserved along the way and returned | ||
* alongside. | ||
*/ | ||
function getTaskParamPathWithTrace(path) { | ||
const trace = []; | ||
while (!isTaskParam(path)) { | ||
let parentKeyPath = new AncestryBuilder(path) | ||
.parentOfKey() | ||
.parent(types_1.YAMLMap) | ||
.getKeyPath(); | ||
if (parentKeyPath) { | ||
const parentKeyNode = parentKeyPath[parentKeyPath.length - 1]; | ||
if (parentKeyNode instanceof types_1.Scalar && | ||
typeof parentKeyNode.value === 'string') { | ||
trace.push([parentKeyNode.value, 'dict']); | ||
path = parentKeyPath; | ||
continue; | ||
} | ||
} | ||
parentKeyPath = new AncestryBuilder(path) | ||
.parentOfKey() | ||
.parent(types_1.YAMLSeq) | ||
.parent(types_1.YAMLMap) | ||
.getKeyPath(); | ||
if (parentKeyPath) { | ||
const parentKeyNode = parentKeyPath[parentKeyPath.length - 1]; | ||
if (parentKeyNode instanceof types_1.Scalar && | ||
typeof parentKeyNode.value === 'string') { | ||
trace.push([parentKeyNode.value, 'list']); | ||
path = parentKeyPath; | ||
continue; | ||
} | ||
} | ||
return [[], []]; // return empty if no structural match found | ||
} | ||
return [path, trace]; | ||
} | ||
exports.getTaskParamPathWithTrace = getTaskParamPathWithTrace; | ||
/** | ||
* For a given Ansible task parameter path, find the module if it has been | ||
@@ -320,0 +406,0 @@ * provided for the task. |
@@ -7,3 +7,3 @@ { | ||
"license": "MIT", | ||
"version": "0.2.6", | ||
"version": "0.3.0", | ||
"contributors": [ | ||
@@ -10,0 +10,0 @@ { |
@@ -153,3 +153,2 @@ # Ansible Language Server | ||
* The shorthand syntax for module options (key=value pairs) is not supported. | ||
* Nested module options are not supported yet. | ||
* Only Jinja *expressions* inside Ansible YAML files are supported. In order to | ||
@@ -156,0 +155,0 @@ have syntax highlighting of Jinja template files, you'll need to install other |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
531052
5029
161