Skip to content

Commit

Permalink
feat: suggest refactor to enable/disable templates
Browse files Browse the repository at this point in the history
fixes: #4
  • Loading branch information
AlCalzone committed Oct 9, 2023
1 parent e8c4e7a commit ef92efd
Show file tree
Hide file tree
Showing 6 changed files with 228 additions and 1 deletion.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
-->

# Changelog
## __WORK IN PROGRESS__
* Suggest refactors when a matching enable/disable template is found

## 0.0.13 (2023-08-29)
* Opening the preview is now done manually

Expand Down
28 changes: 28 additions & 0 deletions src/astUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,31 @@ export function findSurroundingParamDefinition(
node = node.parent;
}
}

export function getPropertyDefinitionFromObject(
node: ObjectASTNode,
propertyName: string,
): PropertyASTNode | undefined {
return node.properties.find((p) => p.keyNode.value === propertyName);
}

export function getOptionsFromParamDefinition(
node: ObjectASTNode,
): { label: string; value: number }[] | undefined {
const optionsArray = node.properties.find(
(p) => p.keyNode.value === "options",
);
if (optionsArray?.valueNode?.type !== "array") return;
const options = optionsArray.valueNode.items
.filter((i): i is ObjectASTNode => i.type === "object")
.map((obj) => ({
label: getPropertyDefinitionFromObject(obj, "label")?.valueNode
?.value,
value: getPropertyDefinitionFromObject(obj, "value")?.valueNode
?.value,
}))
.filter((o): o is { label: string; value: number } => {
return typeof o.label === "string" && typeof o.value === "number";
});
return options;
}
193 changes: 193 additions & 0 deletions src/codeActions/refactorToBaseEnableDisableCodeActionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import * as vscode from "vscode";

import {
findSurroundingParamDefinition,
getOptionsFromParamDefinition,
getPropertyDefinitionFromObject,
getPropertyNameFromNode,
getPropertyValueFromNode,
nodeIsPropertyNameOrValue,
rangeFromNode,
} from "../astUtils";
import { My } from "../my";
import {
getConfigFileDocumentSelector,
masterTemplateImportPath,
resolveTemplate,
} from "../shared";

const possibleTemplates = [
"base_enable_disable",
"base_enable_disable_255",
"base_enable_disable_inverted",
"base_enable_disable_255_inverted",
];

