From f9fb14c53ce4f5996b6171b597afcbd5c02863c8 Mon Sep 17 00:00:00 2001 From: Gaviola Date: Fri, 19 Sep 2025 13:52:13 -0300 Subject: [PATCH 1/7] added refactors "changeFieldToArray" and "changeFieldToSingleValue" --- package.json | 34 +++- src/refactor/refactorDisposables.ts | 4 + src/refactor/refactorInterfaces.ts | 12 ++ src/refactor/tools/changeFieldToArray.ts | 160 +++++++++++++++++ .../tools/changeFieldToSingleValue.ts | 164 ++++++++++++++++++ 5 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 src/refactor/tools/changeFieldToArray.ts create mode 100644 src/refactor/tools/changeFieldToSingleValue.ts diff --git a/package.json b/package.json index edd3d33..b079fd7 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,14 @@ "command": "slingr-vscode-extension.changeFieldType", "title": "Change type" }, + { + "command": "slingr-vscode-extension.changeFieldToArray", + "title": "Change to array" + }, + { + "command": "slingr-vscode-extension.changeFieldToSingleValue", + "title": "Change to single value" + }, { "command": "slingr.runInfraUpdate", "title": "Run infrastructure update" @@ -196,10 +204,20 @@ "group": "0_modification@2" }, { - "command": "slingr-vscode-extension.deleteField", + "command": "slingr-vscode-extension.changeFieldToArray", "when": "view == slingrExplorer && viewItem == 'field'", "group": "0_modification@3" }, + { + "command": "slingr-vscode-extension.changeFieldToSingleValue", + "when": "view == slingrExplorer && viewItem == 'field'", + "group": "0_modification@4" + }, + { + "command": "slingr-vscode-extension.deleteField", + "when": "view == slingrExplorer && viewItem == 'field'", + "group": "0_modification@5" + }, { "command": "slingr-vscode-extension.newFolder", "when": "view == slingrExplorer && viewItem == 'folder'", @@ -309,6 +327,16 @@ "when": "view == slingrExplorer && viewItem == 'field'", "group": "1_modification@5" }, + { + "command": "slingr-vscode-extension.changeFieldToArray", + "when": "view == slingrExplorer && viewItem == 'field'", + "group": "1_modification@6" + }, + { + "command": "slingr-vscode-extension.changeFieldToSingleValue", + "when": "view == slingrExplorer && viewItem == 'field'", + "group": "1_modification@7" + }, { "command": "slingr-vscode-extension.deleteField", "when": "view == slingrExplorer && viewItem == 'field'", @@ -330,7 +358,7 @@ { "id": "slingrExplorer", "name": "Slingr Explorer", - "icon": "resources/slingr-icon.jpg" + "icon": "resources/slingr-icon.jpg" }, { "id": "slingrQuickInfo", @@ -363,4 +391,4 @@ "dependencies": { "ts-morph": "^26.0.0" } -} +} \ No newline at end of file diff --git a/src/refactor/refactorDisposables.ts b/src/refactor/refactorDisposables.ts index afc1f3e..5801211 100644 --- a/src/refactor/refactorDisposables.ts +++ b/src/refactor/refactorDisposables.ts @@ -15,6 +15,8 @@ import { PropertyMetadata } from '../cache/cache'; import { fieldTypeConfig } from '../utils/fieldTypes'; import { RenameDataSourceTool } from './tools/renameDataSource'; import { DeleteDataSourceTool } from './tools/deleteDataSource'; +import { ChangeFieldToArrayTool } from './tools/changeFieldToArray'; +import { ChangeFieldToSingleValueTool } from './tools/changeFieldToSingleValue'; /** * Returns an array of all available refactor tools for the application. @@ -38,6 +40,8 @@ export function getAllRefactorTools(): IRefactorTool[] { new RenameFieldTool(), new DeleteFieldTool(), new ChangeFieldTypeTool(), + new ChangeFieldToArrayTool(), + new ChangeFieldToSingleValueTool(), new AddDecoratorTool(), new RenameDataSourceTool(), new DeleteDataSourceTool(), diff --git a/src/refactor/refactorInterfaces.ts b/src/refactor/refactorInterfaces.ts index 1383504..5e13ce1 100644 --- a/src/refactor/refactorInterfaces.ts +++ b/src/refactor/refactorInterfaces.ts @@ -67,6 +67,16 @@ export interface DeleteDataSourcePayload extends BasePayload { urisToDelete: vscode.Uri[]; } +export interface ChangeFieldToArrayPayload extends BasePayload { + field: PropertyMetadata; + modelName: string; +} + +export interface ChangeFieldToSingleValuePayload extends BasePayload { + field: PropertyMetadata; + modelName: string; +} + // --- Discriminated Union for ChangeObject --- /** @@ -82,6 +92,8 @@ export type ChangePayloadMap = { 'ADD_DECORATOR': AddDecoratorPayload; 'RENAME_DATA_SOURCE': RenameDataSourcePayload; 'DELETE_DATA_SOURCE': DeleteDataSourcePayload; + 'CHANGE_FIELD_TO_ARRAY': ChangeFieldToArrayPayload; + 'CHANGE_FIELD_TO_SINGLE_VALUE': ChangeFieldToSingleValuePayload; }; /** diff --git a/src/refactor/tools/changeFieldToArray.ts b/src/refactor/tools/changeFieldToArray.ts new file mode 100644 index 0000000..0c36808 --- /dev/null +++ b/src/refactor/tools/changeFieldToArray.ts @@ -0,0 +1,160 @@ +import * as vscode from 'vscode'; +import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToArrayPayload } from '../refactorInterfaces'; +import { MetadataCache, PropertyMetadata } from '../../cache/cache'; +import { isModelFile, isField, areRangesEqual } from '../../utils/metadata'; + +/** + * Tool to change a field from a single value type to an array type. + * E.g., changes `tag: string` to `tags: string[]` and updates all references. + */ +export class ChangeFieldToArrayTool implements IRefactorTool { + getCommandId(): string { + return 'slingr-vscode-extension.changeFieldToArray'; + } + + getTitle(): string { + return 'Change to array'; + } + + getHandledChangeTypes(): string[] { + return ['CHANGE_FIELD_TO_ARRAY']; + } + + async canHandleManualTrigger(context: ManualRefactorContext): Promise { + if (!context.metadata) { + return false; + } + if (isModelFile(context.uri) && isField(context.metadata)) { + const field = context.metadata as PropertyMetadata; + return !field.type.endsWith('[]'); + } + return false; + } + + analyze(): ChangeObject[] { + return []; + } + + async initiateManualRefactor(context: ManualRefactorContext): Promise { + if (!context.metadata || !isField(context.metadata)) { + return undefined; + } + const field = context.metadata as PropertyMetadata; + + // Find the containing model's name for context + const fileMetadata = context.cache.getMetadataForFile(context.uri.fsPath); + let modelName = 'UnknownModel'; + if (fileMetadata) { + for (const [className, classData] of Object.entries(fileMetadata.classes)) { + if (classData.properties[field.name]) { + modelName = className; + break; + } + } + } + + const payload: ChangeFieldToArrayPayload = { + field: context.metadata as PropertyMetadata, + modelName, + isManual: true, + }; + + return { + type: 'CHANGE_FIELD_TO_ARRAY', + uri: context.uri, + description: `Change field '${payload.field.name}' to array.`, + payload, + }; + } + + async prepareEdit(change: ChangeObject, cache: MetadataCache): Promise { + if (change.type !== 'CHANGE_FIELD_TO_ARRAY') { + throw new Error(`ChangeFieldToArrayTool can only handle CHANGE_FIELD_TO_ARRAY changes, received: ${change.type}`); + } + + const { field } = change.payload; + const workspaceEdit = new vscode.WorkspaceEdit(); + const { declaration, references = [] } = field; + + // 1. Change the field's type to an array type + const document = await vscode.workspace.openTextDocument(declaration.uri); + const lineText = document.lineAt(declaration.range.start.line).text; + const typeRegex = new RegExp(`:\\s*${field.type}`); + const match = lineText.match(typeRegex); + + if (match && typeof match.index === 'number') { + const startPos = new vscode.Position(declaration.range.start.line, match.index); + const typeNodeText = match[0]; + const endPos = startPos.translate(0, typeNodeText.length); + const typeRange = new vscode.Range(startPos, endPos); + workspaceEdit.replace(declaration.uri, typeRange, `: ${field.type}[]`); + } + + // 2. Pluralize name if it's not already plural and update all references + if (!field.name.endsWith('s')) { + const newName = `${field.name}s`; + // Update the declaration + workspaceEdit.replace(declaration.uri, declaration.range, newName); + + // Update all other references + for (const ref of references) { + // Skip the declaration itself as we just handled it + if (ref.uri.fsPath === declaration.uri.fsPath && areRangesEqual(ref.range, declaration.range)) { + continue; + } + workspaceEdit.replace(ref.uri, ref.range, newName); + } + } + + return workspaceEdit; + } + + async executePrompt(change: ChangeObject): Promise { + if (change.type !== 'CHANGE_FIELD_TO_ARRAY') return; + + const { field, modelName } = change.payload; + const oldName = field.name; + const newName = oldName.endsWith('s') ? oldName : `${oldName}s`; + const oldType = field.type; + const newType = `${oldType}[]`; + const referenceLocations = field.references + .map(ref => `- \`${ref.uri.fsPath.split('/').pop()}\` at line ${ref.range.start.line + 1}`) + .join('\n'); + + const prompt = `## Field Refactoring - Code Review Required + +The field **\`${oldName}\`** in the model **\`${modelName}\`** has been refactored to be an array. + +**Changes Applied:** +- **Name Change:** \`${oldName}\` -> \`${newName}\` +- **Type Change:** \`${oldType}\` -> \`${newType}\` +- All direct references to the field name have been updated. + +**Problem:** The logic using this field might now be incorrect. For example, code that treated it as a single value (e.g., \`record.${newName} === 'value'\`) will now need to handle an array (e.g., \`record.${newName}.includes('value')\`). + +**Your Task:** Help me review and fix the code that uses this field. + +### Instructions: + +1. **Analyze the following locations** where the field was referenced. The logic in these places is likely broken. +2. **For each location, suggest the necessary code changes** to correctly handle the new array type. +3. **Provide clear before/after code snippets** and explain why the change is needed. + +### Reference Locations to Check: +${referenceLocations} + +### Common Patterns to Fix: +- **Direct comparisons:** Change \`===, !==\` to \`.includes()\` or loops. +- **Assignments:** Ensure an array is being assigned, not a single value. +- **UI Display:** Adjust how the field is rendered to show a list or tags. +- **Function arguments:** Update functions that expected a single value. + +Please start by analyzing the first reference location.`; + + try { + await vscode.commands.executeCommand('workbench.action.chat.open', prompt); + } catch (error) { + console.error('Failed to open chat with custom prompt:', error); + } + } +} \ No newline at end of file diff --git a/src/refactor/tools/changeFieldToSingleValue.ts b/src/refactor/tools/changeFieldToSingleValue.ts new file mode 100644 index 0000000..9c9c136 --- /dev/null +++ b/src/refactor/tools/changeFieldToSingleValue.ts @@ -0,0 +1,164 @@ +import * as vscode from 'vscode'; +import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToSingleValuePayload } from '../refactorInterfaces'; +import { MetadataCache, PropertyMetadata } from '../../cache/cache'; +import { isModelFile, isField, areRangesEqual } from '../../utils/metadata'; + +/** + * Tool to change a field from an array type to a single value type. + * E.g., changes `tags: string[]` to `tag: string` and updates all references. + */ +export class ChangeFieldToSingleValueTool implements IRefactorTool { + getCommandId(): string { + return 'slingr-vscode-extension.changeFieldToSingleValue'; + } + + getTitle(): string { + return 'Change to single value'; + } + + getHandledChangeTypes(): string[] { + return ['CHANGE_FIELD_TO_SINGLE_VALUE']; + } + + async canHandleManualTrigger(context: ManualRefactorContext): Promise { + if (!context.metadata) { + return false; + } + if (isModelFile(context.uri) && isField(context.metadata)) { + const field = context.metadata as PropertyMetadata; + // A field can be changed to a single value if its type ends with '[]' + return field.type.endsWith('[]'); + } + return false; + } + + analyze(): ChangeObject[] { + // This refactor is manual-only for now + return []; + } + + async initiateManualRefactor(context: ManualRefactorContext): Promise { + if (!context.metadata || !isField(context.metadata)) { + return undefined; + } + const field = context.metadata as PropertyMetadata; + + // Find the containing model's name for context + const fileMetadata = context.cache.getMetadataForFile(context.uri.fsPath); + let modelName = 'UnknownModel'; + if (fileMetadata) { + for (const [className, classData] of Object.entries(fileMetadata.classes)) { + if (classData.properties[field.name]) { + modelName = className; + break; + } + } + } + + const payload: ChangeFieldToSingleValuePayload = { + field: context.metadata as PropertyMetadata, + modelName, + isManual: true, + }; + + return { + type: 'CHANGE_FIELD_TO_SINGLE_VALUE', + uri: context.uri, + description: `Change field '${payload.field.name}' to single value.`, + payload, + }; + } + + async prepareEdit(change: ChangeObject, cache: MetadataCache): Promise { + if (change.type !== 'CHANGE_FIELD_TO_SINGLE_VALUE') { + throw new Error(`ChangeFieldToSingleValueTool can only handle CHANGE_FIELD_TO_SINGLE_VALUE changes, received: ${change.type}`); + } + + const { field } = change.payload; + const workspaceEdit = new vscode.WorkspaceEdit(); + const { declaration, references = [] } = field; + + // 1. Change type from an array to a single value + const newType = field.type.replace('[]', ''); + const document = await vscode.workspace.openTextDocument(declaration.uri); + const lineText = document.lineAt(declaration.range.start.line).text; + const typeRegex = new RegExp(`:\\s*${field.type.replace('[', '\\[').replace(']', '\\]')}`); + const match = lineText.match(typeRegex); + + if (match && typeof match.index === 'number') { + //TODO: fix the position calculation. Now is counting bad the position and therefore not removing the [] + const startPos = new vscode.Position(declaration.range.start.line, match.index); + const typeNodeText = match[0]; + const endPos = startPos.translate(0, typeNodeText.length); + const typeRange = new vscode.Range(startPos, endPos); + workspaceEdit.replace(declaration.uri, typeRange, `: ${newType}`); + } + + // 2. Singularize name if it's plural and update all references + if (field.name.endsWith('s')) { + const newName = field.name.slice(0, -1); + // Update the declaration + workspaceEdit.replace(declaration.uri, declaration.range, newName); + + // Update all other references + for (const ref of references) { + // Skip the declaration itself + if (ref.uri.fsPath === declaration.uri.fsPath && areRangesEqual(ref.range, declaration.range)) { + continue; + } + workspaceEdit.replace(ref.uri, ref.range, newName); + } + } + + return workspaceEdit; + } + + async executePrompt(change: ChangeObject): Promise { + if (change.type !== 'CHANGE_FIELD_TO_SINGLE_VALUE') return; + + const { field, modelName } = change.payload; + const oldName = field.name; + const newName = oldName.endsWith('s') ? oldName.slice(0, -1) : oldName; + const oldType = field.type; + const newType = oldType.replace('[]', ''); + const referenceLocations = field.references + .map(ref => `- \`${ref.uri.fsPath.split('/').pop()}\` at line ${ref.range.start.line + 1}`) + .join('\n'); + + const prompt = `## Field Refactoring - Code Review Required + +The field **\`${oldName}\`** in the model **\`${modelName}\`** has been refactored to be a single value. + +**Changes Applied:** +- **Name Change:** \`${oldName}\` -> \`${newName}\` +- **Type Change:** \`${oldType}\` -> \`${newType}\` +- All direct references to the field name have been updated. + +**Problem:** The logic using this field might now be incorrect. Code that treated it as an array (e.g., \`record.${newName}.push('value')\`) will now need to handle a single value (e.g., \`record.${newName} = 'value'\`). This could also affect how you handle null or undefined values. + +**Your Task:** Help me review and fix the code that uses this field. + +### Instructions: + +1. **Analyze the following locations** where the field was referenced. The logic in these places is likely broken. +2. **For each location, suggest the necessary code changes** to correctly handle the new single-value type. You might need to decide which element of the former array to use (e.g., the first one) or how to handle cases where the array was empty. +3. **Provide clear before/after code snippets** and explain your reasoning. + +### Reference Locations to Check: +${referenceLocations} + +### Common Patterns to Fix: +- **Array methods:** Replace \`.push()\`, \`.includes()\`, \`.map()\`, etc., with direct assignment or comparison. +- **Loops:** Remove loops that iterated over the field. +- **Assignments:** Ensure a single value is being assigned, not an array. +- **UI Display:** Adjust UI components that expected a list. + +Please start by analyzing the first reference location, paying close attention to how to resolve the array-to-single-value logic.`; + + try { + await vscode.commands.executeCommand('workbench.action.chat.open', prompt); + } catch (error) { + console.error('Failed to open chat with custom prompt:', error); + } + } +} \ No newline at end of file From b3397c96d00b23f7c439f0b40824d239cd62bf5b Mon Sep 17 00:00:00 2001 From: gaviola Date: Mon, 22 Sep 2025 08:54:30 -0300 Subject: [PATCH 2/7] Fix error to convert multiple value field into simge value --- .../tools/changeFieldToSingleValue.ts | 82 ++++++++++--------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/refactor/tools/changeFieldToSingleValue.ts b/src/refactor/tools/changeFieldToSingleValue.ts index 9c9c136..9531588 100644 --- a/src/refactor/tools/changeFieldToSingleValue.ts +++ b/src/refactor/tools/changeFieldToSingleValue.ts @@ -70,49 +70,55 @@ export class ChangeFieldToSingleValueTool implements IRefactorTool { } async prepareEdit(change: ChangeObject, cache: MetadataCache): Promise { - if (change.type !== 'CHANGE_FIELD_TO_SINGLE_VALUE') { - throw new Error(`ChangeFieldToSingleValueTool can only handle CHANGE_FIELD_TO_SINGLE_VALUE changes, received: ${change.type}`); - } + if (change.type !== 'CHANGE_FIELD_TO_SINGLE_VALUE') { + throw new Error(`ChangeFieldToSingleValueTool can only handle CHANGE_FIELD_TO_SINGLE_VALUE changes, received: ${change.type}`); + } - const { field } = change.payload; - const workspaceEdit = new vscode.WorkspaceEdit(); - const { declaration, references = [] } = field; - - // 1. Change type from an array to a single value - const newType = field.type.replace('[]', ''); - const document = await vscode.workspace.openTextDocument(declaration.uri); - const lineText = document.lineAt(declaration.range.start.line).text; - const typeRegex = new RegExp(`:\\s*${field.type.replace('[', '\\[').replace(']', '\\]')}`); - const match = lineText.match(typeRegex); - - if (match && typeof match.index === 'number') { - //TODO: fix the position calculation. Now is counting bad the position and therefore not removing the [] - const startPos = new vscode.Position(declaration.range.start.line, match.index); - const typeNodeText = match[0]; - const endPos = startPos.translate(0, typeNodeText.length); - const typeRange = new vscode.Range(startPos, endPos); - workspaceEdit.replace(declaration.uri, typeRange, `: ${newType}`); - } + const { field } = change.payload; + const workspaceEdit = new vscode.WorkspaceEdit(); + const { declaration, references = [] } = field; - // 2. Singularize name if it's plural and update all references - if (field.name.endsWith('s')) { - const newName = field.name.slice(0, -1); - // Update the declaration - workspaceEdit.replace(declaration.uri, declaration.range, newName); - - // Update all other references - for (const ref of references) { - // Skip the declaration itself - if (ref.uri.fsPath === declaration.uri.fsPath && areRangesEqual(ref.range, declaration.range)) { - continue; - } - workspaceEdit.replace(ref.uri, ref.range, newName); + // Change type from an array to a single value + const newType = field.type.replace('[]', ''); + const document = await vscode.workspace.openTextDocument(declaration.uri); + const lineText = document.lineAt(declaration.range.start.line).text; + + // Helper to escape special characters for use in a regular expression. + const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Build a robust regex that captures the base type and the array brackets separately. + const typeRegex = new RegExp(`(:\\s*${escapeRegExp(newType)})(\\[\\])`); + const match = lineText.match(typeRegex); + + if (match && typeof match.index === 'number') { + // The full text that was matched, e.g., ": string[]" + const fullMatchedText = match[0]; + const replacementText = match[1]; // e.g., ": string" + const startPos = new vscode.Position(declaration.range.start.line, match.index); + const endPos = startPos.translate(0, fullMatchedText.length); + const typeRange = new vscode.Range(startPos, endPos); + + workspaceEdit.replace(declaration.uri, typeRange, replacementText); + } + + // Singularize name if it's plural and update all references + if (field.name.endsWith('s')) { + const newName = field.name.slice(0, -1); + // Update the declaration + workspaceEdit.replace(declaration.uri, declaration.range, newName); + + for (const ref of references) { + // Skip the declaration itself + if (ref.uri.fsPath === declaration.uri.fsPath && areRangesEqual(ref.range, declaration.range)) { + continue; } + workspaceEdit.replace(ref.uri, ref.range, newName); } - - return workspaceEdit; } + return workspaceEdit; +} + async executePrompt(change: ChangeObject): Promise { if (change.type !== 'CHANGE_FIELD_TO_SINGLE_VALUE') return; @@ -131,7 +137,7 @@ The field **\`${oldName}\`** in the model **\`${modelName}\`** has been refactor **Changes Applied:** - **Name Change:** \`${oldName}\` -> \`${newName}\` -- **Type Change:** \`${oldType}\` -> \`${newType}\` +- **Type Change:** \`${oldType}\`[] -> \`${newType}\` - All direct references to the field name have been updated. **Problem:** The logic using this field might now be incorrect. Code that treated it as an array (e.g., \`record.${newName}.push('value')\`) will now need to handle a single value (e.g., \`record.${newName} = 'value'\`). This could also affect how you handle null or undefined values. From 7744b5b9a5e458dc982951b025cb025097b07045 Mon Sep 17 00:00:00 2001 From: gaviola Date: Mon, 22 Sep 2025 09:38:25 -0300 Subject: [PATCH 3/7] changes in the field multiplicity commands display --- package.json | 20 +++++++++---------- src/explorer/appTreeItem.ts | 14 ++++++++++++- src/refactor/tools/changeFieldToArray.ts | 4 ++-- .../tools/changeFieldToSingleValue.ts | 4 ++-- src/utils/metadata.ts | 9 +++++++++ 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index b079fd7..f7553d7 100644 --- a/package.json +++ b/package.json @@ -195,27 +195,27 @@ }, { "command": "slingr-vscode-extension.renameField", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "0_modification@1" }, { "command": "slingr-vscode-extension.changeFieldType", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "0_modification@2" }, { "command": "slingr-vscode-extension.changeFieldToArray", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem == 'fieldSingle'", "group": "0_modification@3" }, { "command": "slingr-vscode-extension.changeFieldToSingleValue", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem == 'fieldArray'", "group": "0_modification@4" }, { "command": "slingr-vscode-extension.deleteField", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "0_modification@5" }, { @@ -319,27 +319,27 @@ }, { "command": "slingr-vscode-extension.renameField", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "1_modification@4" }, { "command": "slingr-vscode-extension.changeFieldType", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "1_modification@5" }, { "command": "slingr-vscode-extension.changeFieldToArray", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem == 'fieldSingle'", "group": "1_modification@6" }, { "command": "slingr-vscode-extension.changeFieldToSingleValue", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem == 'fieldArray'", "group": "1_modification@7" }, { "command": "slingr-vscode-extension.deleteField", - "when": "view == slingrExplorer && viewItem == 'field'", + "when": "view == slingrExplorer && viewItem =~ /field(Single|Array)/", "group": "2_modification@3" } ] diff --git a/src/explorer/appTreeItem.ts b/src/explorer/appTreeItem.ts index 3ff4034..ddab760 100644 --- a/src/explorer/appTreeItem.ts +++ b/src/explorer/appTreeItem.ts @@ -14,9 +14,19 @@ export class AppTreeItem extends vscode.TreeItem { folderPath?: string ) { super(label, collapsibleState); - this.contextValue = itemType; this.folderPath = folderPath; + let finalContextValue = itemType; + if (itemType === 'field' && metadata && 'type' in metadata) { + const propMetadata = metadata as PropertyMetadata; + if (propMetadata.type.endsWith('[]')) { + finalContextValue = 'fieldArray'; + } else { + finalContextValue = 'fieldSingle'; + } + } + this.contextValue = finalContextValue; + // Icon logic if (!this.extensionUri) { console.warn(`[MyTreeItem] Extension URI not provided for item: "${label}". Local icons will not be loaded.`); @@ -47,6 +57,8 @@ export class AppTreeItem extends vscode.TreeItem { iconFileName = "database.svg"; break; case "field": + case "fieldSingle": + case "fieldArray": iconFileName = "field.svg"; break; case "modelActionsFolder": diff --git a/src/refactor/tools/changeFieldToArray.ts b/src/refactor/tools/changeFieldToArray.ts index 0c36808..8d0af45 100644 --- a/src/refactor/tools/changeFieldToArray.ts +++ b/src/refactor/tools/changeFieldToArray.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToArrayPayload } from '../refactorInterfaces'; import { MetadataCache, PropertyMetadata } from '../../cache/cache'; -import { isModelFile, isField, areRangesEqual } from '../../utils/metadata'; +import { isModelFile, isField, areRangesEqual, isFieldMultiple } from '../../utils/metadata'; /** * Tool to change a field from a single value type to an array type. @@ -26,7 +26,7 @@ export class ChangeFieldToArrayTool implements IRefactorTool { } if (isModelFile(context.uri) && isField(context.metadata)) { const field = context.metadata as PropertyMetadata; - return !field.type.endsWith('[]'); + return !isFieldMultiple(field); } return false; } diff --git a/src/refactor/tools/changeFieldToSingleValue.ts b/src/refactor/tools/changeFieldToSingleValue.ts index 9531588..5273643 100644 --- a/src/refactor/tools/changeFieldToSingleValue.ts +++ b/src/refactor/tools/changeFieldToSingleValue.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToSingleValuePayload } from '../refactorInterfaces'; import { MetadataCache, PropertyMetadata } from '../../cache/cache'; -import { isModelFile, isField, areRangesEqual } from '../../utils/metadata'; +import { isModelFile, isField, areRangesEqual, isFieldMultiple } from '../../utils/metadata'; /** * Tool to change a field from an array type to a single value type. @@ -27,7 +27,7 @@ export class ChangeFieldToSingleValueTool implements IRefactorTool { if (isModelFile(context.uri) && isField(context.metadata)) { const field = context.metadata as PropertyMetadata; // A field can be changed to a single value if its type ends with '[]' - return field.type.endsWith('[]'); + return isFieldMultiple(field); } return false; } diff --git a/src/utils/metadata.ts b/src/utils/metadata.ts index da7c83e..c5e4169 100644 --- a/src/utils/metadata.ts +++ b/src/utils/metadata.ts @@ -46,6 +46,15 @@ export function isField(metadata: DecoratedClass | PropertyMetadata | DataSource return 'type' in metadata && metadata.decorators.some(d => fieldDecoratorNames.includes(d.name) || d.name === 'Field'); } +/** + * Checks if a field property has a multiple (array) type. + * @param metadata - The property metadata to check. + * @returns True if the field type is an array (e.g., 'string[]'), false otherwise. + */ +export function isFieldMultiple(metadata: PropertyMetadata): boolean { + return metadata.type.endsWith('[]'); +} + export function isMethodMetadata(value: any): value is MethodMetadata { // Check for properties that uniquely identify a MethodMetadata object return typeof value === 'object' && value !== null && 'parameters' in value && 'declaration' in value; From 6fa7f86a2cffaca4e0b0c1f0c2ebf246980774e1 Mon Sep 17 00:00:00 2001 From: gaviola Date: Mon, 22 Sep 2025 10:16:54 -0300 Subject: [PATCH 4/7] cache now saves the multiplicity of the types --- src/cache/cache.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cache/cache.ts b/src/cache/cache.ts index f553dab..885ac01 100644 --- a/src/cache/cache.ts +++ b/src/cache/cache.ts @@ -721,16 +721,16 @@ export class MetadataCache { /** * Gets a clean, human-readable name for a ts-morph Type object. * This method correctly handles imported types, removing the "import(...)" part, - * and properly extracts element types from arrays. + * and preserves array notation for array types. * @param type The ts-morph Type object. * @returns The clean type name as a string. */ private getCleanTypeName(type: Type): string { - // Handle array types by extracting the element type + // Handle array types by preserving the array notation if (type.isArray()) { const elementType = type.getArrayElementType(); if (elementType) { - return this.getCleanTypeName(elementType); + return this.getCleanTypeName(elementType) + '[]'; } } From 156967df097552e682033e14aaa4dd7c01240d2c Mon Sep 17 00:00:00 2001 From: gaviola Date: Mon, 22 Sep 2025 10:57:37 -0300 Subject: [PATCH 5/7] added automati refactors to the multiplicity field commands --- src/refactor/tools/changeFieldToArray.ts | 97 +++++++++++++--- .../tools/changeFieldToSingleValue.ts | 108 ++++++++++++++---- 2 files changed, 163 insertions(+), 42 deletions(-) diff --git a/src/refactor/tools/changeFieldToArray.ts b/src/refactor/tools/changeFieldToArray.ts index 8d0af45..20af169 100644 --- a/src/refactor/tools/changeFieldToArray.ts +++ b/src/refactor/tools/changeFieldToArray.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; -import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToArrayPayload } from '../refactorInterfaces'; -import { MetadataCache, PropertyMetadata } from '../../cache/cache'; -import { isModelFile, isField, areRangesEqual, isFieldMultiple } from '../../utils/metadata'; +import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToArrayPayload, RenameModelPayload, RenameFieldPayload } from '../refactorInterfaces'; +import { FileMetadata, MetadataCache, PropertyMetadata } from '../../cache/cache'; +import { isModelFile, isField, areRangesEqual, isFieldMultiple, isModel } from '../../utils/metadata'; /** * Tool to change a field from a single value type to an array type. @@ -31,8 +31,67 @@ export class ChangeFieldToArrayTool implements IRefactorTool { return false; } - analyze(): ChangeObject[] { - return []; + analyze(oldFileMeta?: FileMetadata, newFileMeta?: FileMetadata, accumulatedChanges: ChangeObject[] = []): ChangeObject[] { + const changes: ChangeObject[] = []; + if (!oldFileMeta || !newFileMeta || !isModelFile(newFileMeta.uri)) { + return []; + } + + const classRenames = new Map(); + const fieldRenamesByClass = new Map>(); + for (const change of accumulatedChanges) { + if (change.type === 'RENAME_MODEL') { + const payload = change.payload as RenameModelPayload; + classRenames.set(payload.oldName, payload.newName); + } + if (change.type === 'RENAME_FIELD') { + const payload = change.payload as RenameFieldPayload; + if (!fieldRenamesByClass.has(payload.modelName)) { + fieldRenamesByClass.set(payload.modelName, new Map()); + } + fieldRenamesByClass.get(payload.modelName)!.set(payload.oldName, payload.newName); + } + } + + for (const oldClassName in oldFileMeta.classes) { + const oldClass = oldFileMeta.classes[oldClassName]; + const newClassName = classRenames.get(oldClassName) || oldClassName; + const newClass = newFileMeta.classes[newClassName]; + + if (!newClass || !isModel(oldClass) || !isModel(newClass)) { + continue; + } + + const fieldRenames = fieldRenamesByClass.get(oldClassName) || new Map(); + for (const oldPropName in oldClass.properties) { + const oldProp = oldClass.properties[oldPropName]; + const newPropName = fieldRenames.get(oldPropName) || oldPropName; + const newProp = newClass.properties[newPropName]; + + if (!newProp || !isField(oldProp) || !isField(newProp)) { + continue; + } + + const oldType = oldProp.type; + const newType = newProp.type; + + if (!oldType.endsWith('[]') && newType === oldType + '[]') { + const payload: ChangeFieldToArrayPayload = { + field: oldProp, + modelName: newClassName, + isManual: false, + }; + changes.push({ + type: 'CHANGE_FIELD_TO_ARRAY', + uri: newFileMeta.uri, + description: `Field '${newProp.name}' in Model '${newClassName}' changed to an array.`, + payload, + }); + } + } + } + + return changes; } async initiateManualRefactor(context: ManualRefactorContext): Promise { @@ -76,21 +135,23 @@ export class ChangeFieldToArrayTool implements IRefactorTool { const workspaceEdit = new vscode.WorkspaceEdit(); const { declaration, references = [] } = field; - // 1. Change the field's type to an array type - const document = await vscode.workspace.openTextDocument(declaration.uri); - const lineText = document.lineAt(declaration.range.start.line).text; - const typeRegex = new RegExp(`:\\s*${field.type}`); - const match = lineText.match(typeRegex); - - if (match && typeof match.index === 'number') { - const startPos = new vscode.Position(declaration.range.start.line, match.index); - const typeNodeText = match[0]; - const endPos = startPos.translate(0, typeNodeText.length); - const typeRange = new vscode.Range(startPos, endPos); - workspaceEdit.replace(declaration.uri, typeRange, `: ${field.type}[]`); + if (change.payload.isManual) { + // Change the field's type to an array type + const document = await vscode.workspace.openTextDocument(declaration.uri); + const lineText = document.lineAt(declaration.range.start.line).text; + const typeRegex = new RegExp(`:\\s*${field.type}`); + const match = lineText.match(typeRegex); + + if (match && typeof match.index === 'number') { + const startPos = new vscode.Position(declaration.range.start.line, match.index); + const typeNodeText = match[0]; + const endPos = startPos.translate(0, typeNodeText.length); + const typeRange = new vscode.Range(startPos, endPos); + workspaceEdit.replace(declaration.uri, typeRange, `: ${field.type}[]`); + } } - // 2. Pluralize name if it's not already plural and update all references + // Pluralize name if it's not already plural and update all references if (!field.name.endsWith('s')) { const newName = `${field.name}s`; // Update the declaration diff --git a/src/refactor/tools/changeFieldToSingleValue.ts b/src/refactor/tools/changeFieldToSingleValue.ts index 5273643..f809764 100644 --- a/src/refactor/tools/changeFieldToSingleValue.ts +++ b/src/refactor/tools/changeFieldToSingleValue.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; -import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToSingleValuePayload } from '../refactorInterfaces'; -import { MetadataCache, PropertyMetadata } from '../../cache/cache'; -import { isModelFile, isField, areRangesEqual, isFieldMultiple } from '../../utils/metadata'; +import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToSingleValuePayload, RenameFieldPayload, RenameModelPayload } from '../refactorInterfaces'; +import { FileMetadata, MetadataCache, PropertyMetadata } from '../../cache/cache'; +import { isModelFile, isField, areRangesEqual, isFieldMultiple, isModel } from '../../utils/metadata'; /** * Tool to change a field from an array type to a single value type. @@ -32,9 +32,67 @@ export class ChangeFieldToSingleValueTool implements IRefactorTool { return false; } - analyze(): ChangeObject[] { - // This refactor is manual-only for now - return []; + analyze(oldFileMeta?: FileMetadata, newFileMeta?: FileMetadata, accumulatedChanges: ChangeObject[] = []): ChangeObject[] { + const changes: ChangeObject[] = []; + if (!oldFileMeta || !newFileMeta || !isModelFile(newFileMeta.uri)) { + return []; + } + + const classRenames = new Map(); + const fieldRenamesByClass = new Map>(); + for (const change of accumulatedChanges) { + if (change.type === 'RENAME_MODEL') { + const payload = change.payload as RenameModelPayload; + classRenames.set(payload.oldName, payload.newName); + } + if (change.type === 'RENAME_FIELD') { + const payload = change.payload as RenameFieldPayload; + if (!fieldRenamesByClass.has(payload.modelName)) { + fieldRenamesByClass.set(payload.modelName, new Map()); + } + fieldRenamesByClass.get(payload.modelName)!.set(payload.oldName, payload.newName); + } + } + + for (const oldClassName in oldFileMeta.classes) { + const oldClass = oldFileMeta.classes[oldClassName]; + const newClassName = classRenames.get(oldClassName) || oldClassName; + const newClass = newFileMeta.classes[newClassName]; + + if (!newClass || !isModel(oldClass) || !isModel(newClass)) { + continue; + } + + const fieldRenames = fieldRenamesByClass.get(oldClassName) || new Map(); + for (const oldPropName in oldClass.properties) { + const oldProp = oldClass.properties[oldPropName]; + const newPropName = fieldRenames.get(oldPropName) || oldPropName; + const newProp = newClass.properties[newPropName]; + + if (!newProp || !isField(oldProp) || !isField(newProp)) { + continue; + } + + const oldType = oldProp.type; + const newType = newProp.type; + + if (oldType.endsWith('[]') && oldType === newType + '[]') { + const payload: ChangeFieldToSingleValuePayload = { + field: oldProp, + modelName: newClassName, + isManual: false, + }; + changes.push({ + type: 'CHANGE_FIELD_TO_SINGLE_VALUE', + uri: newFileMeta.uri, + description: `Field '${newProp.name}' in Model '${newClassName}' changed to a single value.`, + payload, + }); + } + } + } + + return changes; } async initiateManualRefactor(context: ManualRefactorContext): Promise { @@ -78,27 +136,29 @@ export class ChangeFieldToSingleValueTool implements IRefactorTool { const workspaceEdit = new vscode.WorkspaceEdit(); const { declaration, references = [] } = field; - // Change type from an array to a single value - const newType = field.type.replace('[]', ''); - const document = await vscode.workspace.openTextDocument(declaration.uri); - const lineText = document.lineAt(declaration.range.start.line).text; + if (change.payload.isManual) { + // Change type from an array to a single value + const newType = field.type.replace('[]', ''); + const document = await vscode.workspace.openTextDocument(declaration.uri); + const lineText = document.lineAt(declaration.range.start.line).text; - // Helper to escape special characters for use in a regular expression. - const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // Helper to escape special characters for use in a regular expression. + const escapeRegExp = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // Build a robust regex that captures the base type and the array brackets separately. - const typeRegex = new RegExp(`(:\\s*${escapeRegExp(newType)})(\\[\\])`); - const match = lineText.match(typeRegex); + // Build a robust regex that captures the base type and the array brackets separately. + const typeRegex = new RegExp(`(:\\s*${escapeRegExp(newType)})(\\[\\])`); + const match = lineText.match(typeRegex); - if (match && typeof match.index === 'number') { - // The full text that was matched, e.g., ": string[]" - const fullMatchedText = match[0]; - const replacementText = match[1]; // e.g., ": string" - const startPos = new vscode.Position(declaration.range.start.line, match.index); - const endPos = startPos.translate(0, fullMatchedText.length); - const typeRange = new vscode.Range(startPos, endPos); + if (match && typeof match.index === 'number') { + // The full text that was matched, e.g., ": string[]" + const fullMatchedText = match[0]; + const replacementText = match[1]; // e.g., ": string" + const startPos = new vscode.Position(declaration.range.start.line, match.index); + const endPos = startPos.translate(0, fullMatchedText.length); + const typeRange = new vscode.Range(startPos, endPos); - workspaceEdit.replace(declaration.uri, typeRange, replacementText); + workspaceEdit.replace(declaration.uri, typeRange, replacementText); + } } // Singularize name if it's plural and update all references @@ -137,7 +197,7 @@ The field **\`${oldName}\`** in the model **\`${modelName}\`** has been refactor **Changes Applied:** - **Name Change:** \`${oldName}\` -> \`${newName}\` -- **Type Change:** \`${oldType}\`[] -> \`${newType}\` +- **Type Change:** \`${oldType}[]\` -> \`${newType}\` - All direct references to the field name have been updated. **Problem:** The logic using this field might now be incorrect. Code that treated it as an array (e.g., \`record.${newName}.push('value')\`) will now need to handle a single value (e.g., \`record.${newName} = 'value'\`). This could also affect how you handle null or undefined values. From 5b5a1f8e520106d67cb3969e9cf6b1a40a417665 Mon Sep 17 00:00:00 2001 From: Gaviola Date: Wed, 24 Sep 2025 09:47:59 -0300 Subject: [PATCH 6/7] merge stash --- package-lock.json | 18 ++++++++++++++++++ package.json | 4 +++- src/refactor/tools/changeFieldToArray.ts | 7 ++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4c787fe..0006ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "slingr-vscode-extension", "version": "0.0.1", "dependencies": { + "pluralize": "^8.0.0", "ts-morph": "^26.0.0" }, "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", + "@types/pluralize": "^0.0.33", "@types/vscode": "^1.103.0", "@typescript-eslint/eslint-plugin": "^8.39.0", "@typescript-eslint/parser": "^8.39.0", @@ -471,6 +473,13 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/pluralize": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/pluralize/-/pluralize-0.0.33.tgz", + "integrity": "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/vscode": { "version": "1.103.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.103.0.tgz", @@ -2624,6 +2633,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index f7553d7..b25d245 100644 --- a/package.json +++ b/package.json @@ -380,6 +380,7 @@ "devDependencies": { "@types/mocha": "^10.0.10", "@types/node": "22.x", + "@types/pluralize": "^0.0.33", "@types/vscode": "^1.103.0", "@typescript-eslint/eslint-plugin": "^8.39.0", "@typescript-eslint/parser": "^8.39.0", @@ -389,6 +390,7 @@ "typescript": "^5.9.2" }, "dependencies": { + "pluralize": "^8.0.0", "ts-morph": "^26.0.0" } -} \ No newline at end of file +} diff --git a/src/refactor/tools/changeFieldToArray.ts b/src/refactor/tools/changeFieldToArray.ts index 20af169..4977bc3 100644 --- a/src/refactor/tools/changeFieldToArray.ts +++ b/src/refactor/tools/changeFieldToArray.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToArrayPayload, RenameModelPayload, RenameFieldPayload } from '../refactorInterfaces'; import { FileMetadata, MetadataCache, PropertyMetadata } from '../../cache/cache'; import { isModelFile, isField, areRangesEqual, isFieldMultiple, isModel } from '../../utils/metadata'; +import pluralize from 'pluralize'; /** * Tool to change a field from a single value type to an array type. @@ -151,9 +152,9 @@ export class ChangeFieldToArrayTool implements IRefactorTool { } } - // Pluralize name if it's not already plural and update all references - if (!field.name.endsWith('s')) { - const newName = `${field.name}s`; + // 2. Pluralize name if it's not already plural and update all references + const newName = pluralize.plural(field.name); + if (newName !== field.name) { // Update the declaration workspaceEdit.replace(declaration.uri, declaration.range, newName); From 91d51bbdce0a0df60ba92f0693c728ed2251b2a1 Mon Sep 17 00:00:00 2001 From: Gaviola Date: Wed, 24 Sep 2025 09:57:30 -0300 Subject: [PATCH 7/7] added a better pluralization and singularization --- src/refactor/tools/changeFieldToSingleValue.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/refactor/tools/changeFieldToSingleValue.ts b/src/refactor/tools/changeFieldToSingleValue.ts index f809764..d7758e7 100644 --- a/src/refactor/tools/changeFieldToSingleValue.ts +++ b/src/refactor/tools/changeFieldToSingleValue.ts @@ -2,6 +2,7 @@ import * as vscode from 'vscode'; import { ChangeObject, IRefactorTool, ManualRefactorContext, ChangeFieldToSingleValuePayload, RenameFieldPayload, RenameModelPayload } from '../refactorInterfaces'; import { FileMetadata, MetadataCache, PropertyMetadata } from '../../cache/cache'; import { isModelFile, isField, areRangesEqual, isFieldMultiple, isModel } from '../../utils/metadata'; +import pluralize from 'pluralize'; /** * Tool to change a field from an array type to a single value type. @@ -162,8 +163,8 @@ export class ChangeFieldToSingleValueTool implements IRefactorTool { } // Singularize name if it's plural and update all references - if (field.name.endsWith('s')) { - const newName = field.name.slice(0, -1); + const newName = pluralize.singular(field.name); + if (newName !== field.name) { // Update the declaration workspaceEdit.replace(declaration.uri, declaration.range, newName);