From 6758d6cb2be8e8b8509f7f9a65738ce53f5025b8 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 22 Aug 2024 19:49:57 +0400 Subject: [PATCH 1/8] Add basic sealed class states --- src/commands/sealed-states.command.ts | 204 +++++++++++++++++++++++++- 1 file changed, 197 insertions(+), 7 deletions(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 7af6b6c..28bfc6d 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -1,10 +1,200 @@ +import * as path from 'path'; import * as vscode from 'vscode'; - -import { - Uri -} from "vscode"; +import { Uri } from "vscode"; export const sealedStates = async (uri: Uri) => { - vscode.window.showInformationMessage('Hello World from Flutter Plus!'); - return; -}; \ No newline at end of file + // Extract the file name in a cross-platform way + const fileName = path.basename(uri.fsPath, '.dart'); + if (!fileName) { + vscode.window.showErrorMessage('Invalid file name.'); + return; + } + + // Convert the file name to CamelCase + let camelCaseName = fileName + .split('_') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); + + // Ensure the name ends with "State" + if (camelCaseName.endsWith('State') || camelCaseName.endsWith('States')) { + camelCaseName = camelCaseName.replace(/States?$/, 'State'); + } else { + camelCaseName += 'State'; + } + + // Prompt the user for the class name with a default value of CamelCase file name + const classNameInput = await vscode.window.showInputBox({ + prompt: 'Enter the class name', + value: camelCaseName, + }); + + if (!classNameInput) { + vscode.window.showErrorMessage('Class name input was cancelled.'); + return; + } + + // Convert the classNameInput to snake_case + const snakeCaseName = classNameInput + .replace(/([a-z])([A-Z])/g, '$1_$2') + .replace(/[\s-]/g, '_') + .toLowerCase(); + + // Prompt the user for the list of states, defaulting to common states + const statesInput = await vscode.window.showInputBox({ + prompt: 'Enter the states separated by commas', + value: 'idle, processing, successful, error', + }); + + if (!statesInput) { + vscode.window.showErrorMessage('Input was cancelled.'); + return; + } + + // Prepare a dictionary with different state formats + const states = statesInput.split(',').map(state => state.trim()); + const stateFormats = states.reduce((acc, state) => { + const capitalizedState = state.charAt(0).toUpperCase() + state.slice(1); + acc[state] = { + original: state, + capitalized: capitalizedState, + snakeCase: state.toLowerCase().replace(/ /g, '_') + }; + return acc; + }, {} as Record); + + // Generate the code using a StringBuilder approach + let codeBuilder: string[] = []; + + codeBuilder.push(`import 'package:meta/meta.dart';`); + codeBuilder.push(''); + codeBuilder.push(`/// Entity placeholder`); + codeBuilder.push(`typedef \${1:${classNameInput}}Entity = Object;`); + codeBuilder.push(''); + codeBuilder.push(`/// {@template \${2:${snakeCaseName}}}`); + codeBuilder.push(`/// \${1}.`); + codeBuilder.push(`/// {@endtemplate}`); + codeBuilder.push(`sealed class \${1} extends _\\$\${1}Base {`); + + Object.values(stateFormats).forEach(({ capitalized, original }) => { + codeBuilder.push(` /// ${capitalized}`); + codeBuilder.push(` /// {@macro \${2}}`); + codeBuilder.push(` const factory \${1}.${original}({`); + codeBuilder.push(` required \${1}Entity? data,`); + codeBuilder.push(` String message,`); + codeBuilder.push(` }) = \${1}\\$${capitalized};`); + codeBuilder.push(''); + }); + + codeBuilder.push(` /// Initial`); + codeBuilder.push(` /// {@macro \${2}}`); + codeBuilder.push(` factory \${1}.initial({`); + codeBuilder.push(` \${1}Entity? data,`); + codeBuilder.push(` String? message,`); + codeBuilder.push(` }) =>`); + codeBuilder.push(` \${1}.idle(`); + codeBuilder.push(` data: data,`); + codeBuilder.push(` message: message ?? 'Initial',`); + codeBuilder.push(` );`); + codeBuilder.push(''); + codeBuilder.push(` /// {@macro \${2}}`); + codeBuilder.push(` const \${1}({required super.data, required super.message});`); + codeBuilder.push(`}`); + codeBuilder.push(''); + + Object.values(stateFormats).forEach(({ capitalized }) => { + codeBuilder.push(`/// ${capitalized}`); + codeBuilder.push(`final class \${1}\\$${capitalized} extends \${1} {`); + codeBuilder.push(` const \${1}\\$${capitalized}({required super.data, super.message = '${capitalized}'});`); + codeBuilder.push(`}`); + codeBuilder.push(''); + }); + + // Base class definition with pattern matching methods + codeBuilder.push(`/// Pattern matching for [\${1}].`); + codeBuilder.push(`typedef \${1}Match = R Function(S element);`); + codeBuilder.push(''); + codeBuilder.push('@immutable'); + codeBuilder.push(`abstract base class _\\$\${1}Base {`); + codeBuilder.push(` const _\\$\${1}Base({required this.data, required this.message});`); + codeBuilder.push(''); + codeBuilder.push(` /// Data entity payload.`); + codeBuilder.push(` @nonVirtual`); + codeBuilder.push(` final \${1}Entity? data;`); + codeBuilder.push(''); + codeBuilder.push(` /// Message or description.`); + codeBuilder.push(` @nonVirtual`); + codeBuilder.push(` final String message;`); + codeBuilder.push(''); + codeBuilder.push(` /// Has data?`); + codeBuilder.push(` bool get hasData => data != null;`); + codeBuilder.push(''); + codeBuilder.push(` /// If an error has occurred?`); + codeBuilder.push(` bool get hasError => maybeMap(orElse: () => false, error: (_) => true);`); + codeBuilder.push(''); + codeBuilder.push(` /// Is in progress?`); + codeBuilder.push(` bool get isProcessing => maybeMap(orElse: () => false, processing: (_) => true);`); + codeBuilder.push(''); + codeBuilder.push(` /// Is in idle?`); + codeBuilder.push(` bool get isIdling => !isProcessing;`); + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R map({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` required \${1}Match ${original},`); + }); + codeBuilder.push(` }) =>`); + codeBuilder.push(` switch (this) {`); + Object.values(stateFormats).forEach(({ capitalized, original }) => { + codeBuilder.push(` \${1}\\$${capitalized} s => ${original}(s),`); + }); + codeBuilder.push(` _ => throw AssertionError(),`); + codeBuilder.push(` };`); + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R maybeMap({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` \${1}Match? ${original},`); + }); + codeBuilder.push(` required R Function() orElse,`); + codeBuilder.push(` }) =>`); + codeBuilder.push(` map(`); + Object.values(stateFormats).forEach(({ original }) => { + codeBuilder.push(` ${original}: ${original} ?? (_) => orElse(),`); + }); + codeBuilder.push(` );`); + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R? mapOrNull({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` \${1}Match? ${original},`); + }); + codeBuilder.push(` }) =>`); + codeBuilder.push(` map(`); + Object.values(stateFormats).forEach(({ original }) => { + codeBuilder.push(` ${original}: ${original} ?? (_) => null,`); + }); + codeBuilder.push(` );`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` int get hashCode => data.hashCode;`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` bool operator ==(Object other) => identical(this, other);`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` String toString() => '\${1}{message: \\$message}';`); + codeBuilder.push('}'); + codeBuilder.push(''); + + // Insert the generated code into the current document + const editor = vscode.window.activeTextEditor; + if (editor) { + editor.insertSnippet(new vscode.SnippetString(codeBuilder.join('\n'))); + /* editor.edit(editBuilder => { + editBuilder.insert(new vscode.Position(editor.document.lineCount, 0), codeBuilder.join('\n')); + }); */ + } else { + vscode.window.showErrorMessage('No active editor found.'); + } +}; From 682b4ee33a76496887b7c877781d3bff8049e391 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Thu, 22 Aug 2024 20:12:28 +0400 Subject: [PATCH 2/8] chore: Add options for generating code in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 28bfc6d..0cda187 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -63,6 +63,29 @@ export const sealedStates = async (uri: Uri) => { return acc; }, {} as Record); + const options = [ + { label: "Generate pattern matching", picked: true, id: 'patternMatching' }, + { label: "Generate equality operator (==)", picked: true, id: 'equalityOperator' }, + { label: "Generate toString method", picked: true, id: 'toStringMethod' }, + { label: "Generate property getters", picked: true, id: 'propertyGetters' }, + { label: "Generate type alias", picked: true, id: 'typeAlias' }, + { label: "Generate to/fromJson methods", picked: false, id: 'jsonMethods' }, + { label: "Generate Initial state", picked: true, id: 'initialState' }, + ]; + + const selectedOptions = await vscode.window.showQuickPick(options, { + canPickMany: true, + placeHolder: 'Select the options you want to generate', + }) ?? []; + + let patternMatchingOption = selectedOptions.find(option => option.id === 'patternMatching') !== undefined; + let equalityOperatorOption = selectedOptions.find(option => option.id === 'equalityOperator') !== undefined; + let toStringMethodOption = selectedOptions.find(option => option.id === 'toStringMethod') !== undefined; + let propertyGettersOption = selectedOptions.find(option => option.id === 'propertyGetters') !== undefined; + let typeAliasOption = selectedOptions.find(option => option.id === 'typeAlias') !== undefined; + let jsonMethodsOption = selectedOptions.find(option => option.id === 'jsonMethods') !== undefined; + let initialStateOption = selectedOptions.find(option => option.id === 'initialState') !== undefined; + // Generate the code using a StringBuilder approach let codeBuilder: string[] = []; From bdc1361b70f22a91bc7a0654d1cfd687a258dc59 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 13:33:38 +0400 Subject: [PATCH 3/8] chore: Improve readability and maintainability of GitHub Actions workflow files --- src/commands/sealed-states.command.ts | 218 ++++++++++++++++---------- 1 file changed, 138 insertions(+), 80 deletions(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 0cda187..605a0e5 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -42,7 +42,7 @@ export const sealedStates = async (uri: Uri) => { // Prompt the user for the list of states, defaulting to common states const statesInput = await vscode.window.showInputBox({ - prompt: 'Enter the states separated by commas', + prompt: 'Enter the states (camelCase) separated by commas', value: 'idle, processing, successful, error', }); @@ -53,6 +53,10 @@ export const sealedStates = async (uri: Uri) => { // Prepare a dictionary with different state formats const states = statesInput.split(',').map(state => state.trim()); + if (states.length === 0) { + vscode.window.showErrorMessage('Invalid states input.'); + return; + } const stateFormats = states.reduce((acc, state) => { const capitalizedState = state.charAt(0).toUpperCase() + state.slice(1); acc[state] = { @@ -64,13 +68,14 @@ export const sealedStates = async (uri: Uri) => { }, {} as Record); const options = [ + { label: "Nullable data", picked: true, id: 'nullableData' }, { label: "Generate pattern matching", picked: true, id: 'patternMatching' }, - { label: "Generate equality operator (==)", picked: true, id: 'equalityOperator' }, { label: "Generate toString method", picked: true, id: 'toStringMethod' }, + { label: "Generate Initial state", picked: true, id: 'initialState' }, { label: "Generate property getters", picked: true, id: 'propertyGetters' }, { label: "Generate type alias", picked: true, id: 'typeAlias' }, { label: "Generate to/fromJson methods", picked: false, id: 'jsonMethods' }, - { label: "Generate Initial state", picked: true, id: 'initialState' }, + { label: "Generate equality operator (==)", picked: false, id: 'equalityOperator' }, ]; const selectedOptions = await vscode.window.showQuickPick(options, { @@ -78,135 +83,187 @@ export const sealedStates = async (uri: Uri) => { placeHolder: 'Select the options you want to generate', }) ?? []; + let nullableDataOption = selectedOptions.find(option => option.id === 'nullableData') !== undefined; let patternMatchingOption = selectedOptions.find(option => option.id === 'patternMatching') !== undefined; let equalityOperatorOption = selectedOptions.find(option => option.id === 'equalityOperator') !== undefined; let toStringMethodOption = selectedOptions.find(option => option.id === 'toStringMethod') !== undefined; let propertyGettersOption = selectedOptions.find(option => option.id === 'propertyGetters') !== undefined; let typeAliasOption = selectedOptions.find(option => option.id === 'typeAlias') !== undefined; - let jsonMethodsOption = selectedOptions.find(option => option.id === 'jsonMethods') !== undefined; let initialStateOption = selectedOptions.find(option => option.id === 'initialState') !== undefined; + const dataType = nullableDataOption ? '\${1}Entity?' : '\${1}Entity'; + // Generate the code using a StringBuilder approach let codeBuilder: string[] = []; + // Import statements codeBuilder.push(`import 'package:meta/meta.dart';`); codeBuilder.push(''); codeBuilder.push(`/// Entity placeholder`); - codeBuilder.push(`typedef \${1:${classNameInput}}Entity = Object;`); + codeBuilder.push(`typedef \${1:${classNameInput}}Entity = \${0:Object};`); codeBuilder.push(''); codeBuilder.push(`/// {@template \${2:${snakeCaseName}}}`); codeBuilder.push(`/// \${1}.`); codeBuilder.push(`/// {@endtemplate}`); codeBuilder.push(`sealed class \${1} extends _\\$\${1}Base {`); + // Generate the factory constructors for each state Object.values(stateFormats).forEach(({ capitalized, original }) => { codeBuilder.push(` /// ${capitalized}`); codeBuilder.push(` /// {@macro \${2}}`); codeBuilder.push(` const factory \${1}.${original}({`); - codeBuilder.push(` required \${1}Entity? data,`); + codeBuilder.push(` required ${dataType} data,`); codeBuilder.push(` String message,`); codeBuilder.push(` }) = \${1}\\$${capitalized};`); codeBuilder.push(''); }); - codeBuilder.push(` /// Initial`); - codeBuilder.push(` /// {@macro \${2}}`); - codeBuilder.push(` factory \${1}.initial({`); - codeBuilder.push(` \${1}Entity? data,`); - codeBuilder.push(` String? message,`); - codeBuilder.push(` }) =>`); - codeBuilder.push(` \${1}.idle(`); - codeBuilder.push(` data: data,`); - codeBuilder.push(` message: message ?? 'Initial',`); - codeBuilder.push(` );`); - codeBuilder.push(''); + // Initial state + if (initialStateOption && Object.values(stateFormats).every(({ original }) => original !== 'initial')) { + codeBuilder.push(` /// Initial`); + codeBuilder.push(` /// {@macro \${2}}`); + codeBuilder.push(` factory \${1}.initial({`); + codeBuilder.push(` ${dataType} data,`); + codeBuilder.push(` String? message,`); + codeBuilder.push(` }) =>`); + codeBuilder.push(` \${1}\\$${Object.values(stateFormats)[0].capitalized}(`); + codeBuilder.push(` data: data,`); + codeBuilder.push(` message: message ?? 'Initial',`); + codeBuilder.push(` );`); + codeBuilder.push(''); + } + + // Constructor codeBuilder.push(` /// {@macro \${2}}`); codeBuilder.push(` const \${1}({required super.data, required super.message});`); codeBuilder.push(`}`); codeBuilder.push(''); - Object.values(stateFormats).forEach(({ capitalized }) => { + // Generate the classes for each state + Object.values(stateFormats).forEach(({ capitalized, snakeCase }) => { codeBuilder.push(`/// ${capitalized}`); codeBuilder.push(`final class \${1}\\$${capitalized} extends \${1} {`); codeBuilder.push(` const \${1}\\$${capitalized}({required super.data, super.message = '${capitalized}'});`); + + if (typeAliasOption) { + codeBuilder.push(` @override`); + codeBuilder.push(` String get type => '${snakeCase}';`); + } + codeBuilder.push(`}`); codeBuilder.push(''); }); // Base class definition with pattern matching methods - codeBuilder.push(`/// Pattern matching for [\${1}].`); - codeBuilder.push(`typedef \${1}Match = R Function(S element);`); - codeBuilder.push(''); + if (patternMatchingOption) { + codeBuilder.push(`/// Pattern matching for [\${1}].`); + codeBuilder.push(`typedef \${1}Match = R Function(S element);`); + codeBuilder.push(''); + } + + // Base class definition codeBuilder.push('@immutable'); codeBuilder.push(`abstract base class _\\$\${1}Base {`); codeBuilder.push(` const _\\$\${1}Base({required this.data, required this.message});`); codeBuilder.push(''); + + // Type alias + if (typeAliasOption) { + codeBuilder.push(` /// Type alias for [\${1}].`); + codeBuilder.push(` abstract final String type;`); + codeBuilder.push(''); + } + + // Data entity payload codeBuilder.push(` /// Data entity payload.`); codeBuilder.push(` @nonVirtual`); - codeBuilder.push(` final \${1}Entity? data;`); + codeBuilder.push(` final ${dataType} data;`); codeBuilder.push(''); + + // Message or description codeBuilder.push(` /// Message or description.`); codeBuilder.push(` @nonVirtual`); codeBuilder.push(` final String message;`); codeBuilder.push(''); - codeBuilder.push(` /// Has data?`); - codeBuilder.push(` bool get hasData => data != null;`); - codeBuilder.push(''); - codeBuilder.push(` /// If an error has occurred?`); - codeBuilder.push(` bool get hasError => maybeMap(orElse: () => false, error: (_) => true);`); - codeBuilder.push(''); - codeBuilder.push(` /// Is in progress?`); - codeBuilder.push(` bool get isProcessing => maybeMap(orElse: () => false, processing: (_) => true);`); - codeBuilder.push(''); - codeBuilder.push(` /// Is in idle?`); - codeBuilder.push(` bool get isIdling => !isProcessing;`); - codeBuilder.push(''); - codeBuilder.push(` /// Pattern matching for [\${1}].`); - codeBuilder.push(` R map({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` required \${1}Match ${original},`); - }); - codeBuilder.push(` }) =>`); - codeBuilder.push(` switch (this) {`); - Object.values(stateFormats).forEach(({ capitalized, original }) => { - codeBuilder.push(` \${1}\\$${capitalized} s => ${original}(s),`); - }); - codeBuilder.push(` _ => throw AssertionError(),`); - codeBuilder.push(` };`); - codeBuilder.push(''); - codeBuilder.push(` /// Pattern matching for [\${1}].`); - codeBuilder.push(` R maybeMap({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` \${1}Match? ${original},`); - }); - codeBuilder.push(` required R Function() orElse,`); - codeBuilder.push(` }) =>`); - codeBuilder.push(` map(`); - Object.values(stateFormats).forEach(({ original }) => { - codeBuilder.push(` ${original}: ${original} ?? (_) => orElse(),`); - }); - codeBuilder.push(` );`); - codeBuilder.push(''); - codeBuilder.push(` /// Pattern matching for [\${1}].`); - codeBuilder.push(` R? mapOrNull({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` \${1}Match? ${original},`); - }); - codeBuilder.push(` }) =>`); - codeBuilder.push(` map(`); - Object.values(stateFormats).forEach(({ original }) => { - codeBuilder.push(` ${original}: ${original} ?? (_) => null,`); - }); - codeBuilder.push(` );`); - codeBuilder.push(''); - codeBuilder.push(' @override'); - codeBuilder.push(` int get hashCode => data.hashCode;`); - codeBuilder.push(''); - codeBuilder.push(' @override'); - codeBuilder.push(` bool operator ==(Object other) => identical(this, other);`); - codeBuilder.push(''); - codeBuilder.push(' @override'); - codeBuilder.push(` String toString() => '\${1}{message: \\$message}';`); + + // Check existence of data + if (nullableDataOption) { + codeBuilder.push(` /// Has data?`); + codeBuilder.push(` bool get hasData => data != null;`); + codeBuilder.push(''); + } + + // Property getters + if (propertyGettersOption) { + Object.values(stateFormats).forEach(({ capitalized, snakeCase }) => { + codeBuilder.push(` /// Check if is ${capitalized}.`); + codeBuilder.push(` bool get is${capitalized} => this is \${1}\\$${capitalized};`); + codeBuilder.push(''); + }); + } + + // Pattern matching methods + if (patternMatchingOption) { + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R map({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` required \${1}Match ${original},`); + }); + codeBuilder.push(` }) =>`); + codeBuilder.push(` switch (this) {`); + Object.values(stateFormats).forEach(({ capitalized, original }) => { + codeBuilder.push(` \${1}\\$${capitalized} s => ${original}(s),`); + }); + codeBuilder.push(` _ => throw AssertionError(),`); + codeBuilder.push(` };`); + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R maybeMap({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` \${1}Match? ${original},`); + }); + codeBuilder.push(` required R Function() orElse,`); + codeBuilder.push(` }) =>`); + codeBuilder.push(` map(`); + Object.values(stateFormats).forEach(({ original }) => { + codeBuilder.push(` ${original}: ${original} ?? (_) => orElse(),`); + }); + codeBuilder.push(` );`); + codeBuilder.push(''); + codeBuilder.push(` /// Pattern matching for [\${1}].`); + codeBuilder.push(` R? mapOrNull({`); + Object.values(stateFormats).forEach(({ original, capitalized }) => { + codeBuilder.push(` \${1}Match? ${original},`); + }); + codeBuilder.push(` }) =>`); + codeBuilder.push(` map(`); + Object.values(stateFormats).forEach(({ original }) => { + codeBuilder.push(` ${original}: ${original} ?? (_) => null,`); + }); + codeBuilder.push(` );`); + } + + // Equality operator + if (equalityOperatorOption) { + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` int get hashCode => data.hashCode;`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` bool operator ==(Object other) => identical(this, other);`); + } + + // Generate toString method + if (toStringMethodOption) { + codeBuilder.push(''); + codeBuilder.push(' @override'); + if (typeAliasOption) { + codeBuilder.push(` String toString() => '\${1}.\\$type{message: \\$message}';`); + } else { + codeBuilder.push(` String toString() => '\${1}{message: \\$message}';`); + } + } codeBuilder.push('}'); codeBuilder.push(''); @@ -221,3 +278,4 @@ export const sealedStates = async (uri: Uri) => { vscode.window.showErrorMessage('No active editor found.'); } }; + From f7e3a7b818409d6cf36c559d1902562e9088768e Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 13:39:02 +0400 Subject: [PATCH 4/8] refactor: Improve handling of states in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 605a0e5..01494ba 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -52,7 +52,9 @@ export const sealedStates = async (uri: Uri) => { } // Prepare a dictionary with different state formats - const states = statesInput.split(',').map(state => state.trim()); + const states = statesInput.split(',').map(state => state.replace(/\s/g, '').trim()) + .filter((state) => state.length !== 0) + .map(state => state.charAt(0).toLowerCase() + state.slice(1)); if (states.length === 0) { vscode.window.showErrorMessage('Invalid states input.'); return; @@ -74,7 +76,6 @@ export const sealedStates = async (uri: Uri) => { { label: "Generate Initial state", picked: true, id: 'initialState' }, { label: "Generate property getters", picked: true, id: 'propertyGetters' }, { label: "Generate type alias", picked: true, id: 'typeAlias' }, - { label: "Generate to/fromJson methods", picked: false, id: 'jsonMethods' }, { label: "Generate equality operator (==)", picked: false, id: 'equalityOperator' }, ]; From 1bd3ae4c62662cd9dc25bdf0a6380d86c9781c01 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 14:25:05 +0400 Subject: [PATCH 5/8] Improve handling of states in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 154 ++++++++++++++++++-------- 1 file changed, 106 insertions(+), 48 deletions(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 01494ba..623a6c4 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -43,7 +43,7 @@ export const sealedStates = async (uri: Uri) => { // Prompt the user for the list of states, defaulting to common states const statesInput = await vscode.window.showInputBox({ prompt: 'Enter the states (camelCase) separated by commas', - value: 'idle, processing, successful, error', + value: 'idle, processing, successful, error, InProgress,Completed, Failed,123, 1, 2, 3;a;b ;;;,,,.;;..,abc, ACAB, МамаМылаРаму, 123abc, abc123', }); if (!statesInput) { @@ -51,23 +51,52 @@ export const sealedStates = async (uri: Uri) => { return; } - // Prepare a dictionary with different state formats - const states = statesInput.split(',').map(state => state.replace(/\s/g, '').trim()) - .filter((state) => state.length !== 0) - .map(state => state.charAt(0).toLowerCase() + state.slice(1)); + // Prepare a dictionary with different state formats by "," and ";". + const states = Array.from(new Set(statesInput.split(/,|;/) + .map(state => state.replace(/\s/g, '').trim()) + .filter(state => state.length !== 0) + .filter(state => /^[a-zA-Z]/.test(state)) + .filter(state => /^[A-Za-z0-9\s]+$/.test(state)) + .map(state => state.charAt(0).toLowerCase() + state.slice(1)) + )); + if (states.length === 0) { vscode.window.showErrorMessage('Invalid states input.'); return; } + const stateFormats = states.reduce((acc, state) => { - const capitalizedState = state.charAt(0).toUpperCase() + state.slice(1); + const words = state.split(/(?=[A-Z])|_|-|\s/).filter(word => word.length > 0); + + const pascalCase = words.map((word) => { + if (word.length === 1) { + return word.toUpperCase(); + } else { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + }).join(''); + + const camelCase = words.map((word, index) => { + if (index === 0) { + return word.toLowerCase(); + } else if (word.length === 1) { + return word.toUpperCase(); + } else { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + } + }).join(''); + + const snakeCase = words.map(word => word.toLowerCase()).join('_'); + acc[state] = { original: state, - capitalized: capitalizedState, - snakeCase: state.toLowerCase().replace(/ /g, '_') + pascalCase: pascalCase, + camelCase: camelCase, + snakeCase: snakeCase }; + return acc; - }, {} as Record); + }, {} as Record); const options = [ { label: "Nullable data", picked: true, id: 'nullableData' }, @@ -76,7 +105,7 @@ export const sealedStates = async (uri: Uri) => { { label: "Generate Initial state", picked: true, id: 'initialState' }, { label: "Generate property getters", picked: true, id: 'propertyGetters' }, { label: "Generate type alias", picked: true, id: 'typeAlias' }, - { label: "Generate equality operator (==)", picked: false, id: 'equalityOperator' }, + { label: "Generate equality operator (==)", picked: true, id: 'equalityOperator' }, ]; const selectedOptions = await vscode.window.showQuickPick(options, { @@ -108,45 +137,64 @@ export const sealedStates = async (uri: Uri) => { codeBuilder.push(`/// {@endtemplate}`); codeBuilder.push(`sealed class \${1} extends _\\$\${1}Base {`); + // Constructor + codeBuilder.push(` /// {@macro \${2}}`); + codeBuilder.push(` const \${1}({required super.data, required super.message});`); + // Generate the factory constructors for each state - Object.values(stateFormats).forEach(({ capitalized, original }) => { - codeBuilder.push(` /// ${capitalized}`); + Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { + codeBuilder.push(''); + codeBuilder.push(` /// ${pascalCase}`); codeBuilder.push(` /// {@macro \${2}}`); - codeBuilder.push(` const factory \${1}.${original}({`); - codeBuilder.push(` required ${dataType} data,`); + codeBuilder.push(` const factory \${1}.${camelCase}({`); + if (nullableDataOption) { + codeBuilder.push(` ${dataType} data,`); + } else { + codeBuilder.push(` required ${dataType} data,`); + } codeBuilder.push(` String message,`); - codeBuilder.push(` }) = \${1}\\$${capitalized};`); - codeBuilder.push(''); + codeBuilder.push(` }) = \${1}\\$${pascalCase};`); }); // Initial state - if (initialStateOption && Object.values(stateFormats).every(({ original }) => original !== 'initial')) { + if (initialStateOption && Object.values(stateFormats).every(({ camelCase }) => camelCase !== 'initial')) { + codeBuilder.push(''); codeBuilder.push(` /// Initial`); codeBuilder.push(` /// {@macro \${2}}`); codeBuilder.push(` factory \${1}.initial({`); - codeBuilder.push(` ${dataType} data,`); + if (nullableDataOption) { + codeBuilder.push(` ${dataType} data,`); + } else { + codeBuilder.push(` required ${dataType} data,`); + } codeBuilder.push(` String? message,`); codeBuilder.push(` }) =>`); - codeBuilder.push(` \${1}\\$${Object.values(stateFormats)[0].capitalized}(`); + if (Object.values(stateFormats).find(({ camelCase }) => camelCase === 'idle')) { + codeBuilder.push(` \${1}\\$Idle(`); + } else { + codeBuilder.push(` \${1}\\$${Object.values(stateFormats)[0].pascalCase}(`); + } codeBuilder.push(` data: data,`); codeBuilder.push(` message: message ?? 'Initial',`); codeBuilder.push(` );`); - codeBuilder.push(''); } - // Constructor - codeBuilder.push(` /// {@macro \${2}}`); - codeBuilder.push(` const \${1}({required super.data, required super.message});`); codeBuilder.push(`}`); codeBuilder.push(''); // Generate the classes for each state - Object.values(stateFormats).forEach(({ capitalized, snakeCase }) => { - codeBuilder.push(`/// ${capitalized}`); - codeBuilder.push(`final class \${1}\\$${capitalized} extends \${1} {`); - codeBuilder.push(` const \${1}\\$${capitalized}({required super.data, super.message = '${capitalized}'});`); + Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { + codeBuilder.push(`/// ${pascalCase}`); + codeBuilder.push(`final class \${1}\\$${pascalCase} extends \${1} {`); + + if (nullableDataOption) { + codeBuilder.push(` const \${1}\\$${pascalCase}({super.data, super.message = '${pascalCase}'});`); + } else { + codeBuilder.push(` const \${1}\\$${pascalCase}({required super.data, super.message = '${pascalCase}'});`); + } if (typeAliasOption) { + codeBuilder.push(''); codeBuilder.push(` @override`); codeBuilder.push(` String get type => '${snakeCase}';`); } @@ -196,9 +244,9 @@ export const sealedStates = async (uri: Uri) => { // Property getters if (propertyGettersOption) { - Object.values(stateFormats).forEach(({ capitalized, snakeCase }) => { - codeBuilder.push(` /// Check if is ${capitalized}.`); - codeBuilder.push(` bool get is${capitalized} => this is \${1}\\$${capitalized};`); + Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { + codeBuilder.push(` /// Check if is ${pascalCase}.`); + codeBuilder.push(` bool get is${pascalCase} => this is \${1}\\$${pascalCase};`); codeBuilder.push(''); }); } @@ -208,39 +256,39 @@ export const sealedStates = async (uri: Uri) => { codeBuilder.push(''); codeBuilder.push(` /// Pattern matching for [\${1}].`); codeBuilder.push(` R map({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` required \${1}Match ${original},`); + Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { + codeBuilder.push(` required \${1}Match ${camelCase},`); }); codeBuilder.push(` }) =>`); codeBuilder.push(` switch (this) {`); - Object.values(stateFormats).forEach(({ capitalized, original }) => { - codeBuilder.push(` \${1}\\$${capitalized} s => ${original}(s),`); + Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { + codeBuilder.push(` \${1}\\$${pascalCase} s => ${camelCase}(s),`); }); codeBuilder.push(` _ => throw AssertionError(),`); codeBuilder.push(` };`); codeBuilder.push(''); codeBuilder.push(` /// Pattern matching for [\${1}].`); codeBuilder.push(` R maybeMap({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` \${1}Match? ${original},`); - }); codeBuilder.push(` required R Function() orElse,`); + Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { + codeBuilder.push(` \${1}Match? ${camelCase},`); + }); codeBuilder.push(` }) =>`); codeBuilder.push(` map(`); - Object.values(stateFormats).forEach(({ original }) => { - codeBuilder.push(` ${original}: ${original} ?? (_) => orElse(),`); + Object.values(stateFormats).forEach(({ camelCase }) => { + codeBuilder.push(` ${camelCase}: ${camelCase} ?? (_) => orElse(),`); }); codeBuilder.push(` );`); codeBuilder.push(''); codeBuilder.push(` /// Pattern matching for [\${1}].`); codeBuilder.push(` R? mapOrNull({`); - Object.values(stateFormats).forEach(({ original, capitalized }) => { - codeBuilder.push(` \${1}Match? ${original},`); + Object.values(stateFormats).forEach(({ pascalCase, camelCase }) => { + codeBuilder.push(` \${1}Match? ${camelCase},`); }); codeBuilder.push(` }) =>`); codeBuilder.push(` map(`); - Object.values(stateFormats).forEach(({ original }) => { - codeBuilder.push(` ${original}: ${original} ?? (_) => null,`); + Object.values(stateFormats).forEach(({ camelCase }) => { + codeBuilder.push(` ${camelCase}: ${camelCase} ?? (_) => null,`); }); codeBuilder.push(` );`); } @@ -248,11 +296,21 @@ export const sealedStates = async (uri: Uri) => { // Equality operator if (equalityOperatorOption) { codeBuilder.push(''); - codeBuilder.push(' @override'); - codeBuilder.push(` int get hashCode => data.hashCode;`); - codeBuilder.push(''); - codeBuilder.push(' @override'); - codeBuilder.push(` bool operator ==(Object other) => identical(this, other);`); + if (typeAliasOption) { + codeBuilder.push(' @override'); + codeBuilder.push(` int get hashCode => Object.hash(type, data);`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` bool operator ==(Object other) => identical(this, other)`); + codeBuilder.push(` || (other is _\\$\${1}Base && type == other.type && identical(data, other.data));`); + } else { + codeBuilder.push(' @override'); + codeBuilder.push(` int get hashCode => data.hashCode;`); + codeBuilder.push(''); + codeBuilder.push(' @override'); + codeBuilder.push(` bool operator ==(Object other) => identical(this, other)`); + codeBuilder.push(` || (other is _\\$\${1}Base && runtimeType == other.runtimeType && identical(data, other.data));`); + } } // Generate toString method From 16e8e9ce2094d516dd20959eebf2ab6c07c69915 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 14:29:17 +0400 Subject: [PATCH 6/8] Improve handling of states in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 623a6c4..4389ea1 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -43,7 +43,7 @@ export const sealedStates = async (uri: Uri) => { // Prompt the user for the list of states, defaulting to common states const statesInput = await vscode.window.showInputBox({ prompt: 'Enter the states (camelCase) separated by commas', - value: 'idle, processing, successful, error, InProgress,Completed, Failed,123, 1, 2, 3;a;b ;;;,,,.;;..,abc, ACAB, МамаМылаРаму, 123abc, abc123', + value: 'idle, processing, success, failure', }); if (!statesInput) { From dbfb4f32fecf6bf8394d22a91561cdf79b27701e Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 14:33:36 +0400 Subject: [PATCH 7/8] Improve handling of states in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 4389ea1..32ac9db 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -180,10 +180,10 @@ export const sealedStates = async (uri: Uri) => { } codeBuilder.push(`}`); - codeBuilder.push(''); // Generate the classes for each state Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { + codeBuilder.push(''); codeBuilder.push(`/// ${pascalCase}`); codeBuilder.push(`final class \${1}\\$${pascalCase} extends \${1} {`); @@ -200,54 +200,53 @@ export const sealedStates = async (uri: Uri) => { } codeBuilder.push(`}`); - codeBuilder.push(''); }); // Base class definition with pattern matching methods if (patternMatchingOption) { + codeBuilder.push(''); codeBuilder.push(`/// Pattern matching for [\${1}].`); codeBuilder.push(`typedef \${1}Match = R Function(S element);`); - codeBuilder.push(''); } // Base class definition + codeBuilder.push(''); codeBuilder.push('@immutable'); codeBuilder.push(`abstract base class _\\$\${1}Base {`); codeBuilder.push(` const _\\$\${1}Base({required this.data, required this.message});`); - codeBuilder.push(''); // Type alias if (typeAliasOption) { + codeBuilder.push(''); codeBuilder.push(` /// Type alias for [\${1}].`); codeBuilder.push(` abstract final String type;`); - codeBuilder.push(''); } // Data entity payload + codeBuilder.push(''); codeBuilder.push(` /// Data entity payload.`); codeBuilder.push(` @nonVirtual`); codeBuilder.push(` final ${dataType} data;`); - codeBuilder.push(''); // Message or description + codeBuilder.push(''); codeBuilder.push(` /// Message or description.`); codeBuilder.push(` @nonVirtual`); codeBuilder.push(` final String message;`); - codeBuilder.push(''); // Check existence of data if (nullableDataOption) { + codeBuilder.push(''); codeBuilder.push(` /// Has data?`); codeBuilder.push(` bool get hasData => data != null;`); - codeBuilder.push(''); } // Property getters if (propertyGettersOption) { Object.values(stateFormats).forEach(({ pascalCase, snakeCase }) => { + codeBuilder.push(''); codeBuilder.push(` /// Check if is ${pascalCase}.`); codeBuilder.push(` bool get is${pascalCase} => this is \${1}\\$${pascalCase};`); - codeBuilder.push(''); }); } From 4870f50f06815f5f2ad8ccc15a7e88bcb61d75f9 Mon Sep 17 00:00:00 2001 From: Plague Fox Date: Fri, 23 Aug 2024 14:37:49 +0400 Subject: [PATCH 8/8] Improve handling of states in sealed-states.command.ts --- src/commands/sealed-states.command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/sealed-states.command.ts b/src/commands/sealed-states.command.ts index 32ac9db..49405af 100644 --- a/src/commands/sealed-states.command.ts +++ b/src/commands/sealed-states.command.ts @@ -43,7 +43,7 @@ export const sealedStates = async (uri: Uri) => { // Prompt the user for the list of states, defaulting to common states const statesInput = await vscode.window.showInputBox({ prompt: 'Enter the states (camelCase) separated by commas', - value: 'idle, processing, success, failure', + value: 'idle, processing, succeeded, failed', }); if (!statesInput) {