export function register(my: My): vscode.Disposable {
const { workspace } = my;
return vscode.languages.registerCodeActionsProvider(
[
getConfigFileDocumentSelector(workspace),
// ...getTemplateDocumentSelector(workspace),
],
{
async provideCodeActions(document, range, _context, _token) {
const configDoc = my.configDocument;
if (!configDoc) return;

const node = configDoc.json.getNodeFromOffset(
document.offsetAt(range.start),
);
if (!nodeIsPropertyNameOrValue(node)) return;

// Offer the code action on the "#" property inside a param definition
if (getPropertyNameFromNode(node) !== "#") return;
const paramDefinition = findSurroundingParamDefinition(node);
if (!paramDefinition) return;

// We can offer the refactor, if ...
// ...there isn't already an import
if (
!!getPropertyDefinitionFromObject(
paramDefinition,
"$import",
)
) {
return;
}
// ...there are exactly 2 options
const options = getOptionsFromParamDefinition(paramDefinition);
if (options?.length !== 2) return;
// ... at least the label or the options contain "enable" or "disable"
const labelNode = getPropertyDefinitionFromObject(
paramDefinition,
"label",
);
const label = labelNode?.valueNode?.value;
if (typeof label !== "string") return;
const descriptionNode = getPropertyDefinitionFromObject(
paramDefinition,
"description",
);
const description = descriptionNode?.valueNode?.value;

const isCalledEnableOrDisable =
/(enable|disable)/.test(label.toLowerCase()) ||
(typeof description === "string" &&
/(enable|disable)/.test(description.toLowerCase())) ||
options.some((o) =>
/(enable|disable)/.test(o.label.toLowerCase()),
);
if (!isCalledEnableOrDisable) return;

const valueSize = getPropertyDefinitionFromObject(
paramDefinition,
"valueSize",
)?.valueNode?.value;
const defaultValue = getPropertyDefinitionFromObject(
paramDefinition,
"defaultValue",
)?.valueNode?.value;

// Try to find a matching template in the master template
const disableOptionValue = options.find((o) =>
o.label.toLowerCase().includes("disable"),
)?.value;
const enableOptionValue = options.find((o) =>
o.label.toLowerCase().includes("enable"),
)?.value;
if (
disableOptionValue == undefined ||
enableOptionValue == undefined
) {
return;
}

let matchingTemplateSpecifier: string | undefined;
let matchingTemplate: Record<string, any> | undefined;
for (const specifier of possibleTemplates) {
const template = await resolveTemplate(
my.workspace,
document.uri,
masterTemplateImportPath,
specifier,
);
if (!template) continue;
const templateEnableOptionValue = template.options?.find(
(o: any) => o.label.toLowerCase() === "enable",
)?.value;
const templateDisableOptionValue = template.options?.find(
(o: any) => o.label.toLowerCase() === "disable",
)?.value;
if (
templateEnableOptionValue === enableOptionValue &&
templateDisableOptionValue === disableOptionValue
) {
matchingTemplateSpecifier = specifier;
matchingTemplate = template;
break;
}
}
if (!matchingTemplateSpecifier || !matchingTemplate) return;

const templateDefaultValue = matchingTemplate.defaultValue;
const templateValueSize = matchingTemplate.valueSize;

// Remove occurences of enable/disable from the param label
const fixedLabel = label
.split(" ")
.filter(
(word) =>
!["disable", "enable", "enable/disable"].includes(
word.toLowerCase(),
),
)
.join(" ");

const refactored: Record<string, any> = {
"#": getPropertyValueFromNode(node),
$import: `${masterTemplateImportPath}#${matchingTemplateSpecifier}`,
label: fixedLabel,
description,
};
// Preserve overridden valueSize/default
if (templateValueSize !== valueSize) {
refactored.valueSize = valueSize;
}
if (templateDefaultValue !== defaultValue) {
refactored.defaultValue = defaultValue;
}

const refactorRange = rangeFromNode(document, paramDefinition);
const formatted = JSON.stringify(refactored, undefined, "\t")
.trim()
.split("\n")
.map((line, i, lines) => {
// All lines after the first need to be indented correctly
if (i === 0) {
return line;
}
const isLastLine = i === lines.length - 1;
return (
"\t".repeat(
refactorRange.start.character +
(isLastLine ? 0 : 1),
) + line.replace(/^\t/, "")
);
})
.join("\n");

const edit = new vscode.WorkspaceEdit();
edit.replace(document.uri, refactorRange, formatted);

return [
{
title: "Refactor to use Enable/Disable template",
kind: vscode.CodeActionKind.RefactorInline,
edit,
},
];
},
},
);
}
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as vscode from "vscode";
import { getLanguageService as getJsonLanguageService } from "vscode-json-languageservice";

import { register as registerInlineImportCodeAction } from "./codeActions/inlineImportCodeActionProvider";
import { register as refactorToBaseEnableDisableCodeAction } from "./codeActions/refactorToBaseEnableDisableCodeActionProvider";
import { register as registerCompletions } from "./importCompletionProvider";
import { register as registerGoToDefinition } from "./importGoToDefinitionProvider";
import { register as registerHover } from "./importHoverProvider";
Expand Down Expand Up @@ -38,6 +39,7 @@ export function activate(context: vscode.ExtensionContext): void {
registerHover(my),
registerGoToDefinition(my),
registerInlineImportCodeAction(my),
refactorToBaseEnableDisableCodeAction(my),
registerReferences(my),
registerDiagnosticsProvider(my),
...registerPreviewProvider(my),
Expand Down
2 changes: 1 addition & 1 deletion src/importCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { My } from "./my";
import {
formatTemplateDefinition,
getConfigFileDocumentSelector,
masterTemplateImportPath,
parseImportSpecifier,
resolveTemplateFile,
} from "./shared";
Expand All @@ -16,7 +17,6 @@ const makeTemplateImportSpecifier = (filePath: string) => `${filePath}#\${0}`;
const makeTemplateImport = (filePath: string) =>
`\\$import": "${makeTemplateImportSpecifier(filePath)}",`;

const masterTemplateImportPath = `~/templates/master_template.json`;
const masterTemplateImportSpecifier = makeTemplateImportSpecifier(
masterTemplateImportPath,
);
Expand Down
1 change: 1 addition & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { readJsonWithTemplate } from "./JsonTemplate";
import { My } from "./my";

export const configRoot = "packages/config/config/devices";
export const masterTemplateImportPath = `~/templates/master_template.json`;

export function getConfigFileDocumentSelector(
workspace: vscode.WorkspaceFolder,
Expand Down

0 comments on commit ef92efd

Please sign in to comment.