├── requirements.txt ├── plugin └── nvim_javagenie.lua ├── rplugin └── python3 │ ├── custom_types │ ├── log_level.py │ ├── declaration_type.py │ ├── enum_type.py │ ├── fetch_type.py │ ├── collection_type.py │ ├── field_temporal.py │ ├── entity_type.py │ ├── id_generation.py │ ├── mapping_type.py │ ├── create_java_file_args.py │ ├── id_generation_type.py │ ├── java_file_type.py │ ├── cascade_type.py │ ├── field_time_zone_storage.py │ ├── other.py │ ├── create_entity_args.py │ ├── project_properties.py │ ├── create_enum_field_args.py │ ├── java_file_data.py │ ├── create_id_field_args.py │ ├── create_many_to_many_args.py │ ├── create_basic_field_args.py │ ├── create_one_to_one_args.py │ └── create_many_to_one_args.py │ ├── ui │ ├── utils │ │ └── auto_field_name.lua │ ├── select_one.lua │ ├── create_java_file.lua │ ├── select_many.lua │ ├── create_entity.lua │ ├── enum_field.lua │ ├── many_to_many.lua │ ├── one_to_one.lua │ ├── many_to_one.lua │ ├── id_field.lua │ └── basic_field.lua │ ├── jpa_repo_commands.py │ ├── project_runner_commands.py │ ├── file_creation_commands.py │ ├── constants │ └── java_basic_types.py │ ├── entity_creation_commands.py │ ├── utils │ ├── logging.py │ ├── path_utils.py │ ├── java_file_utils.py │ ├── entity_creation_utils.py │ ├── common_utils.py │ ├── jpa_repo_utils.py │ ├── build_helper.py │ ├── treesitter_utils.py │ └── entity_field_utils.py │ ├── base.py │ ├── entity_rel_commands.py │ └── entity_field_commands.py ├── .gitignore ├── LICENSE ├── lua └── nvim_javagenie │ └── init.lua └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | pynvim==0.5.2 2 | tree-sitter==0.24.0 3 | tree-sitter-java==0.23.5 4 | -------------------------------------------------------------------------------- /plugin/nvim_javagenie.lua: -------------------------------------------------------------------------------- 1 | local nvim_javagenie = require("nvim_javagenie.init") 2 | 3 | nvim_javagenie.setup({}) 4 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/log_level.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class LogLevel(Enum): 5 | INFO = "info" 6 | CRITICAL = "critical" 7 | ERROR = "error" 8 | WARN = "warn" 9 | DEBUG = "debug" 10 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/declaration_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class DeclarationType(Enum): 5 | CLASS = "class" 6 | ENUM = "enum" 7 | INTERFACE = "interface" 8 | ANNOTATION = "annotation" 9 | RECORD = "record" 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore virtual environment directory 2 | venv/ 3 | 4 | # Ignore Python cache files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # Ignore other common Python temporary files 9 | *.pyo 10 | *.pyd 11 | *.pyd 12 | *.pyc 13 | 14 | *.log 15 | 16 | .idea/ 17 | -------------------------------------------------------------------------------- /rplugin/python3/ui/utils/auto_field_name.lua: -------------------------------------------------------------------------------- 1 | local function auto_field_name(str) 2 | if str == nil or str == "" then 3 | return str 4 | else 5 | local first_char = string.lower(string.sub(str, 1, 1)) 6 | local rest_of_string = string.sub(str, 2) 7 | return first_char .. rest_of_string 8 | end 9 | end 10 | 11 | return { 12 | auto_field_name = auto_field_name, 13 | } 14 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/enum_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EnumType(Enum): 5 | ORDINAL = "ORDINAL" 6 | STRING = "STRING" 7 | 8 | @classmethod 9 | def from_value(cls, value: str) -> "EnumType": 10 | for member in cls: 11 | if member.value == value: 12 | return member 13 | raise ValueError(f"No matching enum member for value '{value}'") 14 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/fetch_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FetchType(Enum): 5 | LAZY = "lazy" 6 | EAGER = "eager" 7 | NONE = "none" 8 | 9 | @classmethod 10 | def from_value(cls, value: str) -> "FetchType": 11 | for member in cls: 12 | if member.value == value: 13 | return member 14 | raise ValueError(f"No matching enum member for value '{value}'") 15 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/collection_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class CollectionType(Enum): 5 | SET = "set" 6 | LIST = "list" 7 | COLLECTION = "collection" 8 | 9 | @classmethod 10 | def from_value(cls, value: str) -> "CollectionType": 11 | for member in cls: 12 | if member.value == value: 13 | return member 14 | raise ValueError(f"No matching enum member for value '{value}'") 15 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/field_temporal.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FieldTemporal(Enum): 5 | DATE = "DATE" 6 | TIME = "TIME" 7 | TIMESTAMP = "TIMESTAMP" 8 | 9 | @classmethod 10 | def from_value(cls, value: str) -> "FieldTemporal": 11 | for member in cls: 12 | if member.value == value: 13 | return member 14 | raise ValueError(f"No matching enum member for value '{value}'") 15 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/entity_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EntityType(Enum): 5 | ENTITY = "entity" 6 | EMBEDDABLE = "embeddable" 7 | MAPPED_SUPERCLASS = "mapped_superclass" 8 | 9 | @classmethod 10 | def from_value(cls, value: str) -> "EntityType": 11 | for member in cls: 12 | if member.value == value: 13 | return member 14 | raise ValueError(f"No matching enum member for value '{value}'") 15 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/id_generation.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class IdGeneration(Enum): 5 | NONE = "none" 6 | AUTO = "auto" 7 | IDENTITY = "identity" 8 | SEQUENCE = "sequence" 9 | UUID = "uuid" 10 | 11 | @classmethod 12 | def from_value(cls, value: str) -> "IdGeneration": 13 | for member in cls: 14 | if member.value == value: 15 | return member 16 | raise ValueError(f"No matching enum member for value '{value}'") 17 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/mapping_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class MappingType(Enum): 5 | UNIDIRECTIONAL_JOIN_COLUMN = "unidirectional_join_column" 6 | BIDIRECTIONAL_JOIN_COLUMN = "bidirectional_join_column" 7 | 8 | @classmethod 9 | def from_value(cls, value: str) -> "MappingType": 10 | for member in cls: 11 | if member.value == value: 12 | return member 13 | raise ValueError(f"No matching enum member for value '{value}'") 14 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_java_file_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from custom_types.java_file_type import JavaFileType 3 | 4 | 5 | @dataclass 6 | class CreateJavaFileArgs: 7 | package_path: str 8 | file_name: str 9 | file_type: InitVar[str] 10 | file_type_enum: JavaFileType = field(init=False) 11 | 12 | def __post_init__( 13 | self, 14 | file_type: str, 15 | ): 16 | self.file_type_enum = JavaFileType.from_value(file_type) 17 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/id_generation_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class IdGenerationType(Enum): 5 | NONE = "none" 6 | ORM_PROVIDED = "orm_provided" 7 | ENTITY_EXCLUSIVE_GENERATION = "entity_exclusive_generation" 8 | 9 | @classmethod 10 | def from_value(cls, value: str) -> "IdGenerationType": 11 | for member in cls: 12 | if member.value == value: 13 | return member 14 | raise ValueError(f"No matching enum member for value '{value}'") 15 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/java_file_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class JavaFileType(Enum): 5 | CLASS = "class" 6 | INTERFACE = "interface" 7 | RECORD = "record" 8 | ENUM = "enum" 9 | ANNOTATION = "annotation" 10 | 11 | @classmethod 12 | def from_value(cls, value: str) -> "JavaFileType": 13 | for member in cls: 14 | if member.value == value: 15 | return member 16 | raise ValueError(f"No matching enum member for value '{value}'") 17 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/cascade_type.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class CascadeType(Enum): 5 | ALL = "all" 6 | PERSIST = "persist" 7 | MERGE = "merge" 8 | REMOVE = "remove" 9 | REFRESH = "refresh" 10 | DETACH = "detach" 11 | 12 | @classmethod 13 | def from_value(cls, value: str) -> "CascadeType": 14 | for member in cls: 15 | if member.value == value: 16 | return member 17 | raise ValueError(f"No matching enum member for value '{value}'") 18 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/field_time_zone_storage.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class FieldTimeZoneStorage(Enum): 5 | NATIVE = "NATIVE" 6 | NORMALIZE = "NORMALIZE" 7 | NORMALIZE_UTC = "NORMALIZE_UTC" 8 | COLUMN = "COLUMN" 9 | AUTO = "AUTO" 10 | 11 | @classmethod 12 | def from_value(cls, value: str) -> "FieldTimeZoneStorage": 13 | for member in cls: 14 | if member.value == value: 15 | return member 16 | raise ValueError(f"No matching enum member for value '{value}'") 17 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/other.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Other(Enum): 5 | MANDATORY = "mandatory" 6 | UNIQUE = "unique" 7 | ORPHAN_REMOVAL = "orphan_removal" 8 | LARGE_OBJECT = "large_object" 9 | EQUALS_HASHCODE = "equals_hashcode" 10 | MUTABLE = "mutable" 11 | 12 | @classmethod 13 | def from_value(cls, value: str) -> "Other": 14 | for member in cls: 15 | if member.value == value: 16 | return member 17 | raise ValueError(f"No matching enum member for value '{value}'") 18 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_entity_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import Optional 3 | from custom_types.entity_type import EntityType 4 | 5 | 6 | @dataclass 7 | class CreateEntityArgs: 8 | package_path: str 9 | entity_name: str 10 | entity_type: InitVar[str] 11 | entity_type_enum: EntityType = field(init=False) 12 | parent_entity_type: Optional[str] = None 13 | parent_entity_package_path: Optional[str] = None 14 | 15 | def __post_init__(self, entity_type: str): 16 | self.entity_type_enum = EntityType.from_value(entity_type) 17 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/project_properties.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from pathlib import Path 3 | 4 | 5 | @dataclass 6 | class ProjectProperties: 7 | project_name: str 8 | project_group: str 9 | project_version: str 10 | project_build_dir: InitVar[str] 11 | project_dir: InitVar[str] 12 | project_root_dir: InitVar[str] 13 | project_build_path: Path = field(init=False) 14 | project_path: Path = field(init=False) 15 | project_root_path: Path = field(init=False) 16 | 17 | def __post_init__( 18 | self, project_build_dir: str, project_dir: str, project_root_dir: str 19 | ): 20 | self.project_build_path = Path(project_build_dir) 21 | self.project_path = Path(project_dir) 22 | self.project_root_path = Path(project_root_dir) 23 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_enum_field_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.enum_type import EnumType 4 | from custom_types.other import Other 5 | 6 | 7 | @dataclass 8 | class CreateEnumEntityFieldArgs: 9 | field_path: str 10 | field_package_path: str 11 | field_type: str 12 | field_name: str 13 | enum_type: InitVar[str] 14 | enum_type_enum: EnumType = field(init=False) 15 | field_length: Optional[int] = None 16 | other: InitVar[Optional[List[str]]] = None 17 | other_enum: List[Other] = field(init=False, default_factory=list) 18 | 19 | def __post_init__(self, enum_type: str, other: Optional[List[str]]): 20 | self.enum_type_enum = EnumType.from_value(enum_type) 21 | self.field_length = ( 22 | int(self.field_length) if self.field_length is not None else None 23 | ) 24 | other = other or [] 25 | self.other_enum = [Other.from_value(value) for value in other] 26 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/java_file_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from pathlib import Path 3 | 4 | from tree_sitter import Tree 5 | 6 | from custom_types.declaration_type import DeclarationType 7 | 8 | 9 | @dataclass 10 | class JavaFileData: 11 | package_path: str 12 | file_name: str 13 | path: Path 14 | tree: Tree 15 | declaration_type: DeclarationType 16 | is_jpa_entity: bool 17 | is_mapped_superclass: bool 18 | 19 | def print(self) -> str: 20 | repr = ( 21 | f"EntityData(" 22 | f"package_path='{self.package_path}', " 23 | f"file_name='{self.file_name}', " 24 | f"path='{str(self.path)}', " 25 | f"declaration_type='{self.declaration_type}', " 26 | f"is_jpa_entity='{self.is_jpa_entity}'" 27 | f"is_mapped_superclass='{self.is_mapped_superclass}'" 28 | f")" 29 | ) 30 | # Escape single quotes for Vim 31 | repr = repr.replace("'", "''") 32 | return repr 33 | -------------------------------------------------------------------------------- /rplugin/python3/jpa_repo_commands.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pynvim.api import Nvim 4 | from pynvim import List, command, plugin 5 | 6 | from base import Base 7 | from custom_types.log_level import LogLevel 8 | 9 | 10 | @plugin 11 | class JpaRepoCommands(Base): 12 | def __init__(self, nvim: Nvim) -> None: 13 | super().__init__(nvim) 14 | 15 | @command("CreateJPARepository", nargs="*") 16 | def create_jpa_repo_repository(self, args: List[str]) -> None: 17 | self.logging.reset_log_file() 18 | self.logging.log(args, LogLevel.DEBUG) 19 | if len(args) > 1: 20 | error_msg = "At least one and max 2 arguments allowed" 21 | self.logging.log(error_msg, LogLevel.ERROR) 22 | raise ValueError(error_msg) 23 | self.debug = True if "debug" in args else False 24 | buffer_path = Path(self.nvim.current.buffer.name) 25 | self.jpa_repo_utils.create_jpa_repository( 26 | buffer_path=buffer_path, debug=self.debug 27 | ) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 André Luís de Oliveira Silva 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rplugin/python3/project_runner_commands.py: -------------------------------------------------------------------------------- 1 | from pynvim.api import Nvim 2 | from pynvim import List, command, plugin 3 | 4 | from base import Base 5 | from custom_types.log_level import LogLevel 6 | 7 | 8 | @plugin 9 | class JpaRepoCommands(Base): 10 | def __init__(self, nvim: Nvim) -> None: 11 | super().__init__(nvim) 12 | self.debug: bool = False 13 | 14 | @command("BuildAndRunProject", nargs="*") 15 | def build_and_run_project(self, args: List[str]) -> None: 16 | self.logging.reset_log_file() 17 | self.logging.log(args, LogLevel.DEBUG) 18 | if len(args) > 1: 19 | error_msg = "Only one argument allowed" 20 | self.logging.log(error_msg, LogLevel.ERROR) 21 | raise ValueError(error_msg) 22 | self.debug = True if "debug" in args else False 23 | self.build_helper.run(self.debug) 24 | 25 | @command("BuildProject", nargs="*") 26 | def build__project(self, args: List[str]) -> None: 27 | self.logging.reset_log_file() 28 | self.logging.log(args, LogLevel.DEBUG) 29 | if len(args) > 1: 30 | error_msg = "Only one argument allowed" 31 | self.logging.log(error_msg, LogLevel.ERROR) 32 | raise ValueError(error_msg) 33 | self.debug = True if "debug" in args else False 34 | self.build_helper.build(self.debug) 35 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_id_field_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.id_generation import IdGeneration 4 | from custom_types.id_generation_type import IdGenerationType 5 | from custom_types.other import Other 6 | 7 | 8 | @dataclass 9 | class CreateIdEntityFieldArgs: 10 | field_package_path: str 11 | field_type: str 12 | field_name: str 13 | id_generation: InitVar[str] 14 | id_generation_enum: IdGeneration = field(init=False) 15 | id_generation_type: InitVar[str] 16 | id_generation_type_enum: IdGenerationType = field(init=False) 17 | generator_name: Optional[str] = None 18 | sequence_name: Optional[str] = None 19 | initial_value: Optional[int] = None 20 | allocation_size: Optional[int] = None 21 | other: InitVar[Optional[List[str]]] = None 22 | other_enum: List[Other] = field(init=False, default_factory=list) 23 | 24 | def __post_init__( 25 | self, id_generation: str, id_generation_type: str, other: Optional[List[str]] 26 | ): 27 | self.id_generation_enum = IdGeneration.from_value(id_generation) 28 | self.id_generation_type_enum = IdGenerationType.from_value(id_generation_type) 29 | other = other or [] 30 | self.other_enum = [Other.from_value(value) for value in other] 31 | self.initial_value = ( 32 | int(self.initial_value) if self.initial_value is not None else None 33 | ) 34 | self.allocation_size = ( 35 | int(self.allocation_size) if self.allocation_size is not None else None 36 | ) 37 | 38 | -------------------------------------------------------------------------------- /rplugin/python3/file_creation_commands.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | from pynvim import plugin, command, function 3 | from pynvim.api import Nvim 4 | 5 | from base import Base 6 | from custom_types.log_level import LogLevel 7 | from custom_types.create_java_file_args import CreateJavaFileArgs 8 | 9 | 10 | @plugin 11 | class JavaFileCreationCommands(Base): 12 | def __init__(self, nvim: Nvim) -> None: 13 | super().__init__(nvim) 14 | self.debug: bool = False 15 | 16 | @command("CreateNewJavaFile", nargs="*") 17 | def create_java_file(self, args: List[str]) -> None: 18 | self.logging.reset_log_file() 19 | if len(args) > 1: 20 | error_msg = "Only one arg is allowed" 21 | self.logging.log(error_msg, LogLevel.ERROR) 22 | raise ValueError(error_msg) 23 | self.debug = True if "debug" in args else False 24 | root_package_path = str(self.path_utils.get_spring_root_package_path(True)) 25 | self.nvim.exec_lua( 26 | self.common_utils.read_ui_file_as_string("create_java_file.lua"), 27 | ( 28 | self.ui_path, 29 | root_package_path, 30 | ), 31 | ) 32 | 33 | @function("CreateNewJavaFileCallback") 34 | def create_new_java_file_callback(self, args: List[Dict]): 35 | converted_args = CreateJavaFileArgs(**args[0]) 36 | if self.debug: 37 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 38 | self.java_file_utils.create_java_file( 39 | args=converted_args, 40 | debug=self.debug, 41 | ) 42 | -------------------------------------------------------------------------------- /rplugin/python3/ui/select_one.lua: -------------------------------------------------------------------------------- 1 | local n = require("nui-components") 2 | 3 | -- @param size number|nil: The size of the tree (optional). 4 | -- @param label string: The label for the tree border. 5 | -- @param data table: A list of nodes for the tree, where each node is a table containing text and id. 6 | -- @param signal_key string: The key in the signal table to store the selected node id. 7 | -- @param signal table: A table that stores the selected node id. 8 | -- @param autofocus boolean|nil: If true, autofocuses the tree component (optional). 9 | -- @param on_select_callback function|nil: A callback function triggered on node selection (optional). 10 | -- @return table: The rendered tree component. 11 | local function render_component(size, label, data, signal_key, signal, autofocus, on_select_callback) 12 | return n.tree({ 13 | autofocus = autofocus or false, 14 | size = size or #data, 15 | border_label = label, 16 | data = data, 17 | on_select = function(selected_node, component) 18 | local tree = component:get_tree() 19 | for _, node in ipairs(data) do 20 | node.is_done = false 21 | end 22 | selected_node.is_done = true 23 | signal[signal_key] = selected_node.id 24 | if on_select_callback then 25 | on_select_callback(selected_node, signal) 26 | end 27 | tree:render() 28 | end, 29 | prepare_node = function(node, line, _) 30 | if node.is_done then 31 | line:append("◉", "String") 32 | else 33 | line:append("○", "Comment") 34 | end 35 | line:append(" ") 36 | line:append(node.text) 37 | return line 38 | end, 39 | }) 40 | end 41 | 42 | return { 43 | render_component = render_component, 44 | } 45 | -------------------------------------------------------------------------------- /rplugin/python3/constants/java_basic_types.py: -------------------------------------------------------------------------------- 1 | JAVA_BASIC_TYPES = [ 2 | ("String", "java.lang"), 3 | ("Long", "java.lang"), 4 | ("Integer", "java.lang"), 5 | ("Boolean", "java.lang"), 6 | ("Double", "java.lang"), 7 | ("BigDecimal", "java.math"), 8 | ("Instant", "java.time"), 9 | ("LocalDateTime", "java.time"), 10 | ("LocalDate", "java.time"), 11 | ("LocalTime", "java.time"), 12 | ("OffsetDateTime", "java.time"), 13 | ("OffsetTime", "java.time"), 14 | ("Date", "java.util"), 15 | ("Date", "java.sql"), 16 | ("Time", "java.sql"), 17 | ("Timestamp", "java.sql"), 18 | ("TimeZone", "java.util"), 19 | ("Byte[]", "java.lang"), 20 | ("Blob", "java.sql"), 21 | ("Byte", "java.lang"), 22 | ("Character", "java.lang"), 23 | ("Short", "java.lang"), 24 | ("Float", "java.lang"), 25 | ("BigInteger", "java.math"), 26 | ("URL", "java.net"), 27 | ("Duration", "java.time"), 28 | ("ZonedDateTime", "java.time"), 29 | ("Calendar", "java.util"), 30 | ("Locale", "java.util"), 31 | ("Currency", "java.util"), 32 | ("Class", "java.lang"), 33 | ("UUID", "java.util"), 34 | ("Character[]", "java.lang"), 35 | ("Clob", "java.sql"), 36 | ("NClob", "java.sql"), 37 | ("boolean", None), 38 | ("byte", None), 39 | ("float", None), 40 | ("char", None), 41 | ("int", None), 42 | ("double", None), 43 | ("short", None), 44 | ("long", None), 45 | ("byte[]", None), 46 | ("char[]", None), 47 | ("Geometry", "org.geolatte.geom"), 48 | ("Geometry", "com.vividsolutions.jts.geom"), 49 | ("InetAddress", "java.net"), 50 | ("ZoneOffset", "java.time"), 51 | ] 52 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_many_to_many_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.mapping_type import MappingType 4 | from custom_types.cascade_type import CascadeType 5 | from custom_types.other import Other 6 | 7 | 8 | @dataclass 9 | class CreateManyToManyRelArgs: 10 | inverse_field_type: str 11 | mapping_type: InitVar[Optional[str]] = None 12 | mapping_type_enum: Optional[MappingType] = field(init=False) 13 | owning_side_cascades: InitVar[Optional[List[str]]] = None 14 | owning_side_cascades_enum: List[CascadeType] = field( 15 | init=False, default_factory=list 16 | ) 17 | inverse_side_cascades: InitVar[Optional[List[str]]] = None 18 | inverse_side_cascades_enum: List[CascadeType] = field( 19 | init=False, default_factory=list 20 | ) 21 | inverse_side_other: InitVar[Optional[List[str]]] = None 22 | inverse_side_other_enum: List[Other] = field(init=False, default_factory=list) 23 | 24 | def __post_init__( 25 | self, 26 | mapping_type: Optional[str], 27 | owning_side_cascades: Optional[List[str]], 28 | inverse_side_cascades: Optional[List[str]], 29 | inverse_side_other: Optional[List[str]], 30 | ): 31 | self.mapping_type_enum = ( 32 | MappingType.from_value(mapping_type) if mapping_type else None 33 | ) 34 | owning_side_cascades = owning_side_cascades or [] 35 | self.owning_side_cascades_enum = [ 36 | CascadeType.from_value(value) for value in owning_side_cascades 37 | ] 38 | inverse_side_cascades = inverse_side_cascades or [] 39 | self.inverse_side_cascades_enum = [ 40 | CascadeType.from_value(value) for value in inverse_side_cascades 41 | ] 42 | inverse_side_other = inverse_side_other or [] 43 | self.inverse_side_other_enum = [ 44 | Other.from_value(value) for value in inverse_side_other 45 | ] 46 | 47 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_basic_field_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.field_time_zone_storage import FieldTimeZoneStorage 4 | from custom_types.field_temporal import FieldTemporal 5 | from custom_types.other import Other 6 | 7 | 8 | @dataclass 9 | class CreateBasicEntityFieldArgs: 10 | field_package_path: str 11 | field_type: str 12 | field_name: str 13 | field_length: Optional[int] = None 14 | field_precision: Optional[int] = None 15 | field_scale: Optional[int] = None 16 | field_time_zone_storage: InitVar[Optional[str]] = None 17 | field_time_zone_storage_enum: Optional[FieldTimeZoneStorage] = field(init=False) 18 | field_temporal: InitVar[Optional[str]] = None 19 | field_temporal_enum: Optional[FieldTemporal] = field(init=False) 20 | other: InitVar[Optional[List[str]]] = None 21 | other_enum: List[Other] = field(init=False, default_factory=list) 22 | 23 | def __post_init__( 24 | self, 25 | field_time_zone_storage: Optional[str], 26 | field_temporal: Optional[str], 27 | other: Optional[List[str]], 28 | ): 29 | self.field_time_zone_storage_enum = ( 30 | FieldTimeZoneStorage.from_value(field_time_zone_storage) 31 | if field_time_zone_storage 32 | else None 33 | ) 34 | self.field_temporal_enum = ( 35 | FieldTemporal.from_value(field_temporal) if field_temporal else None 36 | ) 37 | other = other or [] 38 | self.other_enum = [Other.from_value(value) for value in other] 39 | self.field_length = ( 40 | int(self.field_length) if self.field_length is not None else None 41 | ) 42 | self.field_precision = ( 43 | int(self.field_precision) if self.field_precision is not None else None 44 | ) 45 | self.field_scale = ( 46 | int(self.field_scale) if self.field_scale is not None else None 47 | ) 48 | -------------------------------------------------------------------------------- /rplugin/python3/entity_creation_commands.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | from pynvim import plugin, command, function 3 | from pynvim.api import Nvim 4 | 5 | from base import Base 6 | from custom_types.create_entity_args import CreateEntityArgs 7 | from custom_types.log_level import LogLevel 8 | 9 | 10 | @plugin 11 | class EntityCreationCommands(Base): 12 | def __init__(self, nvim: Nvim) -> None: 13 | super().__init__(nvim) 14 | self.debug: bool = False 15 | 16 | @command("CreateNewJPAEntity", nargs="*") 17 | def create_new_jpa_entity(self, args: List[str]) -> None: 18 | self.logging.reset_log_file() 19 | if len(args) > 1: 20 | error_msg = "Only one arg is allowed" 21 | self.logging.log(error_msg, LogLevel.ERROR) 22 | raise ValueError(error_msg) 23 | self.debug = True if "debug" in args else False 24 | found_entities = [ 25 | e 26 | for e in self.common_utils.get_all_java_files_data(True) 27 | if e.is_jpa_entity or e.is_mapped_superclass 28 | ] 29 | parent_entities = [ 30 | { 31 | "name": f"{v.file_name} ({v.package_path})", 32 | "id": f"{str(v.path)}", 33 | "type": f"{v.file_name}", 34 | "package_path": f"{v.package_path}", 35 | } 36 | for v in found_entities 37 | ] 38 | root_package_path = str(self.path_utils.get_spring_root_package_path(True)) 39 | self.nvim.exec_lua( 40 | self.common_utils.read_ui_file_as_string("create_entity.lua"), 41 | ( 42 | self.ui_path, 43 | parent_entities, 44 | root_package_path, 45 | ), 46 | ) 47 | 48 | @function("CreateNewJpaEntityCallback") 49 | def many_to_one_callback(self, args: List[Dict]): 50 | converted_args = CreateEntityArgs(**args[0]) 51 | if self.debug: 52 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 53 | self.entity_creation_utils.create_new_entity( 54 | args=converted_args, 55 | debug=self.debug, 56 | ) 57 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_one_to_one_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.mapping_type import MappingType 4 | from custom_types.cascade_type import CascadeType 5 | from custom_types.other import Other 6 | 7 | 8 | @dataclass 9 | class CreateOneToOneRelArgs: 10 | inverse_field_type: str 11 | mapping_type: InitVar[Optional[str]] = None 12 | mapping_type_enum: Optional[MappingType] = field(init=False) 13 | owning_side_cascades: InitVar[Optional[List[str]]] = None 14 | owning_side_cascades_enum: List[CascadeType] = field( 15 | init=False, default_factory=list 16 | ) 17 | inverse_side_cascades: InitVar[Optional[List[str]]] = None 18 | inverse_side_cascades_enum: List[CascadeType] = field( 19 | init=False, default_factory=list 20 | ) 21 | owning_side_other: InitVar[Optional[List[str]]] = None 22 | owning_side_other_enum: List[Other] = field(init=False, default_factory=list) 23 | inverse_side_other: InitVar[Optional[List[str]]] = None 24 | inverse_side_other_enum: List[Other] = field(init=False, default_factory=list) 25 | 26 | def __post_init__( 27 | self, 28 | mapping_type: Optional[str], 29 | owning_side_cascades: Optional[List[str]], 30 | inverse_side_cascades: Optional[List[str]], 31 | owning_side_other: Optional[List[str]], 32 | inverse_side_other: Optional[List[str]], 33 | ): 34 | self.mapping_type_enum = ( 35 | MappingType.from_value(mapping_type) if mapping_type else None 36 | ) 37 | owning_side_cascades = owning_side_cascades or [] 38 | self.owning_side_cascades_enum = [ 39 | CascadeType.from_value(value) for value in owning_side_cascades 40 | ] 41 | inverse_side_cascades = inverse_side_cascades or [] 42 | self.inverse_side_cascades_enum = [ 43 | CascadeType.from_value(value) for value in inverse_side_cascades 44 | ] 45 | owning_side_other = owning_side_other or [] 46 | self.owning_side_other_enum = [ 47 | Other.from_value(value) for value in owning_side_other 48 | ] 49 | inverse_side_other = inverse_side_other or [] 50 | self.inverse_side_other_enum = [ 51 | Other.from_value(value) for value in inverse_side_other 52 | ] 53 | -------------------------------------------------------------------------------- /rplugin/python3/ui/create_java_file.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local select_one = require("select_one") 6 | 7 | local n = require("nui-components") 8 | 9 | local renderer = n.create_renderer({ 10 | width = 65, 11 | height = 15, 12 | }) 13 | 14 | local signal = n.create_signal({ 15 | file_name = "NewFile", 16 | file_type = "class", 17 | package_path = args[2], 18 | }) 19 | 20 | local function render_main_title() 21 | return n.rows( 22 | { flex = 0 }, 23 | n.paragraph({ 24 | lines = { 25 | n.line(n.text("New Java file", "String")), 26 | }, 27 | align = "center", 28 | is_focusable = false, 29 | }) 30 | ) 31 | end 32 | 33 | local function render_text_input_component(title, signal_key, signal_hidden, autofocus) 34 | return n.text_input({ 35 | size = 1, 36 | autofocus = autofocus or false, 37 | value = signal[signal_key], 38 | border_label = title, 39 | on_change = function(value, _) 40 | signal[signal_key] = value 41 | end, 42 | hidden = signal[signal_hidden] or false, 43 | }) 44 | end 45 | 46 | local function render_file_type_component(_signal) 47 | local data = { 48 | n.node({ text = "Class", is_done = true, id = "class" }), 49 | n.node({ text = "Interface", is_done = false, id = "interface" }), 50 | n.node({ text = "Record", is_done = false, id = "record" }), 51 | n.node({ text = "Enum", is_done = false, id = "enum" }), 52 | n.node({ text = "Annotation", is_done = false, id = "annotation" }), 53 | } 54 | return select_one.render_component(nil, "File type", data, "file_type", _signal) 55 | end 56 | 57 | local function render_confirm_button() 58 | return n.button({ 59 | flex = 1, 60 | label = "Confirm", 61 | align = "center", 62 | global_press_key = "", 63 | padding = { top = 1 }, 64 | on_press = function() 65 | local result = { 66 | file_name = signal.file_name:get_value(), 67 | file_type = signal.file_type:get_value(), 68 | package_path = signal.package_path:get_value(), 69 | } 70 | vim.call("CreateNewJavaFileCallback", result) 71 | renderer:close() 72 | end, 73 | hidden = signal.confirm_btn_hidden, 74 | }) 75 | end 76 | 77 | local function render_component() 78 | return n.rows( 79 | { flex = 0 }, 80 | render_main_title(), 81 | n.gap(1), 82 | 83 | render_text_input_component("File name", "file_name", nil, true), 84 | render_text_input_component("Package path", "package_path", nil, false), 85 | render_file_type_component(signal), 86 | render_confirm_button() 87 | ) 88 | end 89 | 90 | renderer:render(render_component()) 91 | -------------------------------------------------------------------------------- /rplugin/python3/ui/select_many.lua: -------------------------------------------------------------------------------- 1 | local n = require("nui-components") 2 | 3 | -- @param t1 table: The table to be extended. 4 | -- @param t2 table: The table whose elements are appended to t1. 5 | -- @return table: The extended t1 table. 6 | local function extend_array(t1, t2) 7 | for _, v in ipairs(t2) do 8 | table.insert(t1, v) 9 | end 10 | return t1 11 | end 12 | -- 13 | -- @param size number|nil: The size of the tree (optional). 14 | -- @param label string: The label for the tree border. 15 | -- @param data table: A list of nodes for the tree, where each node is a table. 16 | -- @param signal table: A table that stores selected node states. 17 | -- @param signal_key string: The key in the `signal` table to store selected nodes. 18 | -- @param enable_all_option boolean: If true, enables an "All" option in the tree. 19 | -- @param on_select_callback function|nil: A callback function triggered on node selection (optional). 20 | -- @return table: The rendered tree component. 21 | local function render_component(size, label, data, signal, signal_key, enable_all_option, on_select_callback) 22 | local to_add = {} 23 | return n.tree({ 24 | size = size or #data, 25 | border_label = label, 26 | data = data, 27 | on_select = function(selected_node, component) 28 | local tree = component:get_tree() 29 | local all_enable = enable_all_option or false 30 | if all_enable and selected_node.text == "All" then 31 | local all_done = not selected_node.is_done 32 | for _, node in ipairs(data) do 33 | node.is_done = all_done 34 | end 35 | signal[signal_key] = all_done and vim.tbl_map(function(node) 36 | return node.id 37 | end, data) or {} 38 | else 39 | local done = not selected_node.is_done 40 | selected_node.is_done = done 41 | if done then 42 | table.insert(to_add, selected_node.id) 43 | signal[signal_key] = extend_array(to_add, signal[signal_key]) 44 | else 45 | to_add = vim.tbl_filter(function(value) 46 | return value ~= selected_node.id 47 | end, to_add) 48 | signal[signal_key] = extend_array(to_add, signal[signal_key]) 49 | end 50 | if all_enable then 51 | local all_checked = true 52 | for i = 2, #data do 53 | if not data[i].is_done then 54 | all_checked = false 55 | break 56 | end 57 | end 58 | data[1].is_done = all_checked 59 | end 60 | end 61 | if on_select_callback then 62 | on_select_callback(selected_node, signal) 63 | end 64 | tree:render() 65 | end, 66 | prepare_node = function(node, line, _) 67 | if node.is_done then 68 | line:append("☑", "String") 69 | else 70 | line:append("◻", "Comment") 71 | end 72 | line:append(" ") 73 | line:append(node.text) 74 | return line 75 | end, 76 | }) 77 | end 78 | 79 | return { 80 | render_component = render_component, 81 | } 82 | -------------------------------------------------------------------------------- /rplugin/python3/custom_types/create_many_to_one_args.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, InitVar 2 | from typing import List, Optional 3 | from custom_types.fetch_type import FetchType 4 | from custom_types.collection_type import CollectionType 5 | from custom_types.mapping_type import MappingType 6 | from custom_types.cascade_type import CascadeType 7 | from custom_types.other import Other 8 | 9 | 10 | @dataclass 11 | class CreateManyToOneRelArgs: 12 | inverse_field_type: str 13 | fetch_type: InitVar[str] 14 | fetch_type_enum: FetchType = field(init=False) 15 | collection_type: InitVar[str] 16 | collection_type_enum: CollectionType = field(init=False) 17 | mapping_type: InitVar[Optional[str]] = None 18 | mapping_type_enum: Optional[MappingType] = field(init=False) 19 | owning_side_cascades: InitVar[Optional[List[str]]] = None 20 | owning_side_cascades_enum: List[CascadeType] = field( 21 | init=False, default_factory=list 22 | ) 23 | inverse_side_cascades: InitVar[Optional[List[str]]] = None 24 | inverse_side_cascades_enum: List[CascadeType] = field( 25 | init=False, default_factory=list 26 | ) 27 | owning_side_other: InitVar[Optional[List[str]]] = None 28 | owning_side_other_enum: List[Other] = field(init=False, default_factory=list) 29 | inverse_side_other: InitVar[Optional[List[str]]] = None 30 | inverse_side_other_enum: List[Other] = field(init=False, default_factory=list) 31 | 32 | def __post_init__( 33 | self, 34 | fetch_type: str, 35 | collection_type: str, 36 | mapping_type: Optional[str], 37 | owning_side_cascades: Optional[List[str]], 38 | inverse_side_cascades: Optional[List[str]], 39 | owning_side_other: Optional[List[str]], 40 | inverse_side_other: Optional[List[str]], 41 | ): 42 | self.fetch_type_enum = FetchType.from_value(fetch_type) 43 | self.collection_type_enum = CollectionType.from_value(collection_type) 44 | self.mapping_type_enum = ( 45 | MappingType.from_value(mapping_type) if mapping_type else None 46 | ) 47 | owning_side_cascades = owning_side_cascades or [] 48 | self.owning_side_cascades_enum = [ 49 | CascadeType.from_value(value) for value in owning_side_cascades 50 | ] 51 | inverse_side_cascades = inverse_side_cascades or [] 52 | self.inverse_side_cascades_enum = [ 53 | CascadeType.from_value(value) for value in inverse_side_cascades 54 | ] 55 | owning_side_other = owning_side_other or [] 56 | self.owning_side_other_enum = [ 57 | Other.from_value(value) for value in owning_side_other 58 | ] 59 | inverse_side_other = inverse_side_other or [] 60 | self.inverse_side_other_enum = [ 61 | Other.from_value(value) for value in inverse_side_other 62 | ] 63 | -------------------------------------------------------------------------------- /rplugin/python3/utils/logging.py: -------------------------------------------------------------------------------- 1 | from logging import log as _log, basicConfig, DEBUG 2 | from pathlib import Path 3 | from typing import List, Optional 4 | from inspect import stack 5 | 6 | from pynvim.api import Nvim 7 | 8 | from custom_types.log_level import LogLevel 9 | 10 | 11 | class Logging: 12 | def __init__(self, nvim: Nvim): 13 | self.nvim = nvim 14 | self.file_path = Path(__file__).resolve() 15 | self.plugin_path = Path( 16 | *self.file_path.parts[: self.file_path.parts.index("nvim-javagenie") + 1] 17 | ) 18 | self.log_file_path = self.plugin_path.joinpath("logging.log") 19 | if not self.plugin_path.exists(): 20 | raise FileNotFoundError 21 | basicConfig( 22 | filename=self.plugin_path.joinpath("logging.log"), 23 | level=DEBUG, 24 | format="[%(asctime)s - %(name)s - %(levelname)s] - %(message)s", 25 | datefmt="%Y-%m-%d %H:%M:%S", 26 | ) 27 | self.last_call_stack: Optional[str] = None 28 | 29 | @staticmethod 30 | def get_caller_params(): 31 | call_stack = stack() 32 | caller_frame = call_stack[2].frame 33 | return caller_frame.f_locals 34 | 35 | def build_call_stack(self) -> str: 36 | call_stack: list[str] = [] 37 | for i, s in enumerate(stack()): 38 | class_name = s[0].f_locals["self"].__class__.__name__ 39 | method_name = s[0].f_code.co_name 40 | if class_name == "Host": 41 | break 42 | if i == 0: 43 | continue 44 | if class_name != "Logging" and method_name != "log": 45 | call_stack.append(method_name) 46 | call_stack.append(class_name) 47 | return ":".join(reversed(call_stack)) 48 | 49 | def reset_log_file(self) -> None: 50 | if self.log_file_path.exists() and self.log_file_path.is_file(): 51 | self.log_file_path.write_text("") 52 | self.last_call_stack = None 53 | 54 | def log( 55 | self, 56 | msg: str | List[str], 57 | level: LogLevel, 58 | ) -> None: 59 | level_int: int 60 | match level: 61 | case LogLevel.INFO: 62 | level_int = 20 63 | case LogLevel.CRITICAL: 64 | level_int = 50 65 | case LogLevel.ERROR: 66 | level_int = 40 67 | case LogLevel.WARN: 68 | level_int = 30 69 | case _: 70 | level_int = 10 71 | if isinstance(msg, list): 72 | msg = "\n".join(msg) 73 | log_msg = "" 74 | call_stack = self.build_call_stack() 75 | if call_stack != self.last_call_stack: 76 | log_msg += f"[{call_stack}]:\nParams:\n" 77 | params = self.get_caller_params() 78 | if params is not None: 79 | for k, v in params.items(): 80 | log_msg += f"{k}: {v}\n" 81 | log_msg += "\n" 82 | log_msg += msg 83 | _log(level_int, log_msg) 84 | self.last_call_stack = call_stack 85 | 86 | def echomsg(self, msg: str) -> None: 87 | self.nvim.command(f"echomsg '{msg}'") 88 | -------------------------------------------------------------------------------- /rplugin/python3/utils/path_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from custom_types.log_level import LogLevel 4 | from utils.treesitter_utils import TreesitterUtils 5 | from utils.logging import Logging 6 | from shutil import which 7 | 8 | 9 | class PathUtils: 10 | def __init__(self, cwd: Path, treesitter_utils: TreesitterUtils, logging: Logging): 11 | self.cwd: Path = cwd 12 | self.logging: Logging = logging 13 | self.treesitter_utils: TreesitterUtils = treesitter_utils 14 | self.root_files = [ 15 | "pom.xml", 16 | "build.gradle", 17 | "build.gradle.kts", 18 | "settings.gradle.kts", 19 | "settings.gradle", 20 | ] 21 | 22 | def get_java_executable_path(self) -> Path: 23 | java_path = which("java") 24 | if java_path: 25 | return Path(java_path) 26 | error_msg = "Java executable not found in PATH" 27 | self.logging.log(error_msg, LogLevel.CRITICAL) 28 | raise FileNotFoundError("Java executable not found in PATH.") 29 | 30 | def get_project_root_path(self) -> Path: 31 | cwd = Path(self.cwd) 32 | while cwd != cwd.root: 33 | if any((cwd / root_file).exists() for root_file in self.root_files): 34 | return Path(cwd.resolve()) 35 | cwd = cwd.parent 36 | error_msg = "Root path not found" 37 | self.logging.log( 38 | error_msg, 39 | LogLevel.CRITICAL, 40 | ) 41 | raise FileNotFoundError(error_msg) 42 | 43 | def get_spring_main_class_path(self) -> Path: 44 | root_path = self.get_project_root_path() 45 | for p in root_path.rglob("*.java"): 46 | buffer_tree = self.treesitter_utils.convert_path_to_tree(p) 47 | buffer_is_main_class = ( 48 | self.treesitter_utils.buffer_public_class_has_annotation( 49 | buffer_tree, "SpringBootApplication", False 50 | ) 51 | ) 52 | if buffer_is_main_class: 53 | return p.resolve() 54 | error_msg = "Main class path not found" 55 | self.logging.log( 56 | error_msg, 57 | LogLevel.CRITICAL, 58 | ) 59 | raise FileNotFoundError(error_msg) 60 | 61 | def get_spring_root_package_path(self, debug: bool = False) -> str: 62 | main_dir_name = "main" 63 | full_path = self.get_spring_main_class_path() 64 | path_parts = Path(full_path).parts 65 | try: 66 | main_dir_index = path_parts.index(main_dir_name) 67 | except ValueError: 68 | error_msg = f"Couldn't find {main_dir_name} in the path" 69 | self.logging.log( 70 | error_msg, 71 | LogLevel.CRITICAL, 72 | ) 73 | raise ValueError(error_msg) 74 | package_path = ".".join(path_parts[main_dir_index + 2 : -1]) 75 | if debug: 76 | self.logging.log( 77 | [ 78 | f"main dir name: {main_dir_name}", 79 | f"full path: {str(full_path)}", 80 | f"package path: {package_path}", 81 | ], 82 | LogLevel.CRITICAL, 83 | ) 84 | return package_path 85 | -------------------------------------------------------------------------------- /rplugin/python3/utils/java_file_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from custom_types.log_level import LogLevel 4 | from custom_types.create_java_file_args import CreateJavaFileArgs 5 | from custom_types.java_file_type import JavaFileType 6 | from utils.path_utils import PathUtils 7 | from utils.common_utils import CommonUtils 8 | from utils.treesitter_utils import TreesitterUtils 9 | from utils.logging import Logging 10 | 11 | 12 | class JavaFileLib: 13 | def __init__( 14 | self, 15 | nvim, 16 | logging: Logging, 17 | treesitter_utils: TreesitterUtils, 18 | path_utils: PathUtils, 19 | common_utils: CommonUtils, 20 | ): 21 | self.nvim = nvim 22 | self.logging = logging 23 | self.treesitter_utils = treesitter_utils 24 | self.path_utils = path_utils 25 | self.common_utils = common_utils 26 | 27 | def get_boiler_plate( 28 | self, 29 | file_type: JavaFileType, 30 | package_path: str, 31 | file_name: str, 32 | debug: bool = False, 33 | ) -> bytes: 34 | boiler_plates = { 35 | "class": f"""package {package_path};\n\npublic class {file_name} {{\n\n}}""", 36 | "interface": f"""package {package_path};\n\npublic interface {file_name} {{\n\n}}""", 37 | "enum": f"""package {package_path};\n\npublic enum {file_name} {{\n\n}}""", 38 | "record": f"""package {package_path};\n\npublic record {file_name}(\n\n) {{}}""", 39 | "annotation": f"""package {package_path};\n\npublic @interface {file_name} {{\n\n}}""", 40 | } 41 | boiler_plate = boiler_plates.get(file_type.value, "") 42 | if debug: 43 | self.logging.log( 44 | f"Boiler plate: {boiler_plate}", 45 | LogLevel.DEBUG, 46 | ) 47 | return boiler_plate.encode("utf-8") 48 | 49 | def get_file_path( 50 | self, 51 | package_path: str, 52 | file_name: str, 53 | debug: bool = False, 54 | ) -> Path: 55 | main_class_path = self.path_utils.get_spring_main_class_path() 56 | base_path = self.common_utils.get_base_path(main_class_path) 57 | relative_path = self.common_utils.get_relative_path(package_path) 58 | file_path = self.common_utils.construct_file_path( 59 | base_path, relative_path, file_name 60 | ) 61 | if debug: 62 | self.logging.log( 63 | [ 64 | f"Base path: {str(base_path)}", 65 | f"Relative path: {str(relative_path)}", 66 | f"File path: {str(file_path.parent)}", 67 | f"Successfully created: {file_path}", 68 | ], 69 | LogLevel.DEBUG, 70 | ) 71 | return file_path 72 | 73 | def create_java_file( 74 | self, 75 | args: CreateJavaFileArgs, 76 | debug: bool = False, 77 | ) -> None: 78 | boiler_plate = self.get_boiler_plate( 79 | args.file_type_enum, args.package_path, args.file_name, debug 80 | ) 81 | file_path = self.get_file_path(args.package_path, args.file_name, debug) 82 | file_tree = self.treesitter_utils.convert_bytes_to_tree(boiler_plate) 83 | self.treesitter_utils.update_buffer(file_tree, file_path, True) 84 | -------------------------------------------------------------------------------- /rplugin/python3/base.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pynvim.api.nvim import Nvim 4 | 5 | from constants.java_basic_types import JAVA_BASIC_TYPES 6 | from utils.build_helper import BuildHelper 7 | from utils.java_file_utils import JavaFileLib 8 | from utils.common_utils import CommonUtils 9 | from utils.entity_creation_utils import EntityCreationUtils 10 | from utils.entity_field_utils import EntityFieldUtils 11 | from utils.entity_rel_utils import EntityRelationshipUtils 12 | from utils.jpa_repo_utils import JpaRepositoryUtils 13 | from utils.logging import Logging 14 | from utils.path_utils import PathUtils 15 | from utils.treesitter_utils import TreesitterUtils 16 | 17 | 18 | class Base(object): 19 | def __init__(self, nvim: Nvim) -> None: 20 | self.nvim = nvim 21 | self.cwd = Path(self.nvim.funcs.getcwd()).resolve() 22 | self.ui_path = str(Path(__file__).parent.resolve().joinpath("ui")) 23 | self.logging = Logging(self.nvim) 24 | self.java_basic_types = JAVA_BASIC_TYPES 25 | self.treesitter_utils = TreesitterUtils( 26 | nvim=self.nvim, 27 | java_basic_types=self.java_basic_types, 28 | cwd=self.cwd, 29 | logging=self.logging, 30 | ) 31 | self.path_utils = PathUtils( 32 | cwd=self.cwd, treesitter_utils=self.treesitter_utils, logging=self.logging 33 | ) 34 | self.common_utils = CommonUtils( 35 | cwd=self.cwd, 36 | path_utils=self.path_utils, 37 | treesitter_utils=self.treesitter_utils, 38 | logging=self.logging, 39 | ) 40 | self.entity_creation_utils = EntityCreationUtils( 41 | nvim=self.nvim, 42 | treesitter_utils=self.treesitter_utils, 43 | path_utils=self.path_utils, 44 | common_utils=self.common_utils, 45 | logging=self.logging, 46 | ) 47 | self.jpa_repo_utils = JpaRepositoryUtils( 48 | nvim=self.nvim, 49 | java_basic_types=self.java_basic_types, 50 | common_utils=self.common_utils, 51 | treesitter_utils=self.treesitter_utils, 52 | path_utils=self.path_utils, 53 | logging=self.logging, 54 | ) 55 | self.entity_field_utils = EntityFieldUtils( 56 | nvim=self.nvim, 57 | java_basic_types=self.java_basic_types, 58 | treesitter_utils=self.treesitter_utils, 59 | common_utils=self.common_utils, 60 | logging=self.logging, 61 | ) 62 | self.entity_relationship_utils = EntityRelationshipUtils( 63 | nvim=self.nvim, 64 | treesitter_utils=self.treesitter_utils, 65 | path_utils=self.path_utils, 66 | common_utils=self.common_utils, 67 | logging=self.logging, 68 | ) 69 | self.java_file_utils = JavaFileLib( 70 | nvim=self.nvim, 71 | logging=self.logging, 72 | treesitter_utils=self.treesitter_utils, 73 | path_utils=self.path_utils, 74 | common_utils=self.common_utils, 75 | ) 76 | self.build_helper = BuildHelper( 77 | nvim=self.nvim, 78 | cwd=self.cwd, 79 | path_utils=self.path_utils, 80 | treesitter_utils=self.treesitter_utils, 81 | common_utils=self.common_utils, 82 | logging=self.logging, 83 | ) 84 | -------------------------------------------------------------------------------- /lua/nvim_javagenie/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function get_plugin_root() 4 | local str = debug.getinfo(1, "S").source:sub(2) 5 | return str:match("(.*/)lua/") 6 | end 7 | 8 | local plugin_root = get_plugin_root() 9 | local venv_path = plugin_root .. "venv" 10 | 11 | -- Check if Python 3 is available 12 | if not vim.fn.has("python3") then 13 | vim.cmd("echomsg ':python3 is not available, nvim-javagenie will not be loaded.'") 14 | return 15 | end 16 | 17 | -- Create virtual environment if it doesn't exist 18 | if vim.fn.empty(vim.fn.glob(venv_path)) > 0 then 19 | vim.fn.system({ "python3", "-m", "venv", venv_path }) 20 | end 21 | 22 | -- Install requirements if venv exists but requirements are not installed 23 | local pip_path = venv_path .. "/bin/pip" 24 | local requirements_path = plugin_root .. "requirements.txt" 25 | 26 | -- TODO: check before installing? 27 | vim.fn.system({ pip_path, "install", "-r", requirements_path }) 28 | 29 | -- Set Python host program 30 | vim.g.python3_host_prog = vim.fn.expand(venv_path .. "/bin/python") 31 | 32 | vim.schedule(function() 33 | vim.cmd("silent! UpdateRemotePlugins") 34 | end) 35 | 36 | -- Keymaps 37 | vim.api.nvim_set_keymap("n", "cj", "", { noremap = true, silent = true, desc = "Java" }) 38 | vim.api.nvim_set_keymap( 39 | "n", 40 | "cjbb", 41 | ":BuildProject", 42 | { noremap = true, silent = true, desc = "Build project" } 43 | ) 44 | vim.api.nvim_set_keymap( 45 | "n", 46 | "cjbr", 47 | ":BuildAndRunProject", 48 | { noremap = true, silent = true, desc = "Build and run project" } 49 | ) 50 | vim.api.nvim_set_keymap( 51 | "n", 52 | "cjn", 53 | ":CreateNewJavaFile", 54 | { noremap = true, silent = true, desc = "Create new Java file" } 55 | ) 56 | vim.api.nvim_set_keymap( 57 | "n", 58 | "cje", 59 | ":CreateNewJPAEntity", 60 | { noremap = true, silent = true, desc = "Create new JPA Entity" } 61 | ) 62 | vim.api.nvim_set_keymap( 63 | "n", 64 | "cjj", 65 | ":CreateJPARepository", 66 | { noremap = true, silent = true, desc = "Create JPA Repository for this Entity" } 67 | ) 68 | vim.api.nvim_set_keymap("n", "cjf", "", { noremap = true, silent = true, desc = "Entity field creation" }) 69 | vim.api.nvim_set_keymap( 70 | "n", 71 | "cjfi", 72 | ":CreateEntityField id", 73 | { noremap = true, silent = true, desc = "Create new Entity id field" } 74 | ) 75 | vim.api.nvim_set_keymap( 76 | "n", 77 | "cjfb", 78 | ":CreateEntityField basic", 79 | { noremap = true, silent = true, desc = "Create new Entity basic field" } 80 | ) 81 | vim.api.nvim_set_keymap( 82 | "n", 83 | "cjfe", 84 | ":CreateEntityField enum", 85 | { noremap = true, silent = true, desc = "Create Entity enum field" } 86 | ) 87 | vim.api.nvim_set_keymap( 88 | "n", 89 | "cjr", 90 | "", 91 | { noremap = true, silent = true, desc = "Entity relationship creation" } 92 | ) 93 | vim.api.nvim_set_keymap( 94 | "n", 95 | "cjro", 96 | ":CreateEntityRelationship one-to-one", 97 | { noremap = true, silent = true, desc = "Create one-to-one relationship" } 98 | ) 99 | vim.api.nvim_set_keymap( 100 | "n", 101 | "cjrn", 102 | ":CreateEntityRelationship many-to-one", 103 | { noremap = true, silent = true, desc = "Create many-to-one relationship" } 104 | ) 105 | vim.api.nvim_set_keymap( 106 | "n", 107 | "cjrm", 108 | ":CreateEntityRelationship many-to-many", 109 | { noremap = true, silent = true, desc = "Create many-to-many relationship" } 110 | ) 111 | 112 | M.setup = function(_) end 113 | 114 | return M 115 | -------------------------------------------------------------------------------- /rplugin/python3/ui/create_entity.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local renderer = n.create_renderer({ 8 | width = 65, 9 | height = 20, 10 | }) 11 | 12 | local signal = n.create_signal({ 13 | entity_name = "NewEntity", 14 | entity_type = "entity", 15 | package_path = args[3], 16 | parent_entity_type = "entity", 17 | parent_entity_package_path = nil, 18 | parent_entity_path = nil, 19 | parent_entity_hidden = false, 20 | }) 21 | 22 | local function render_main_title() 23 | return n.rows( 24 | { flex = 0 }, 25 | n.paragraph({ 26 | lines = { 27 | n.line(n.text("New Entity", "String")), 28 | }, 29 | align = "center", 30 | is_focusable = false, 31 | }) 32 | ) 33 | end 34 | 35 | local function render_parent_entity_component(_signal, options) 36 | local data = {} 37 | for _, v in ipairs(options) do 38 | table.insert( 39 | data, 40 | n.node({ text = v.name, is_done = false, id = v.id, type = v.type, package_path = v.package_path }) 41 | ) 42 | end 43 | return n.tree({ 44 | size = 6, 45 | border_label = "Parent Entity (optional)", 46 | data = data, 47 | on_select = function(selected_node, component) 48 | local tree = component:get_tree() 49 | for _, node in ipairs(data) do 50 | node.is_done = false 51 | end 52 | selected_node.is_done = true 53 | _signal.parent_entity_type = selected_node.type 54 | _signal.parent_entity_package_path = selected_node.package_path 55 | _signal.parent_entity_path = selected_node.id 56 | tree:render() 57 | end, 58 | prepare_node = function(node, line, _) 59 | if node.is_done then 60 | line:append("◉", "String") 61 | else 62 | line:append("○", "Comment") 63 | end 64 | line:append(" ") 65 | line:append(node.text) 66 | return line 67 | end, 68 | hidden = _signal.parent_entity_hidden, 69 | }) 70 | end 71 | 72 | local function render_entity_type_component(_signal, _data) 73 | return n.tree({ 74 | size = 3, 75 | border_label = "Entity type", 76 | data = _data, 77 | on_select = function(selected_node, component) 78 | local tree = component:get_tree() 79 | for _, node in ipairs(_data) do 80 | node.is_done = false 81 | end 82 | selected_node.is_done = true 83 | _signal.entity_type = selected_node.id 84 | if selected_node.id == "embeddable" then 85 | _signal.parent_entity_hidden = true 86 | else 87 | _signal.parent_entity_hidden = false 88 | end 89 | tree:render() 90 | end, 91 | prepare_node = function(node, line, _) 92 | if node.is_done then 93 | line:append("◉", "String") 94 | else 95 | line:append("○", "Comment") 96 | end 97 | line:append(" ") 98 | line:append(node.text) 99 | return line 100 | end, 101 | }) 102 | end 103 | 104 | local function render_text_input_component(title, signal_key, signal_hidden, autofocus) 105 | return n.text_input({ 106 | size = 1, 107 | autofocus = autofocus or false, 108 | value = signal[signal_key], 109 | border_label = title, 110 | on_change = function(value, _) 111 | signal[signal_key] = value 112 | end, 113 | hidden = signal[signal_hidden] or false, 114 | }) 115 | end 116 | 117 | local function render_confirm_button() 118 | return n.button({ 119 | flex = 1, 120 | label = "Confirm", 121 | align = "center", 122 | global_press_key = "", 123 | padding = { top = 1 }, 124 | on_press = function() 125 | local result = { 126 | entity_name = signal.entity_name:get_value(), 127 | entity_type = signal.entity_type:get_value(), 128 | package_path = signal.package_path:get_value(), 129 | parent_entity_type = signal.parent_entity_type:get_value(), 130 | parent_entity_package_path = signal.parent_entity_package_path:get_value(), 131 | } 132 | vim.call("CreateNewJpaEntityCallback", result) 133 | renderer:close() 134 | end, 135 | hidden = signal.confirm_btn_hidden, 136 | }) 137 | end 138 | 139 | local function render_component() 140 | return n.rows( 141 | { flex = 0 }, 142 | render_main_title(), 143 | n.gap(1), 144 | render_text_input_component("Entity name", "entity_name", nil, true), 145 | render_text_input_component("Package path", "package_path", nil, false), 146 | render_entity_type_component(signal, { 147 | n.node({ text = "Entity", is_done = true, id = "entity" }), 148 | n.node({ text = "Embeddable", is_done = false, id = "embeddable" }), 149 | n.node({ text = "Mapped Superclass", is_done = false, id = "mapped_superclass" }), 150 | }), 151 | render_parent_entity_component(signal, args[2]), 152 | render_confirm_button() 153 | ) 154 | end 155 | 156 | renderer:render(render_component()) 157 | -------------------------------------------------------------------------------- /rplugin/python3/utils/entity_creation_utils.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | from pynvim.api.nvim import Nvim 3 | 4 | from custom_types.entity_type import EntityType 5 | from custom_types.log_level import LogLevel 6 | from custom_types.create_entity_args import CreateEntityArgs 7 | from utils.treesitter_utils import TreesitterUtils 8 | from utils.common_utils import CommonUtils 9 | from utils.path_utils import PathUtils 10 | from utils.logging import Logging 11 | 12 | 13 | class EntityCreationUtils: 14 | def __init__( 15 | self, 16 | nvim: Nvim, 17 | treesitter_utils: TreesitterUtils, 18 | path_utils: PathUtils, 19 | common_utils: CommonUtils, 20 | logging: Logging, 21 | ): 22 | self.nvim = nvim 23 | self.treesitter_utils = treesitter_utils 24 | self.path_utils = path_utils 25 | self.logging = logging 26 | self.common_utils = common_utils 27 | 28 | def generate_new_entity_template( 29 | self, 30 | package_path: str, 31 | entity_name: str, 32 | entity_type: EntityType, 33 | parent_entity_type: Optional[str], 34 | parent_entity_package_path: Optional[str], 35 | debug: bool = False, 36 | ) -> str: 37 | imports_to_add: List[str] = ["jakarta.persistence.Table"] 38 | snaked_entity_name = self.common_utils.convert_to_snake_case(entity_name, debug) 39 | if entity_name == "User": 40 | snaked_entity_name += "_" 41 | template = f"package {package_path};\n\n" 42 | if entity_type == EntityType.ENTITY: 43 | imports_to_add.append("jakarta.persistence.Entity") 44 | template += "@Entity\n" 45 | elif entity_type == EntityType.EMBEDDABLE: 46 | imports_to_add.append("jakarta.persistence.Embeddable") 47 | template += "@Embeddable\n" 48 | else: 49 | imports_to_add.append("jakarta.persistence.MappedSuperclass") 50 | template += "@MappedSuperclass\n" 51 | template += f'@Table(name = "{snaked_entity_name}")\n' 52 | if parent_entity_type and parent_entity_package_path: 53 | imports_to_add.append(parent_entity_package_path + "." + parent_entity_type) 54 | template += f"public class {entity_name} extends {parent_entity_type} {{}}" 55 | else: 56 | template += f"public class {entity_name} {{}}" 57 | if debug: 58 | self.logging.log( 59 | [ 60 | f"Snaked entity name: {snaked_entity_name}", 61 | f"Template:\n{template}", 62 | ], 63 | LogLevel.DEBUG, 64 | ) 65 | self.treesitter_utils.add_to_importing_list(imports_to_add, debug) 66 | return template 67 | 68 | def create_new_entity( 69 | self, 70 | args: CreateEntityArgs, 71 | debug: bool = False, 72 | ): 73 | main_class_path = self.path_utils.get_spring_main_class_path() 74 | base_path = self.common_utils.get_base_path(main_class_path) 75 | relative_path = self.common_utils.get_relative_path(args.package_path) 76 | final_path = self.common_utils.construct_file_path( 77 | base_path=base_path, relative_path=relative_path, file_name=args.entity_name 78 | ) 79 | if final_path.exists(): 80 | error_msg = f"File {str(final_path)} already exists" 81 | self.logging.log(error_msg, LogLevel.ERROR) 82 | template = self.generate_new_entity_template( 83 | package_path=args.package_path, 84 | entity_name=args.entity_name, 85 | entity_type=args.entity_type_enum, 86 | parent_entity_type=args.parent_entity_type, 87 | parent_entity_package_path=args.parent_entity_package_path, 88 | debug=debug, 89 | ) 90 | buffer_tree = self.treesitter_utils.convert_bytes_to_tree(template.encode()) 91 | buffer_tree = self.treesitter_utils.add_imports_to_file_tree(buffer_tree, debug) 92 | self.treesitter_utils.update_buffer( 93 | tree=buffer_tree, buffer_path=final_path, save=True, debug=debug 94 | ) 95 | if debug: 96 | self.logging.log( 97 | [ 98 | f"Main class path: {str(main_class_path)}", 99 | f"Base path: {str(base_path)}", 100 | f"Relative path: {str(relative_path)}", 101 | f"Final path: {str(final_path)}", 102 | f"Template:\n{template}", 103 | f"Final file:\n{buffer_tree.root_node.__repr__()}", 104 | ], 105 | LogLevel.DEBUG, 106 | ) 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo was ported to https://github.com/syntaxpresso/syntaxpresso.nvim. 2 | 3 |
4 | 5 | # Java Genie 6 | 7 | ##### Bringing joy to Java programming on Neovim 8 | 9 | [![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org) 10 | [![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54)](http://www.python.org) 11 | [![Neovim](https://img.shields.io/badge/Neovim%200.9+-blue.svg?style=for-the-badge&logo=neovim)](https://neovim.io) 12 | 13 | Nvim's Java-Genie 14 |
15 | 16 | # Introduction 17 | 18 | **nvim-javagenie** is a Neovim plugin that streamlines Java Persistence API (JPA) development, offering powerful commands and interactive UI components for generating entities, attributes, and relationships directly within Java files, among other functionalities. 19 | 20 | The plugin mainly aims to replicate the advanced functionalities of modern JPA development tools, bringing those capabilities seamlessly to Neovim. This effort seeks to attract Java developers back to Neovim by enabling an efficient and integrated workflow for JPA-centric tasks. It is a part of a series of plugins I'm working on, related to Java and Spring development. 21 | 22 | For ease of development, **nvim-javagenie** utilizes a standalone Tree-sitter instance provided by Python libraries in a virtual environment, rather than relying on Neovim’s built-in Tree-sitter support. 23 | 24 | **It is still under development**. 25 | 26 | I'll be slowly developing it. Feel free to contribute. 27 | 28 | # Dependencies 29 | 30 | The plugin depends on three Python libraries, which are installed in a virtual environment (venv): 31 | 32 | - pynvim 33 | - tree-sitter 34 | - tree-sitter-java 35 | 36 | For the TUI, it relies on **nui-components.nvim**, which also depends on **nui.nvim**. 37 | 38 | **jdtls** should be properly configured on Neovim. 39 | 40 | # Installation 41 | 42 | ## Lazy 43 | 44 | ```lua 45 | { 'andreluisos/nvim-javagenie', 46 | dependencies = { 47 | 'grapp-dev/nui-components.nvim', 48 | 'MunifTanjim/nui.nvim', 49 | } 50 | } 51 | ``` 52 | 53 | ## Packer 54 | 55 | ```lua 56 | use { 57 | 'andreluisos/nvim-javagenie', 58 | requires = { 59 | { 'grapp-dev/nui-components.nvim' }, 60 | { 'MunifTanjim/nui.nvim' }, 61 | } 62 | } 63 | ``` 64 | 65 | # TODO 66 | 67 | Among with bug fixing, I also plan to: 68 | 69 | - [ ] Create DTO for an entity (WIP) 70 | - [ ] Implement UI for JPA repository creation 71 | - [x] Create Java files (enum, interface, annotation, class, record) - ![#f6ba974](https://github.com/andreluisos/nvim-javagenie/commit/f6ba97411af316226dde400be704c1f1a91d96f4) 72 | - [ ] Implement Spring Initializr and project creation/loading interfaces ![WIP](https://github.com/andreluisos/nvim-javagenie/tree/29-implement-project-creation) 73 | - [x] Runner for Java applications - ![#053b0d3](https://github.com/andreluisos/nvim-javagenie/commit/053b0d3e4f6d2c600b13cc04e4876696859074c8) 74 | - [ ] Figure out a way to create tests 75 | - [ ] Implement Entity attribute/relationship editing 76 | - [ ] ![Port to Java?](https://github.com/andreluisos/nvim-jpagenie/issues/9) 77 | 78 | Request new features in the ![**Issues**](https://github.com/andreluisos/nvim-jpagenie/issues) tab and I will check it out. 79 | 80 | **Please**, also ![**report bugs**](https://github.com/andreluisos/nvim-jpagenie/issues) in the ![**Issues**](https://github.com/andreluisos/nvim-jpagenie/issues) tab. It's important. 81 | 82 | # Features Overview 83 | 84 | The plugin includes a range of features, each powered by dedicated Tree-sitter queries, to streamline common JPA tasks. These functionalities are aimed at giving Java developers a robust development experience within Neovim: 85 | 86 | ## Create a JPA Entity 87 | 88 | ![JPA Entity creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_entity.gif) 89 | 90 | ## Easily add Entity attributes 91 | 92 | - ID attributes 93 | - Basic type attributes 94 | - Enum type attributes 95 | 96 | ![Entity ID attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_id_attribute.gif) 97 | ![Entity basic attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_basic_attribute.gif) 98 | ![Entity enum attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_enum_attribute.gif) 99 | 100 | ## Quickly create Entity relationships 101 | 102 | - Many-to-one relationships 103 | - One-to-one relationships 104 | - Many-to-one relationships with automatic equals() and hashCode() method generation 105 | 106 | ![Entity ID attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_many_to_one.gif) 107 | ![Entity basic attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_one_to_one.gif) 108 | ![Entity enum attribute creation](https://github.com/andreluisos/nvim-jpagenie/blob/media/create_many_to_many.gif) 109 | -------------------------------------------------------------------------------- /rplugin/python3/ui/enum_field.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local select_many = require("select_many") 8 | 9 | local auto_field_name = require("utils.auto_field_name").auto_field_name 10 | 11 | local renderer = n.create_renderer({ 12 | width = 65, 13 | height = 20, 14 | }) 15 | 16 | local signal = n.create_signal({ 17 | field_path = nil, 18 | field_type = nil, 19 | field_name = "", 20 | field_package_path = nil, 21 | enum_type = "ORDINAL", 22 | field_length = "255", 23 | field_length_hidden = true, 24 | other = {}, 25 | }) 26 | 27 | local function render_main_title() 28 | return n.rows( 29 | { flex = 0 }, 30 | n.paragraph({ 31 | lines = { 32 | n.line(n.text("New enum type attribute", "String")), 33 | }, 34 | align = "center", 35 | is_focusable = false, 36 | }) 37 | ) 38 | end 39 | 40 | local function render_field_type_component(_signal, options) 41 | local data = {} 42 | for _, v in ipairs(options) do 43 | table.insert( 44 | data, 45 | n.node({ text = v.name, type = v.type, package_path = v.package_path, is_done = false, id = v.id }) 46 | ) 47 | end 48 | return n.tree({ 49 | autofocus = true, 50 | size = #data, 51 | border_label = "Type", 52 | data = data, 53 | on_select = function(selected_node, component) 54 | local tree = component:get_tree() 55 | for _, node in ipairs(data) do 56 | node.is_done = false 57 | end 58 | selected_node.is_done = true 59 | _signal["field_path"] = selected_node.id 60 | _signal["field_type"] = selected_node.type 61 | _signal["field_package_path"] = selected_node.package_path 62 | _signal["field_name"] = auto_field_name(selected_node.type) 63 | tree:render() 64 | end, 65 | prepare_node = function(node, line, _) 66 | if node.is_done then 67 | line:append("◉", "String") 68 | else 69 | line:append("○", "Comment") 70 | end 71 | line:append(" ") 72 | line:append(node.text) 73 | return line 74 | end, 75 | }) 76 | end 77 | 78 | local function render_other_component(_signal) 79 | local data = { 80 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 81 | n.node({ text = "Unique", is_done = false, id = "unique" }), 82 | } 83 | return select_many.render_component(nil, "Other", data, _signal, "other") 84 | end 85 | 86 | local function render_custom_select_one_component(_signal, _data, _title, _signal_key, _signal_hidden_key) 87 | return n.tree({ 88 | autofocus = false, 89 | size = #_data, 90 | border_label = _title, 91 | data = _data, 92 | on_select = function(selected_node, component) 93 | local tree = component:get_tree() 94 | for _, node in ipairs(_data) do 95 | node.is_done = false 96 | end 97 | selected_node.is_done = true 98 | _signal[_signal_key] = selected_node.id 99 | if selected_node.id == "STRING" then 100 | _signal[_signal_hidden_key] = false 101 | else 102 | _signal[_signal_hidden_key] = true 103 | end 104 | tree:render() 105 | end, 106 | prepare_node = function(node, line, _) 107 | if node.is_done then 108 | line:append("◉", "String") 109 | else 110 | line:append("○", "Comment") 111 | end 112 | line:append(" ") 113 | line:append(node.text) 114 | return line 115 | end, 116 | }) 117 | end 118 | 119 | local function render_text_input_component(title, signal_key, signal_hidden, size) 120 | return n.text_input({ 121 | flex = 1, 122 | size = size or 0, 123 | value = signal[signal_key], 124 | border_label = title, 125 | on_change = function(value, _) 126 | signal[signal_key] = value 127 | end, 128 | hidden = signal[signal_hidden] or false, 129 | }) 130 | end 131 | 132 | local function render_confirm_button() 133 | return n.button({ 134 | flex = 1, 135 | label = "Confirm", 136 | align = "center", 137 | global_press_key = "", 138 | padding = { top = 1 }, 139 | on_press = function() 140 | local result = { 141 | field_path = signal.field_path:get_value(), 142 | field_package_path = signal.field_package_path:get_value(), 143 | field_type = signal.field_type:get_value(), 144 | field_name = signal.field_name:get_value(), 145 | enum_type = signal.enum_type:get_value(), 146 | field_length = signal.field_length:get_value(), 147 | other = signal.other:get_value(), 148 | } 149 | vim.call("CreateEnumEntityFieldCallback", result) 150 | renderer:close() 151 | end, 152 | hidden = signal.confirm_btn_hidden, 153 | }) 154 | end 155 | 156 | local function render_component() 157 | return n.rows( 158 | { flex = 0 }, 159 | render_main_title(), 160 | n.gap(1), 161 | render_field_type_component(signal, args[2]), 162 | render_custom_select_one_component(signal, { 163 | n.node({ text = "ORDINAL", is_done = false, id = "ORDINAL" }), 164 | n.node({ text = "STRING", is_done = false, id = "STRING" }), 165 | }, "Enum type", "enum_type", "field_length_hidden"), 166 | render_text_input_component("Field name", "field_name", false, 1), 167 | render_text_input_component("Field length", "field_length", "field_length_hidden", 1), 168 | render_other_component(signal), 169 | render_confirm_button() 170 | ) 171 | end 172 | 173 | renderer:render(render_component()) 174 | -------------------------------------------------------------------------------- /rplugin/python3/entity_rel_commands.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Dict, List, Literal, Optional 3 | 4 | from pynvim import plugin, command, function 5 | from pynvim.api import Nvim 6 | from tree_sitter import Tree 7 | 8 | from base import Base 9 | from custom_types.java_file_data import JavaFileData 10 | from custom_types.log_level import LogLevel 11 | from custom_types.create_many_to_one_args import CreateManyToOneRelArgs 12 | from custom_types.create_one_to_one_args import CreateOneToOneRelArgs 13 | from custom_types.create_many_to_many_args import CreateManyToManyRelArgs 14 | 15 | 16 | @plugin 17 | class EntityRelationshipCommands(Base): 18 | def __init__(self, nvim: Nvim) -> None: 19 | super().__init__(nvim) 20 | self.all_java_files: List[JavaFileData] = [] 21 | self.owning_side_file_data: Optional[JavaFileData] = None 22 | self.inverse_side_file_data: Optional[JavaFileData] = None 23 | self.ui_file: Literal["many_to_one.lua", "many_to_many.lua", "one_to_one.lua"] 24 | self.debug: bool = False 25 | 26 | def process_command_args(self, args) -> None: 27 | self.logging.reset_log_file() 28 | self.logging.log(args, LogLevel.DEBUG) 29 | if len(args) < 1 or len(args) > 2: 30 | error_msg = "At least one and max 2 arguments allowed" 31 | self.logging.log(error_msg, LogLevel.ERROR) 32 | raise ValueError(error_msg) 33 | match args[0]: 34 | case "many-to-one": 35 | self.ui_file = "many_to_one.lua" 36 | case "many-to-many": 37 | self.ui_file = "many_to_many.lua" 38 | case "one-to-one": 39 | self.ui_file = "one_to_one.lua" 40 | case _: 41 | error_msg = "Unable to get ui file" 42 | self.logging.log(error_msg, LogLevel.ERROR) 43 | raise FileNotFoundError(error_msg) 44 | self.debug = args[1] if len(args) == 2 else False 45 | self.all_java_files = self.common_utils.get_all_java_files_data(self.debug) 46 | 47 | def get_owning_side_file_data( 48 | self, current_buffer_tree: Tree, buffer_path: Path, debug: bool = False 49 | ) -> JavaFileData: 50 | for file in self.all_java_files: 51 | if file.path == buffer_path: 52 | file.tree = current_buffer_tree 53 | return file 54 | error_msg = "Unable to get owning side buffer data" 55 | if debug: 56 | self.logging.log(error_msg, LogLevel.ERROR) 57 | raise FileNotFoundError(error_msg) 58 | 59 | def get_inverse_side_file_data( 60 | self, field_type: str, debug: bool = False 61 | ) -> JavaFileData: 62 | for file in self.all_java_files: 63 | if file.file_name == field_type: 64 | for buf in self.nvim.buffers: 65 | if buf.name and Path(buf.name).resolve() == file.path: 66 | file.tree = self.treesitter_utils.convert_buffer_to_tree(buf) 67 | return file 68 | error_msg = "Unable to get inverse side buffer data" 69 | if debug: 70 | self.logging.log(error_msg, LogLevel.ERROR) 71 | raise FileNotFoundError(error_msg) 72 | 73 | @command("CreateEntityRelationship", nargs="*") 74 | def create_entity_relationship(self, args) -> None: 75 | self.process_command_args(args) 76 | buffer_tree = self.treesitter_utils.convert_buffer_to_tree( 77 | self.nvim.current.buffer 78 | ) 79 | buffer_path = Path(self.nvim.current.buffer.name) 80 | self.owning_side_file_data = self.get_owning_side_file_data( 81 | buffer_tree, buffer_path, self.debug 82 | ) 83 | data = [ 84 | { 85 | "name": f"{v.file_name} ({v.package_path})", 86 | "type": f"{v.file_name}", 87 | "id": f"{v.path}", 88 | } 89 | for v in self.all_java_files 90 | if v.path != buffer_path and v.is_jpa_entity 91 | ] 92 | self.nvim.exec_lua( 93 | self.common_utils.read_ui_file_as_string(self.ui_file), 94 | (self.ui_path, data), 95 | ) 96 | 97 | @function("ManyToOneCallback") 98 | def many_to_one_callback(self, args: List[Dict]): 99 | converted_args = CreateManyToOneRelArgs(**args[0]) 100 | if self.debug: 101 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 102 | self.inverse_side_file_data = self.get_inverse_side_file_data( 103 | args[0]["inverse_field_type"] 104 | ) 105 | if self.owning_side_file_data and self.inverse_side_file_data: 106 | self.entity_relationship_utils.create_many_to_one_relationship_field( 107 | owning_side_file_data=self.owning_side_file_data, 108 | inverse_side_file_data=self.inverse_side_file_data, 109 | args=converted_args, 110 | debug=self.debug, 111 | ) 112 | 113 | @function("OneToOneCallback") 114 | def one_to_one_callback(self, args): 115 | converted_args = CreateOneToOneRelArgs(**args[0]) 116 | if self.debug: 117 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 118 | self.inverse_side_file_data = self.get_inverse_side_file_data( 119 | args[0]["inverse_field_type"] 120 | ) 121 | if self.owning_side_file_data and self.inverse_side_file_data: 122 | self.entity_relationship_utils.create_one_to_one_relationship_field( 123 | owning_side_file_data=self.owning_side_file_data, 124 | inverse_side_file_data=self.inverse_side_file_data, 125 | args=converted_args, 126 | debug=self.debug, 127 | ) 128 | 129 | @function("ManyToManyCallback") 130 | def many_to_many_callback(self, args: List[Dict]): 131 | converted_args = CreateManyToManyRelArgs(**args[0]) 132 | if self.debug: 133 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 134 | self.inverse_side_file_data = self.get_inverse_side_file_data( 135 | args[0]["inverse_field_type"] 136 | ) 137 | if self.owning_side_file_data and self.inverse_side_file_data: 138 | self.entity_relationship_utils.create_many_to_many_relationship_field( 139 | owning_side_file_data=self.owning_side_file_data, 140 | inverse_side_file_data=self.inverse_side_file_data, 141 | args=converted_args, 142 | debug=self.debug, 143 | ) 144 | -------------------------------------------------------------------------------- /rplugin/python3/ui/many_to_many.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local select_many = require("select_many") 8 | 9 | local renderer = n.create_renderer({ 10 | width = 65, 11 | height = 20, 12 | }) 13 | 14 | local signal = n.create_signal({ 15 | confirm_btn_hidden = false, 16 | next_btn_hidden = true, 17 | active_tab = "owning_side", 18 | inverse_field_type = nil, 19 | mapping_type = "unidirectional_join_column", 20 | owning_side_cascades = {}, 21 | inverse_side_cascades = {}, 22 | inverse_side_other = { "equals_hashcode" }, 23 | }) 24 | 25 | local function render_main_title(subtitle) 26 | return n.rows( 27 | { flex = 0 }, 28 | n.paragraph({ 29 | lines = { 30 | n.line(n.text("Create many-to-many relationship", "String")), 31 | }, 32 | align = "center", 33 | is_focusable = false, 34 | }), 35 | n.paragraph({ 36 | lines = { 37 | n.line(n.text(subtitle, "String")), 38 | }, 39 | align = "center", 40 | is_focusable = false, 41 | }) 42 | ) 43 | end 44 | 45 | local function render_field_type_component(_signal, options) 46 | local data = {} 47 | for _, v in ipairs(options) do 48 | table.insert(data, n.node({ text = v.name, type = v.type, is_done = false, id = v.id })) 49 | end 50 | return n.tree({ 51 | size = 6, 52 | border_label = "Inverse Entity", 53 | data = data, 54 | on_select = function(selected_node, component) 55 | local tree = component:get_tree() 56 | for _, node in ipairs(data) do 57 | node.is_done = false 58 | end 59 | selected_node.is_done = true 60 | _signal.inverse_field_type = selected_node.type 61 | tree:render() 62 | end, 63 | prepare_node = function(node, line, _) 64 | if node.is_done then 65 | line:append("◉", "String") 66 | else 67 | line:append("○", "Comment") 68 | end 69 | line:append(" ") 70 | line:append(node.text) 71 | return line 72 | end, 73 | hidden = _signal.parent_entity_hidden, 74 | }) 75 | end 76 | 77 | local function render_cascade_component(_signal, signal_key) 78 | local data = { 79 | n.node({ text = "Merge", is_done = false, id = "merge" }), 80 | n.node({ text = "Persist", is_done = false, id = "persist" }), 81 | n.node({ text = "Refresh", is_done = false, id = "refresh" }), 82 | n.node({ text = "Detach", is_done = false, id = "detach" }), 83 | } 84 | return select_many.render_component(nil, "Cascade type", data, _signal, signal_key, false) 85 | end 86 | 87 | local function render_mapping_component() 88 | local data = { 89 | n.node({ text = "Unidirectional JoinColumn", is_done = true, id = "unidirectional_join_column" }), 90 | n.node({ text = "Bidirectional JoinColumn", is_done = false, id = "bidirectional_join_column" }), 91 | } 92 | return n.tree({ 93 | autofocus = true, 94 | size = 2, 95 | border_label = "Mapping type", 96 | data = data, 97 | on_select = function(selected_node, component) 98 | local tree = component:get_tree() 99 | for _, node in ipairs(data) do 100 | node.is_done = false 101 | end 102 | selected_node.is_done = true 103 | signal["mapping_type"] = selected_node.id 104 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 105 | signal.confirm_btn_hidden = false 106 | signal.next_btn_hidden = true 107 | end 108 | if signal.mapping_type:get_value() == "bidirectional_join_column" then 109 | signal.confirm_btn_hidden = true 110 | signal.next_btn_hidden = false 111 | end 112 | tree:render() 113 | end, 114 | prepare_node = function(node, line, _) 115 | if node.is_done then 116 | line:append("x", "String") 117 | else 118 | line:append("◻", "Comment") 119 | end 120 | line:append(" ") 121 | line:append(node.text) 122 | return line 123 | end, 124 | }) 125 | end 126 | 127 | local function render_inverse_other_component(_signal) 128 | local data = { 129 | n.node({ text = "Generate equals() and hashCode()", is_done = true, id = "equals_hashcode" }), 130 | } 131 | return select_many.render_component(nil, "Other", data, _signal, "inverse_side_other") 132 | end 133 | 134 | local function render_confirm_button() 135 | return n.button({ 136 | flex = 1, 137 | label = "Confirm", 138 | align = "center", 139 | global_press_key = "", 140 | padding = { top = 1 }, 141 | on_press = function() 142 | local result = { 143 | inverse_field_type = signal.inverse_field_type:get_value(), 144 | mapping_type = signal.mapping_type:get_value(), 145 | owning_side_cascades = signal.owning_side_cascades:get_value(), 146 | inverse_side_cascades = signal.inverse_side_cascades:get_value(), 147 | inverse_side_other = signal.inverse_side_other:get_value(), 148 | } 149 | vim.call("ManyToManyCallback", result) 150 | renderer:close() 151 | end, 152 | hidden = signal.confirm_btn_hidden, 153 | }) 154 | end 155 | 156 | local function render_component() 157 | return n.tabs( 158 | { active_tab = signal.active_tab }, 159 | n.tab( 160 | { id = "owning_side" }, 161 | n.rows( 162 | { flex = 0 }, 163 | render_main_title("Owning side"), 164 | n.gap(1), 165 | render_mapping_component(), 166 | render_field_type_component(signal, args[2]), 167 | render_cascade_component(signal, "owning_side_cascades"), 168 | n.button({ 169 | label = "Next", 170 | align = "center", 171 | global_press_key = "", 172 | padding = { top = 1 }, 173 | on_press = function() 174 | signal.active_tab = "inverse_side" 175 | signal.confirm_btn_hidden = false 176 | renderer:set_size({ height = 5 }) 177 | end, 178 | hidden = signal.next_btn_hidden, 179 | }), 180 | render_confirm_button() 181 | ) 182 | ), 183 | n.tab( 184 | { id = "inverse_side" }, 185 | n.rows( 186 | { flex = 0 }, 187 | render_main_title("Inverse side"), 188 | n.gap(1), 189 | render_cascade_component(signal, "inverse_side_cascades"), 190 | render_inverse_other_component(signal), 191 | n.columns( 192 | { flex = 0 }, 193 | n.button({ 194 | flex = 1, 195 | label = "Previous", 196 | align = "center", 197 | global_press_key = "", 198 | padding = { top = 1 }, 199 | on_press = function() 200 | signal.active_tab = "owning_side" 201 | renderer:set_size({ height = 30 }) 202 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 203 | signal.confirm_btn_hidden = false 204 | else 205 | signal.confirm_btn_hidden = true 206 | end 207 | end, 208 | hidden = signal.next_btn_hidden, 209 | }), 210 | render_confirm_button() 211 | ) 212 | ) 213 | ) 214 | ) 215 | end 216 | 217 | renderer:render(render_component()) 218 | -------------------------------------------------------------------------------- /rplugin/python3/entity_field_commands.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Dict, List, Literal, Optional 3 | 4 | from pynvim import plugin, command, function 5 | 6 | from pynvim.api import Nvim 7 | from tree_sitter import Tree 8 | 9 | from base import Base 10 | from custom_types.declaration_type import DeclarationType 11 | from custom_types.log_level import LogLevel 12 | from custom_types.java_file_data import JavaFileData 13 | from custom_types.create_id_field_args import CreateIdEntityFieldArgs 14 | from custom_types.create_basic_field_args import CreateBasicEntityFieldArgs 15 | from custom_types.create_enum_field_args import CreateEnumEntityFieldArgs 16 | 17 | 18 | @plugin 19 | class EntityFieldCommands(Base): 20 | def __init__(self, nvim: Nvim) -> None: 21 | super().__init__(nvim) 22 | self.all_java_files: List[JavaFileData] = [] 23 | self.data: List[Dict[str, str]] = [] 24 | self.buffer_file_data: Optional[JavaFileData] = None 25 | self.ui_file: Literal["basic_field.lua", "id_field.lua", "enum_field.lua"] 26 | self.debug: bool = False 27 | 28 | def process_command_args(self, args: List[str]) -> None: 29 | self.logging.reset_log_file() 30 | self.logging.log(args, LogLevel.DEBUG) 31 | if len(args) < 1 or len(args) > 2: 32 | error_msg = "At least one and max 2 arguments allowed" 33 | self.logging.log(error_msg, LogLevel.ERROR) 34 | raise ValueError(error_msg) 35 | self.debug = True if "debug" in args else False 36 | self.all_java_files = self.common_utils.get_all_java_files_data(self.debug) 37 | match args[0]: 38 | case "basic": 39 | self.ui_file = "basic_field.lua" 40 | self.data = [ 41 | { 42 | "name": f"{v[0]} ({v[1]})", 43 | "id": f"{v[1]}.{v[0]}", 44 | "type": f"{v[0]}", 45 | "package_path": f"{v[1]}", 46 | } 47 | for v in self.java_basic_types 48 | ] 49 | case "id": 50 | self.ui_file = "id_field.lua" 51 | self.data = [ 52 | { 53 | "name": f"{v[0]} ({v[1]})", 54 | "id": f"{v[1]}.{v[0]}", 55 | "type": f"{v[0]}", 56 | "package_path": f"{v[1]}", 57 | } 58 | for v in self.java_basic_types 59 | if v[0] in ["Long", "Integer", "String", "UUID"] 60 | ] 61 | case "enum": 62 | self.ui_file = "enum_field.lua" 63 | all_enum_files = [ 64 | f 65 | for f in self.all_java_files 66 | if f.declaration_type == DeclarationType.ENUM 67 | ] 68 | self.data = [ 69 | { 70 | "name": f"{v.file_name} ({v.package_path})", 71 | "package_path": f"{v.package_path}", 72 | "type": f"{v.file_name}", 73 | "id": f"{v.path}", 74 | } 75 | for v in all_enum_files 76 | ] 77 | case _: 78 | error_msg = "Unable to get ui file" 79 | self.logging.log(error_msg, LogLevel.ERROR) 80 | raise FileNotFoundError(error_msg) 81 | 82 | def get_buffer_file_data( 83 | self, current_buffer_tree: Tree, buffer_path: Path, debug: bool = False 84 | ) -> JavaFileData: 85 | for file in self.all_java_files: 86 | if file.path == buffer_path: 87 | file.tree = current_buffer_tree 88 | return file 89 | error_msg = "Unable to get owning side buffer data" 90 | if debug: 91 | self.logging.log(error_msg, LogLevel.ERROR) 92 | raise FileNotFoundError(error_msg) 93 | 94 | @command("CreateEntityField", nargs="*") 95 | def create_entity_field(self, args) -> None: 96 | self.process_command_args(args) 97 | buffer_tree = self.treesitter_utils.convert_buffer_to_tree( 98 | self.nvim.current.buffer 99 | ) 100 | buffer_path = Path(self.nvim.current.buffer.name) 101 | self.buffer_file_data = self.get_buffer_file_data( 102 | buffer_tree, buffer_path, self.debug 103 | ) 104 | if self.buffer_file_data is None: 105 | error_msg = "Unable to get current buffer's file data" 106 | self.logging.log(error_msg, LogLevel.ERROR) 107 | raise FileNotFoundError(error_msg) 108 | if self.buffer_file_data: 109 | snaked_class_name = self.common_utils.convert_to_snake_case( 110 | self.buffer_file_data.file_name, self.debug 111 | ) 112 | self.nvim.exec_lua( 113 | self.common_utils.read_ui_file_as_string(self.ui_file), 114 | (self.ui_path, self.data, snaked_class_name), 115 | ) 116 | 117 | @function("CreateBasicEntityFieldCallback") 118 | def crease_basic_entity_field_callback(self, args: List[Dict]): 119 | converted_args = CreateBasicEntityFieldArgs(**args[0]) 120 | if self.debug: 121 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 122 | if self.buffer_file_data: 123 | self.entity_field_utils.create_basic_entity_field( 124 | buffer_file_data=self.buffer_file_data, 125 | args=converted_args, 126 | debug=self.debug, 127 | ) 128 | 129 | @function("CreateEnumEntityFieldCallback") 130 | def crease_enum_entity_field_callback(self, args): 131 | converted_args = CreateEnumEntityFieldArgs(**args[0]) 132 | if self.debug: 133 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 134 | if self.buffer_file_data: 135 | self.entity_field_utils.create_enum_entity_field( 136 | buffer_file_data=self.buffer_file_data, 137 | args=converted_args, 138 | debug=self.debug, 139 | ) 140 | 141 | @function("CreateIdEntityFieldCallback") 142 | def crease_id_entity_field_callback(self, args: List[Dict]): 143 | converted_args = CreateIdEntityFieldArgs(**args[0]) 144 | if self.debug: 145 | self.logging.log(f"Converted args: {converted_args}", LogLevel.DEBUG) 146 | if self.buffer_file_data: 147 | self.entity_field_utils.create_id_entity_field( 148 | buffer_file_data=self.buffer_file_data, 149 | args=converted_args, 150 | debug=self.debug, 151 | ) 152 | -------------------------------------------------------------------------------- /rplugin/python3/ui/one_to_one.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local select_one = require("select_one") 8 | 9 | local select_many = require("select_many") 10 | 11 | local renderer = n.create_renderer({ 12 | width = 65, 13 | height = 30, 14 | }) 15 | 16 | local signal = n.create_signal({ 17 | confirm_btn_hidden = false, 18 | next_btn_hidden = true, 19 | active_tab = "owning_side", 20 | inverse_field_type = nil, 21 | mapping_type = "unidirectional_join_column", 22 | owning_side_cascades = {}, 23 | inverse_side_cascades = {}, 24 | owning_side_other = {}, 25 | inverse_side_other = {}, 26 | }) 27 | 28 | local function render_main_title(subtitle) 29 | return n.rows( 30 | { flex = 0 }, 31 | n.paragraph({ 32 | lines = { 33 | n.line(n.text("Create one-to-one relationship", "String")), 34 | }, 35 | align = "center", 36 | is_focusable = false, 37 | }), 38 | n.paragraph({ 39 | lines = { 40 | n.line(n.text(subtitle, "String")), 41 | }, 42 | align = "center", 43 | is_focusable = false, 44 | }) 45 | ) 46 | end 47 | 48 | local function render_field_type_component(_signal, options) 49 | local data = {} 50 | for _, v in ipairs(options) do 51 | table.insert(data, n.node({ text = v.name, type = v.type, is_done = false, id = v.id })) 52 | end 53 | return n.tree({ 54 | size = 6, 55 | border_label = "Inverse Entity", 56 | data = data, 57 | on_select = function(selected_node, component) 58 | local tree = component:get_tree() 59 | for _, node in ipairs(data) do 60 | node.is_done = false 61 | end 62 | selected_node.is_done = true 63 | _signal.inverse_field_type = selected_node.type 64 | tree:render() 65 | end, 66 | prepare_node = function(node, line, _) 67 | if node.is_done then 68 | line:append("◉", "String") 69 | else 70 | line:append("○", "Comment") 71 | end 72 | line:append(" ") 73 | line:append(node.text) 74 | return line 75 | end, 76 | hidden = _signal.parent_entity_hidden, 77 | }) 78 | end 79 | 80 | local function render_cascade_component(_signal, signal_key) 81 | local data = { 82 | n.node({ text = "All", is_done = false, id = "all" }), 83 | n.node({ text = "Merge", is_done = false, id = "merge" }), 84 | n.node({ text = "Persist", is_done = false, id = "persist" }), 85 | n.node({ text = "Remove", is_done = false, id = "remove" }), 86 | n.node({ text = "Refresh", is_done = false, id = "refresh" }), 87 | n.node({ text = "Detach", is_done = false, id = "detach" }), 88 | } 89 | return select_many.render_component(nil, "Cascade type", data, _signal, signal_key, true) 90 | end 91 | 92 | local function render_mapping_component() 93 | local data = { 94 | n.node({ text = "Unidirectional JoinColumn", is_done = true, id = "unidirectional_join_column" }), 95 | n.node({ text = "Bidirectional JoinColumn", is_done = false, id = "bidirectional_join_column" }), 96 | -- TODO: implement mappedBy 97 | } 98 | return n.tree({ 99 | autofocus = true, 100 | size = 2, 101 | border_label = "Mapping type", 102 | data = data, 103 | on_select = function(selected_node, component) 104 | local tree = component:get_tree() 105 | for _, node in ipairs(data) do 106 | node.is_done = false 107 | end 108 | selected_node.is_done = true 109 | signal["mapping_type"] = selected_node.id 110 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 111 | signal.confirm_btn_hidden = false 112 | signal.next_btn_hidden = true 113 | else 114 | signal.confirm_btn_hidden = true 115 | signal.next_btn_hidden = false 116 | end 117 | tree:render() 118 | end, 119 | prepare_node = function(node, line, _) 120 | if node.is_done then 121 | line:append("◉", "String") 122 | else 123 | line:append("○", "Comment") 124 | end 125 | line:append(" ") 126 | line:append(node.text) 127 | return line 128 | end, 129 | }) 130 | end 131 | 132 | local function render_owning_other_component(_signal) 133 | local data = { 134 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 135 | n.node({ text = "Unique", is_done = false, id = "unique" }), 136 | n.node({ text = "Orphan removal", is_done = false, id = "orphan_removal" }), 137 | } 138 | return select_many.render_component(nil, "Other", data, _signal, "owning_side_other") 139 | end 140 | 141 | local function render_inverse_other_component(_signal) 142 | local data = { 143 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 144 | n.node({ text = "Orphan removal", is_done = false, id = "orphan_removal" }), 145 | } 146 | return select_many.render_component(nil, "Other", data, _signal, "inverse_side_other") 147 | end 148 | 149 | local function render_confirm_button() 150 | return n.button({ 151 | flex = 1, 152 | label = "Confirm", 153 | align = "center", 154 | global_press_key = "", 155 | padding = { top = 1 }, 156 | on_press = function() 157 | local result = { 158 | inverse_field_type = signal.inverse_field_type:get_value(), 159 | mapping_type = signal.mapping_type:get_value(), 160 | owning_side_cascades = signal.owning_side_cascades:get_value(), 161 | inverse_side_cascades = signal.inverse_side_cascades:get_value(), 162 | owning_side_other = signal.owning_side_other:get_value(), 163 | inverse_side_other = signal.inverse_side_other:get_value(), 164 | } 165 | vim.call("OneToOneCallback", result) 166 | renderer:close() 167 | end, 168 | hidden = signal.confirm_btn_hidden, 169 | }) 170 | end 171 | 172 | local function render_component() 173 | return n.tabs( 174 | { active_tab = signal.active_tab }, 175 | n.tab( 176 | { id = "owning_side" }, 177 | n.rows( 178 | { flex = 0 }, 179 | render_main_title("Owning side"), 180 | n.gap(1), 181 | render_mapping_component(), 182 | render_field_type_component(signal, args[2]), 183 | render_cascade_component(signal, "owning_side_cascades"), 184 | render_owning_other_component(signal), 185 | n.button({ 186 | label = "Next", 187 | align = "center", 188 | global_press_key = "", 189 | padding = { top = 1 }, 190 | on_press = function() 191 | signal.active_tab = "inverse_side" 192 | signal.confirm_btn_hidden = false 193 | renderer:set_size({ height = 15 }) 194 | end, 195 | hidden = signal.next_btn_hidden, 196 | }), 197 | render_confirm_button() 198 | ) 199 | ), 200 | n.tab( 201 | { id = "inverse_side" }, 202 | n.rows( 203 | { flex = 0 }, 204 | render_main_title("Inverse side"), 205 | n.gap(1), 206 | render_cascade_component(signal, "inverse_side_cascades"), 207 | render_inverse_other_component(signal), 208 | n.columns( 209 | { flex = 0 }, 210 | n.button({ 211 | flex = 1, 212 | label = "Previous", 213 | align = "center", 214 | global_press_key = "", 215 | padding = { top = 1 }, 216 | on_press = function() 217 | signal.active_tab = "owning_side" 218 | renderer:set_size({ height = 30 }) 219 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 220 | signal.confirm_btn_hidden = false 221 | else 222 | signal.confirm_btn_hidden = true 223 | end 224 | end, 225 | hidden = signal.next_btn_hidden, 226 | }), 227 | render_confirm_button() 228 | ) 229 | ) 230 | ) 231 | ) 232 | end 233 | 234 | renderer:render(render_component()) 235 | -------------------------------------------------------------------------------- /rplugin/python3/ui/many_to_one.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local select_one = require("select_one") 8 | 9 | local select_many = require("select_many") 10 | 11 | local renderer = n.create_renderer({ 12 | width = 65, 13 | height = 30, 14 | }) 15 | 16 | local signal = n.create_signal({ 17 | confirm_btn_hidden = false, 18 | next_btn_hidden = true, 19 | active_tab = "owning_side", 20 | inverse_field_type = nil, 21 | fetch_type = "lazy", 22 | collection_type = "set", 23 | mapping_type = "unidirectional_join_column", 24 | owning_side_cascades = {}, 25 | inverse_side_cascades = {}, 26 | owning_side_other = {}, 27 | inverse_side_other = { "orphan_removal" }, 28 | }) 29 | 30 | local function render_main_title(subtitle) 31 | return n.rows( 32 | { flex = 0 }, 33 | n.paragraph({ 34 | lines = { 35 | n.line(n.text("Create many-to-one relationship", "String")), 36 | }, 37 | align = "center", 38 | is_focusable = false, 39 | }), 40 | n.paragraph({ 41 | lines = { 42 | n.line(n.text(subtitle, "String")), 43 | }, 44 | align = "center", 45 | is_focusable = false, 46 | }) 47 | ) 48 | end 49 | 50 | local function render_field_type_component(_signal, options) 51 | local data = {} 52 | for _, v in ipairs(options) do 53 | table.insert(data, n.node({ text = v.name, type = v.type, is_done = false, id = v.id })) 54 | end 55 | return n.tree({ 56 | size = 6, 57 | border_label = "Inverse Entity", 58 | data = data, 59 | on_select = function(selected_node, component) 60 | local tree = component:get_tree() 61 | for _, node in ipairs(data) do 62 | node.is_done = false 63 | end 64 | selected_node.is_done = true 65 | _signal.inverse_field_type = selected_node.type 66 | tree:render() 67 | end, 68 | prepare_node = function(node, line, _) 69 | if node.is_done then 70 | line:append("◉", "String") 71 | else 72 | line:append("○", "Comment") 73 | end 74 | line:append(" ") 75 | line:append(node.text) 76 | return line 77 | end, 78 | hidden = _signal.parent_entity_hidden, 79 | }) 80 | end 81 | 82 | local function render_cascade_component(_signal, signal_key) 83 | local data = { 84 | n.node({ text = "All", is_done = false, id = "all" }), 85 | n.node({ text = "Merge", is_done = false, id = "merge" }), 86 | n.node({ text = "Persist", is_done = false, id = "persist" }), 87 | n.node({ text = "Remove", is_done = false, id = "remove" }), 88 | n.node({ text = "Refresh", is_done = false, id = "refresh" }), 89 | n.node({ text = "Detach", is_done = false, id = "detach" }), 90 | } 91 | return select_many.render_component(nil, "Cascade type", data, _signal, signal_key, true) 92 | end 93 | 94 | local function render_mapping_component() 95 | local data = { 96 | n.node({ text = "Unidirectional JoinColumn", is_done = true, id = "unidirectional_join_column" }), 97 | n.node({ text = "Bidirectional JoinColumn", is_done = false, id = "bidirectional_join_column" }), 98 | } 99 | return n.tree({ 100 | autofocus = true, 101 | size = 2, 102 | border_label = "Mapping type", 103 | data = data, 104 | on_select = function(selected_node, component) 105 | local tree = component:get_tree() 106 | for _, node in ipairs(data) do 107 | node.is_done = false 108 | end 109 | selected_node.is_done = true 110 | signal["mapping_type"] = selected_node.id 111 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 112 | signal.confirm_btn_hidden = false 113 | signal.next_btn_hidden = true 114 | end 115 | if signal.mapping_type:get_value() == "bidirectional_join_column" then 116 | signal.confirm_btn_hidden = true 117 | signal.next_btn_hidden = false 118 | end 119 | tree:render() 120 | end, 121 | prepare_node = function(node, line, _) 122 | if node.is_done then 123 | line:append("x", "String") 124 | else 125 | line:append("◻", "Comment") 126 | end 127 | line:append(" ") 128 | line:append(node.text) 129 | return line 130 | end, 131 | }) 132 | end 133 | 134 | local function render_fetch_component(_signal) 135 | local data = { 136 | n.node({ text = "Lazy", is_done = true, id = "lazy" }), 137 | n.node({ text = "Eager", is_done = false, id = "eager" }), 138 | } 139 | return select_one.render_component(nil, "Fetch type", data, "fetch_type", _signal) 140 | end 141 | 142 | local function render_collection_component(_signal) 143 | local data = { 144 | n.node({ text = "Set", is_done = true, id = "set" }), 145 | n.node({ text = "List", is_done = false, id = "list" }), 146 | n.node({ text = "Collection", is_done = false, id = "collection" }), 147 | } 148 | return select_one.render_component(nil, "Collection type", data, "collection_type", _signal) 149 | end 150 | 151 | local function render_owning_other_component(_signal) 152 | local data = { 153 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 154 | n.node({ text = "Unique", is_done = false, id = "unique" }), 155 | } 156 | return select_many.render_component(nil, "Other", data, _signal, "owning_side_other") 157 | end 158 | 159 | local function render_inverse_other_component(_signal) 160 | local data = { 161 | n.node({ text = "Orphan removal", is_done = true, id = "orphan_removal" }), 162 | } 163 | return select_many.render_component(nil, "Other", data, _signal, "inverse_side_other") 164 | end 165 | 166 | local function render_confirm_button() 167 | return n.button({ 168 | flex = 1, 169 | label = "Confirm", 170 | align = "center", 171 | global_press_key = "", 172 | padding = { top = 1 }, 173 | on_press = function() 174 | local result = { 175 | inverse_field_type = signal.inverse_field_type:get_value(), 176 | fetch_type = signal.fetch_type:get_value(), 177 | collection_type = signal.collection_type:get_value(), 178 | mapping_type = signal.mapping_type:get_value(), 179 | owning_side_cascades = signal.owning_side_cascades:get_value(), 180 | inverse_side_cascades = signal.inverse_side_cascades:get_value(), 181 | owning_side_other = signal.owning_side_other:get_value(), 182 | inverse_side_other = signal.inverse_side_other:get_value(), 183 | } 184 | vim.call("ManyToOneCallback", result) 185 | renderer:close() 186 | end, 187 | hidden = signal.confirm_btn_hidden, 188 | }) 189 | end 190 | 191 | local function render_component() 192 | return n.tabs( 193 | { active_tab = signal.active_tab }, 194 | n.tab( 195 | { id = "owning_side" }, 196 | n.rows( 197 | { flex = 0 }, 198 | render_main_title("Owning side"), 199 | n.gap(1), 200 | render_mapping_component(), 201 | render_field_type_component(signal, args[2]), 202 | render_cascade_component(signal, "owning_side_cascades"), 203 | render_fetch_component(signal), 204 | render_owning_other_component(signal), 205 | n.button({ 206 | label = "Next", 207 | align = "center", 208 | global_press_key = "", 209 | padding = { top = 1 }, 210 | on_press = function() 211 | signal.active_tab = "inverse_side" 212 | signal.confirm_btn_hidden = false 213 | renderer:set_size({ height = 15 }) 214 | end, 215 | hidden = signal.next_btn_hidden, 216 | }), 217 | render_confirm_button() 218 | ) 219 | ), 220 | n.tab( 221 | { id = "inverse_side" }, 222 | n.rows( 223 | { flex = 0 }, 224 | render_main_title("Inverse side"), 225 | n.gap(1), 226 | render_cascade_component(signal, "inverse_side_cascades"), 227 | render_collection_component(signal), 228 | render_inverse_other_component(signal), 229 | n.columns( 230 | { flex = 0 }, 231 | n.button({ 232 | flex = 1, 233 | label = "Previous", 234 | align = "center", 235 | global_press_key = "", 236 | padding = { top = 1 }, 237 | on_press = function() 238 | signal.active_tab = "owning_side" 239 | renderer:set_size({ height = 30 }) 240 | if signal.mapping_type:get_value() == "unidirectional_join_column" then 241 | signal.confirm_btn_hidden = false 242 | else 243 | signal.confirm_btn_hidden = true 244 | end 245 | end, 246 | hidden = signal.next_btn_hidden, 247 | }), 248 | render_confirm_button() 249 | ) 250 | ) 251 | ) 252 | ) 253 | end 254 | 255 | renderer:render(render_component()) 256 | -------------------------------------------------------------------------------- /rplugin/python3/ui/id_field.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local select_many = require("select_many") 8 | 9 | local renderer = n.create_renderer({ 10 | width = 65, 11 | height = 20, 12 | position = { 13 | row = "30%", 14 | col = "50%", 15 | }, 16 | }) 17 | 18 | local signal = n.create_signal({ 19 | field_package_path = "java.lang", 20 | field_type = "Long", 21 | field_name = "id", 22 | id_generation = "auto", 23 | id_generation_type = "none", 24 | generator_name = args[3] .. "__gen", 25 | sequence_name = args[3] .. "__seq", 26 | initial_value = "1", 27 | allocation_size = "50", 28 | id_generation_type_hidden = true, 29 | generator_name_hidden = true, 30 | sequence_name_hidden = true, 31 | initial_value_hidden = true, 32 | allocation_size_hidden = true, 33 | uuid_type_generation_type_hidden = true, 34 | other = { "mandatory" }, 35 | }) 36 | 37 | local function render_main_title() 38 | return n.rows( 39 | { flex = 0 }, 40 | n.paragraph({ 41 | lines = { 42 | n.line(n.text("New id type attribute", "String")), 43 | }, 44 | align = "center", 45 | is_focusable = false, 46 | }) 47 | ) 48 | end 49 | 50 | local function render_field_type_component(_signal, options) 51 | local data = {} 52 | for _, v in ipairs(options) do 53 | if v.type == "Long" then 54 | table.insert( 55 | data, 56 | n.node({ text = v.name, type = v.type, package_path = v.package_path, is_done = true, id = v.id }) 57 | ) 58 | else 59 | table.insert( 60 | data, 61 | n.node({ text = v.name, type = v.type, package_path = v.package_path, is_done = false, id = v.id }) 62 | ) 63 | end 64 | end 65 | return n.tree({ 66 | autofocus = true, 67 | size = #data, 68 | border_label = "Type", 69 | data = data, 70 | on_select = function(selected_node, component) 71 | local tree = component:get_tree() 72 | for _, node in ipairs(data) do 73 | node.is_done = false 74 | end 75 | selected_node.is_done = true 76 | _signal.field_type = selected_node.type 77 | _signal.field_package_path = selected_node.package_path 78 | if selected_node.type == "UUID" then 79 | _signal.uuid_type_generation_type_hidden = false 80 | _signal.id_generation = "uuid" 81 | else 82 | _signal.uuid_type_generation_type_hidden = true 83 | _signal.id_generation = "auto" 84 | end 85 | tree:render() 86 | end, 87 | prepare_node = function(node, line, _) 88 | if node.is_done then 89 | line:append("◉", "String") 90 | else 91 | line:append("○", "Comment") 92 | end 93 | line:append(" ") 94 | line:append(node.text) 95 | return line 96 | end, 97 | }) 98 | end 99 | 100 | local function render_other_component(_signal) 101 | local data = { 102 | n.node({ text = "Mandatory", is_done = true, id = "mandatory" }), 103 | n.node({ text = "Mutable", is_done = false, id = "mutable" }), 104 | } 105 | return select_many.render_component(nil, "Other", data, _signal, "other") 106 | end 107 | 108 | local function render_uuid_id_generation_component(_signal, _data, _title, _signal_key, _signal_hidden_key) 109 | return n.tree({ 110 | autofocus = false, 111 | size = #_data, 112 | border_label = _title, 113 | data = _data, 114 | on_select = function(selected_node, component) 115 | local tree = component:get_tree() 116 | for _, node in ipairs(_data) do 117 | node.is_done = false 118 | end 119 | selected_node.is_done = true 120 | _signal[_signal_key] = selected_node.id 121 | tree:render() 122 | end, 123 | prepare_node = function(node, line, _) 124 | if node.is_done then 125 | line:append("◉", "String") 126 | else 127 | line:append("○", "Comment") 128 | end 129 | line:append(" ") 130 | line:append(node.text) 131 | return line 132 | end, 133 | hidden = _signal[_signal_hidden_key], 134 | }) 135 | end 136 | 137 | local function render_id_generation_component(_signal, _data, _title, _signal_key, _signal_hidden_key) 138 | return n.tree({ 139 | autofocus = false, 140 | size = #_data, 141 | border_label = _title, 142 | data = _data, 143 | on_select = function(selected_node, component) 144 | local tree = component:get_tree() 145 | for _, node in ipairs(_data) do 146 | node.is_done = false 147 | end 148 | selected_node.is_done = true 149 | _signal[_signal_key] = selected_node.id 150 | if selected_node.id == "sequence" then 151 | _signal.id_generation_type_hidden = false 152 | else 153 | _signal.id_generation_type_hidden = true 154 | end 155 | tree:render() 156 | end, 157 | prepare_node = function(node, line, _) 158 | if node.is_done then 159 | line:append("◉", "String") 160 | else 161 | line:append("○", "Comment") 162 | end 163 | line:append(" ") 164 | line:append(node.text) 165 | return line 166 | end, 167 | hidden = _signal[_signal_hidden_key]:negate(), 168 | }) 169 | end 170 | 171 | local function render_id_generation_type_component(_signal, _data, _title, _signal_key) 172 | return n.tree({ 173 | autofocus = false, 174 | size = #_data, 175 | border_label = _title, 176 | data = _data, 177 | on_select = function(selected_node, component) 178 | local tree = component:get_tree() 179 | for _, node in ipairs(_data) do 180 | node.is_done = false 181 | end 182 | selected_node.is_done = true 183 | _signal[_signal_key] = selected_node.id 184 | if selected_node.id == "entity_exclusive_generation" then 185 | _signal["generator_name_hidden"] = false 186 | _signal["sequence_name_hidden"] = false 187 | _signal["initial_value_hidden"] = false 188 | _signal["allocation_size_hidden"] = false 189 | else 190 | _signal["generator_name_hidden"] = true 191 | _signal["sequence_name_hidden"] = true 192 | _signal["initial_value_hidden"] = true 193 | _signal["allocation_size_hidden"] = true 194 | end 195 | tree:render() 196 | end, 197 | prepare_node = function(node, line, _) 198 | if node.is_done then 199 | line:append("◉", "String") 200 | else 201 | line:append("○", "Comment") 202 | end 203 | line:append(" ") 204 | line:append(node.text) 205 | return line 206 | end, 207 | hidden = _signal.id_generation_type_hidden, 208 | }) 209 | end 210 | 211 | local function render_text_input_component(title, signal_key, signal_hidden, size) 212 | return n.text_input({ 213 | flex = 1, 214 | size = size or 0, 215 | value = signal[signal_key], 216 | border_label = title, 217 | on_change = function(value, _) 218 | signal[signal_key] = value 219 | end, 220 | hidden = signal[signal_hidden] or false, 221 | }) 222 | end 223 | 224 | local function render_confirm_button() 225 | return n.button({ 226 | flex = 1, 227 | label = "Confirm", 228 | align = "center", 229 | global_press_key = "", 230 | padding = { top = 1 }, 231 | on_press = function() 232 | local result = { 233 | field_package_path = signal.field_package_path:get_value(), 234 | field_type = signal.field_type:get_value(), 235 | field_name = signal.field_name:get_value(), 236 | id_generation = signal.id_generation:get_value(), 237 | id_generation_type = signal.id_generation_type:get_value(), 238 | generator_name = signal.generator_name:get_value(), 239 | sequence_name = signal.sequence_name:get_value(), 240 | initial_value = signal.initial_value:get_value(), 241 | allocation_size = signal.allocation_size:get_value(), 242 | other = signal.other:get_value(), 243 | } 244 | vim.call("CreateIdEntityFieldCallback", result) 245 | renderer:close() 246 | end, 247 | hidden = signal.confirm_btn_hidden, 248 | }) 249 | end 250 | 251 | local function render_component() 252 | return n.rows( 253 | { flex = 0 }, 254 | render_main_title(), 255 | n.gap(1), 256 | render_field_type_component(signal, args[2]), 257 | render_text_input_component("Field name", "field_name", false, 1), 258 | render_uuid_id_generation_component(signal, { 259 | n.node({ text = "None", is_done = false, id = "none" }), 260 | n.node({ text = "Auto", is_done = false, id = "auto" }), 261 | n.node({ text = "UUID", is_done = true, id = "uuid" }), 262 | }, "Id generation", "id_generation", "uuid_type_generation_type_hidden"), 263 | render_id_generation_component(signal, { 264 | n.node({ text = "None", is_done = false, id = "none" }), 265 | n.node({ text = "Auto", is_done = true, id = "auto" }), 266 | n.node({ text = "Identity", is_done = false, id = "identity" }), 267 | n.node({ text = "Sequence", is_done = false, id = "sequence" }), 268 | }, "Id generation", "id_generation", "uuid_type_generation_type_hidden"), 269 | render_id_generation_type_component(signal, { 270 | n.node({ text = "None", is_done = true, id = "none" }), 271 | n.node({ text = "Generate exclusively for entity", is_done = false, id = "entity_exclusive_generation" }), 272 | n.node({ text = "Provided by ORM", is_done = false, id = "orm_provided" }), 273 | }, "Generation type", "id_generation_type"), 274 | render_text_input_component("Generator name", "generator_name", "generator_name_hidden", 1), 275 | render_text_input_component("Sequence name", "sequence_name", "sequence_name_hidden", 1), 276 | render_text_input_component("Initial value", "initial_value", "initial_value_hidden", 1), 277 | render_text_input_component("Allocation size", "allocation_size", "allocation_size_hidden", 1), 278 | render_other_component(signal), 279 | render_confirm_button() 280 | ) 281 | end 282 | 283 | renderer:render(render_component()) 284 | -------------------------------------------------------------------------------- /rplugin/python3/utils/common_utils.py: -------------------------------------------------------------------------------- 1 | from re import sub 2 | from subprocess import run, CompletedProcess, CalledProcessError 3 | from typing import List, Optional 4 | 5 | 6 | from custom_types.java_file_data import JavaFileData 7 | from custom_types.declaration_type import DeclarationType 8 | from custom_types.log_level import LogLevel 9 | from utils.treesitter_utils import TreesitterUtils 10 | from utils.path_utils import PathUtils 11 | from pathlib import Path 12 | 13 | from utils.logging import Logging 14 | 15 | 16 | class CommonUtils: 17 | def __init__( 18 | self, 19 | cwd: Path, 20 | path_utils: PathUtils, 21 | treesitter_utils: TreesitterUtils, 22 | logging: Logging, 23 | ) -> None: 24 | self.cwd = cwd 25 | self.logging = logging 26 | self.treesitter_utils = treesitter_utils 27 | self.path_utils = path_utils 28 | 29 | def pluralize_word(self, word: str, debug: bool = False) -> str: 30 | pluralized_word: str 31 | if word.endswith(("s", "sh", "ch", "x", "z")): 32 | pluralized_word = word + "es" 33 | elif word.endswith("y") and word[-2] not in "aeiou": 34 | pluralized_word = word[:-1] + "ies" 35 | elif word.endswith("f"): 36 | pluralized_word = word[:-1] + "ves" 37 | elif word.endswith("fe"): 38 | pluralized_word = word[:-2] + "ves" 39 | else: 40 | pluralized_word = word + "s" 41 | if debug: 42 | self.logging.log(f"Pluralized word: {pluralized_word}", LogLevel.DEBUG) 43 | return pluralized_word 44 | 45 | def convert_to_snake_case(self, text: str, debug: bool = False) -> str: 46 | snaked_field_name = sub(r"(? str: 59 | parent_path = buffer_path.parent 60 | index_to_replace: int 61 | try: 62 | index_to_replace = parent_path.parts.index("main") 63 | except ValueError: 64 | error_msg = "Unable to split parent path" 65 | self.logging.log(error_msg, LogLevel.DEBUG) 66 | raise ValueError(error_msg) 67 | package_path = str(Path(*parent_path.parts[index_to_replace + 2 :])).replace( 68 | "/", "." 69 | ) 70 | if debug: 71 | self.logging.log( 72 | [ 73 | f"Index to replace: {index_to_replace}", 74 | f"Parent path: {str(parent_path)}", 75 | f"Package path: {package_path}", 76 | ], 77 | LogLevel.DEBUG, 78 | ) 79 | return package_path 80 | 81 | def get_java_file_data( 82 | self, file_path: Path, debug: bool = False 83 | ) -> Optional[JavaFileData]: 84 | file_tree = self.treesitter_utils.convert_path_to_tree(file_path) 85 | decl_type_query_param = """ 86 | [ 87 | (class_declaration) 88 | (enum_declaration) 89 | (annotation_type_declaration) 90 | (interface_declaration) 91 | (record_declaration) 92 | ] @decl_type 93 | """ 94 | query_result = self.treesitter_utils.query_match( 95 | file_tree, decl_type_query_param 96 | ) 97 | for result in query_result: 98 | decl_name = result.child_by_field_name("name") 99 | if decl_name and decl_name.text and result.text: 100 | decl_name_str = self.treesitter_utils.convert_bytes_to_string( 101 | decl_name.text 102 | ) 103 | if decl_name_str == file_path.stem: 104 | result_tree = self.treesitter_utils.convert_bytes_to_tree( 105 | result.text 106 | ) 107 | declaration_type: DeclarationType = DeclarationType.CLASS 108 | is_jpa_entity = ( 109 | self.treesitter_utils.buffer_public_class_has_annotation( 110 | tree=result_tree, annotation_name="Entity", debug=debug 111 | ) 112 | ) 113 | is_mapped_superclass = ( 114 | self.treesitter_utils.buffer_public_class_has_annotation( 115 | tree=result_tree, 116 | annotation_name="MappedSuperclass", 117 | debug=debug, 118 | ) 119 | ) 120 | if result.type == "class_declaration": 121 | declaration_type = DeclarationType.CLASS 122 | elif result.type == "enum_declaration": 123 | declaration_type = DeclarationType.ENUM 124 | elif result.type == "interface_declaration": 125 | declaration_type = DeclarationType.INTERFACE 126 | elif result.type == "annotation_declaration": 127 | declaration_type = DeclarationType.ANNOTATION 128 | else: 129 | declaration_type = DeclarationType.RECORD 130 | return JavaFileData( 131 | file_name=decl_name_str, 132 | package_path=self.get_buffer_package_path( 133 | buffer_path=file_path, debug=debug 134 | ), 135 | path=file_path, 136 | tree=file_tree, 137 | declaration_type=declaration_type, 138 | is_jpa_entity=is_jpa_entity, 139 | is_mapped_superclass=is_mapped_superclass, 140 | ) 141 | 142 | def get_all_java_files_data(self, debug: bool = False) -> List[JavaFileData]: 143 | root_path = self.path_utils.get_project_root_path() 144 | files_found: List[JavaFileData] = [] 145 | for p in root_path.rglob("*.java"): 146 | if "main" not in p.parts: 147 | continue 148 | file_data: Optional[JavaFileData] = self.get_java_file_data(p, debug) 149 | if file_data: 150 | files_found.append(file_data) 151 | if debug: 152 | self.logging.log( 153 | [ 154 | f"Root path: {str(root_path)}", 155 | f"Files found:\n{[f.print() for f in files_found]}", 156 | ], 157 | LogLevel.DEBUG, 158 | ) 159 | return files_found 160 | 161 | def generate_field_name( 162 | self, field_type: str, plural: bool = False, debug: bool = False 163 | ) -> str: 164 | field_name = field_type 165 | if plural: 166 | field_name = self.pluralize_word(field_name) 167 | field_name = field_name[0].lower() + field_name[1:] 168 | if debug: 169 | self.logging.log(f"Field name: {field_name}", LogLevel.DEBUG) 170 | return field_name 171 | 172 | def read_ui_file_as_string(self, file_name: str, debug: bool = False) -> str: 173 | file_path = ( 174 | Path(__file__).parent.resolve().parent.joinpath("ui").joinpath(file_name) 175 | ) 176 | with open(file_path, "r") as f: 177 | file_content_str = f.read().strip() 178 | if debug: 179 | self.logging.log( 180 | [ 181 | f"File path: {str(file_path)}", 182 | f"File content:\n{file_content_str}", 183 | ], 184 | LogLevel.DEBUG, 185 | ) 186 | return file_content_str 187 | 188 | def get_base_path(self, main_class_path: Path, debug: bool = False) -> Path: 189 | base_path = main_class_path.parent 190 | if debug: 191 | self.logging.log(f"Base path: {str(base_path)}", LogLevel.DEBUG) 192 | return base_path 193 | 194 | def get_relative_path(self, package_path: str, debug: bool = False) -> Path: 195 | relative_path = Path(package_path.replace(".", "/")) 196 | if debug: 197 | self.logging.log(f"Relative path: {str(relative_path)}", LogLevel.DEBUG) 198 | return relative_path 199 | 200 | def construct_file_path( 201 | self, base_path: Path, relative_path: Path, file_name: str, debug: bool = False 202 | ) -> Path: 203 | try: 204 | index_to_replace = base_path.parts.index("main") 205 | except ValueError: 206 | error_msg = "Unable to parse root directory" 207 | self.logging.log(error_msg, LogLevel.ERROR) 208 | raise ValueError(error_msg) 209 | file_path = ( 210 | Path(*base_path.parts[: index_to_replace + 2]) 211 | / relative_path 212 | / f"{file_name}.java" 213 | ) 214 | if debug: 215 | self.logging.log(f"File path: {str(file_path)}", LogLevel.DEBUG) 216 | return file_path 217 | 218 | def run_subprocess( 219 | self, command: list[str], debug: bool = False 220 | ) -> CompletedProcess: 221 | try: 222 | result = run( 223 | command, 224 | capture_output=True, 225 | text=True, 226 | check=True, 227 | ) 228 | if debug: 229 | self.logging.log(f"Command: {' '.join(command)}", LogLevel.DEBUG) 230 | self.logging.log(f"Output: {result.stdout}", LogLevel.DEBUG) 231 | self.logging.log(f"Error: {result.stderr}", LogLevel.DEBUG) 232 | return result 233 | except CalledProcessError as e: 234 | if debug: 235 | self.logging.log(f"Command failed: {' '.join(command)}", LogLevel.DEBUG) 236 | self.logging.log(f"Output: {e.stdout}", LogLevel.DEBUG) 237 | self.logging.log(f"Error: {e.stderr}", LogLevel.DEBUG) 238 | raise 239 | except Exception as e: 240 | error_msg = f"Unexpected error: {str(e)}" 241 | self.logging.echomsg(error_msg) 242 | self.logging.log(error_msg, LogLevel.ERROR) 243 | raise ValueError(error_msg) 244 | -------------------------------------------------------------------------------- /rplugin/python3/ui/basic_field.lua: -------------------------------------------------------------------------------- 1 | local args = ... 2 | 3 | package.path = package.path .. ";" .. args[1] .. "/?.lua" 4 | 5 | local n = require("nui-components") 6 | 7 | local renderer = n.create_renderer({ 8 | width = 65, 9 | height = 20, 10 | }) 11 | 12 | local signal = n.create_signal({ 13 | field_package_path = "java.lang", 14 | field_type = "String", 15 | field_name = "", 16 | field_length = "255", 17 | other = {}, 18 | field_precision = "19", 19 | field_scale = "2", 20 | field_time_zone_storage = nil, 21 | field_temporal = nil, 22 | field_length_hidden = false, 23 | field_temporal_hidden = true, 24 | field_time_zone_storage_hidden = true, 25 | field_scale_hidden = true, 26 | field_precision_hidden = true, 27 | other_extra_hidden = false, 28 | other_hidden = true, 29 | }) 30 | 31 | local function extend_array(t1, t2) 32 | for _, v in ipairs(t2) do 33 | table.insert(t1, v) 34 | end 35 | return t1 36 | end 37 | 38 | local function render_main_title() 39 | return n.rows( 40 | { flex = 0 }, 41 | n.paragraph({ 42 | lines = { 43 | n.line(n.text("New basic type attribute", "String")), 44 | }, 45 | align = "center", 46 | is_focusable = false, 47 | }) 48 | ) 49 | end 50 | 51 | local function render_field_package_type_component(_signal, options) 52 | local has_field_length = { 53 | "java.lang.String", 54 | "java.net.URL", 55 | "java.util.Locale", 56 | "java.util.Currency", 57 | "java.lang.Class", 58 | "java.lang.Character%[%]", 59 | "char%[%]", 60 | "java.util.TimeZone", 61 | "java.time.ZoneOffset", 62 | } 63 | local has_time_zone_storage = { 64 | "java.time.OffsetDateTime", 65 | "java.time.OffsetTime", 66 | "java.time.ZonedDateTime", 67 | } 68 | local has_temporal = { 69 | "java.util.Date", 70 | "java.util.Calendar", 71 | } 72 | local has_extra_other = { 73 | "java.lang.String", 74 | "java.lang.Byte%[%]", 75 | "byte%[%]", 76 | "char%[%]", 77 | "java.lang.Character%[%]", 78 | "java.sql.Blob", 79 | "java.sql.Clob", 80 | "java.sql.NClob", 81 | } 82 | local data = {} 83 | for _, v in ipairs(options) do 84 | local is_done = false 85 | if v.id == "java.lang.String" then 86 | is_done = true 87 | end 88 | table.insert( 89 | data, 90 | n.node({ text = v.name, package_path = v.package_path, type = v.type, is_done = is_done, id = v.id }) 91 | ) 92 | end 93 | return n.tree({ 94 | autofocus = true, 95 | size = 10, 96 | border_label = "Field type", 97 | data = data, 98 | on_select = function(selected_node, component) 99 | local tree = component:get_tree() 100 | local field_length_hidden = true 101 | local field_precision_hidden = true 102 | local field_scale_hidden = true 103 | local field_time_zone_storage_hidden = true 104 | local field_temporal_hidden = true 105 | local other_extra_hidden = true 106 | local other_hidden = false 107 | for _, node in ipairs(data) do 108 | node.is_done = false 109 | end 110 | selected_node.is_done = true 111 | _signal.field_package_path = selected_node.package_path 112 | _signal.field_type = selected_node.type 113 | for _, element in ipairs(has_field_length) do 114 | if selected_node.id == element then 115 | field_length_hidden = false 116 | end 117 | end 118 | for _, element in ipairs(has_time_zone_storage) do 119 | if selected_node.id == element then 120 | field_time_zone_storage_hidden = false 121 | end 122 | end 123 | for _, element in ipairs(has_temporal) do 124 | if selected_node.id == element then 125 | field_temporal_hidden = false 126 | end 127 | end 128 | for _, element in ipairs(has_extra_other) do 129 | if selected_node.id == element then 130 | other_hidden = true 131 | other_extra_hidden = false 132 | end 133 | end 134 | if selected_node.id == "java.math.BigDecimal" then 135 | field_precision_hidden = false 136 | field_scale_hidden = false 137 | end 138 | _signal.field_length_hidden = field_length_hidden 139 | _signal.field_precision_hidden = field_precision_hidden 140 | _signal.field_scale_hidden = field_scale_hidden 141 | _signal.field_time_zone_storage_hidden = field_time_zone_storage_hidden 142 | _signal.field_temporal_hidden = field_temporal_hidden 143 | _signal.other_hidden = other_hidden 144 | _signal.other_extra_hidden = other_extra_hidden 145 | tree:render() 146 | end, 147 | prepare_node = function(node, line, _) 148 | if node.is_done then 149 | line:append("x", "String") 150 | else 151 | line:append("◻", "Comment") 152 | end 153 | line:append(" ") 154 | line:append(node.text) 155 | return line 156 | end, 157 | }) 158 | end 159 | 160 | local function render_custom_select_one_component(_signal, _data, _title, _signal_key, _signal_hidden_key) 161 | return n.tree({ 162 | autofocus = false, 163 | size = #_data, 164 | border_label = _title, 165 | data = _data, 166 | on_select = function(selected_node, component) 167 | local tree = component:get_tree() 168 | for _, node in ipairs(_data) do 169 | node.is_done = false 170 | end 171 | selected_node.is_done = true 172 | _signal[_signal_key] = selected_node.id 173 | tree:render() 174 | end, 175 | prepare_node = function(node, line, _) 176 | if node.is_done then 177 | line:append("◉", "String") 178 | else 179 | line:append("○", "Comment") 180 | end 181 | line:append(" ") 182 | line:append(node.text) 183 | return line 184 | end, 185 | hidden = _signal[_signal_hidden_key], 186 | }) 187 | end 188 | 189 | local function render_text_input_component(title, signal_key, signal_hidden, size) 190 | return n.text_input({ 191 | flex = 1, 192 | size = size or 0, 193 | value = signal[signal_key], 194 | border_label = title, 195 | on_change = function(value, _) 196 | signal[signal_key] = value 197 | end, 198 | hidden = signal[signal_hidden] or false, 199 | }) 200 | end 201 | 202 | local function render_custom_select_many_component(_signal, _data, _title, _signal_key, _signal_hidden_key) 203 | local to_add = {} 204 | return n.tree({ 205 | size = #_data, 206 | border_label = _title, 207 | data = _data, 208 | on_select = function(selected_node, component) 209 | local tree = component:get_tree() 210 | local all_enable = false 211 | if all_enable and selected_node.text == "All" then 212 | local all_done = not selected_node.is_done 213 | for _, node in ipairs(_data) do 214 | node.is_done = all_done 215 | end 216 | _signal[_signal_key] = all_done and vim.tbl_map(function(node) 217 | return node.id 218 | end, _data) or {} 219 | else 220 | local done = not selected_node.is_done 221 | selected_node.is_done = done 222 | if done then 223 | table.insert(to_add, selected_node.id) 224 | _signal[_signal_key] = extend_array(to_add, _signal[_signal_key]) 225 | else 226 | to_add = vim.tbl_filter(function(value) 227 | return value ~= selected_node.id 228 | end, to_add) 229 | _signal[_signal_key] = extend_array(to_add, _signal[_signal_key]) 230 | end 231 | if all_enable then 232 | local all_checked = true 233 | for i = 2, #_data do 234 | if not _data[i].is_done then 235 | all_checked = false 236 | break 237 | end 238 | end 239 | _data[1].is_done = all_checked 240 | end 241 | end 242 | tree:render() 243 | end, 244 | prepare_node = function(node, line, _) 245 | if node.is_done then 246 | line:append("☑", "String") 247 | else 248 | line:append("◻", "Comment") 249 | end 250 | line:append(" ") 251 | line:append(node.text) 252 | return line 253 | end, 254 | hidden = _signal[_signal_hidden_key], 255 | }) 256 | end 257 | 258 | local function render_confirm_button() 259 | return n.button({ 260 | flex = 1, 261 | label = "Confirm", 262 | align = "center", 263 | global_press_key = "", 264 | padding = { top = 1 }, 265 | on_press = function() 266 | local result = { 267 | field_package_path = signal.field_package_path:get_value(), 268 | field_type = signal.field_type:get_value(), 269 | field_name = signal.field_name:get_value(), 270 | field_length = signal.field_length:get_value(), 271 | field_precision = signal.field_precision:get_value(), 272 | field_scale = signal.field_scale:get_value(), 273 | field_time_zone_storage = signal.field_time_zone_storage:get_value(), 274 | field_temporal = signal.field_temporal:get_value(), 275 | other = signal.other:get_value(), 276 | } 277 | vim.call("CreateBasicEntityFieldCallback", result) 278 | renderer:close() 279 | end, 280 | hidden = signal.confirm_btn_hidden, 281 | }) 282 | end 283 | 284 | local function render_component() 285 | return n.rows( 286 | { flex = 0 }, 287 | render_main_title(), 288 | n.gap(1), 289 | render_field_package_type_component(signal, args[2]), 290 | render_text_input_component("Field name", "field_name", false, 1), 291 | render_text_input_component("Field length", "field_length", "field_length_hidden", 1), 292 | render_custom_select_one_component(signal, { 293 | n.node({ text = "NATIVE", is_done = false, id = "NATIVE" }), 294 | n.node({ text = "NORMALIZE", is_done = false, id = "NORMALIZE" }), 295 | n.node({ text = "NORMALIZE_UTC", is_done = false, id = "NORMALIZE_UTC" }), 296 | n.node({ text = "COLUMN", is_done = false, id = "COLUMN" }), 297 | n.node({ text = "AUTO", is_done = false, id = "AUTO" }), 298 | }, "Time Zone Storage", "field_time_zone_storage", "field_time_zone_storage_hidden"), 299 | render_custom_select_one_component(signal, { 300 | n.node({ text = "DATE", is_done = false, id = "DATE" }), 301 | n.node({ text = "TIME", is_done = false, id = "TIME" }), 302 | n.node({ text = "TIMESTAMP", is_done = false, id = "TIMESTAMP" }), 303 | }, "Temporal", "field_temporal", "field_temporal_hidden"), 304 | n.columns( 305 | { flex = 0, hidden = signal.field_precision_hidden and signal.field_scale_hidden }, 306 | render_text_input_component("Field precision", "field_precision", "field_precision_hidden", 1), 307 | render_text_input_component("Field scale", "field_scale", "field_scale_hidden", 1) 308 | ), 309 | render_custom_select_many_component(signal, { 310 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 311 | n.node({ text = "Unique", is_done = false, id = "unique" }), 312 | }, "Other", "other", "other_hidden"), 313 | render_custom_select_many_component(signal, { 314 | n.node({ text = "Large object", is_done = false, id = "large_object" }), 315 | n.node({ text = "Mandatory", is_done = false, id = "mandatory" }), 316 | n.node({ text = "Unique", is_done = false, id = "unique" }), 317 | }, "Other", "other", "other_extra_hidden"), 318 | render_confirm_button() 319 | ) 320 | end 321 | 322 | renderer:render(render_component()) 323 | -------------------------------------------------------------------------------- /rplugin/python3/utils/jpa_repo_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional 3 | 4 | from pynvim.api.nvim import Nvim 5 | from tree_sitter import Node, Tree 6 | 7 | from custom_types.log_level import LogLevel 8 | from custom_types.declaration_type import DeclarationType 9 | from utils.common_utils import CommonUtils 10 | from utils.path_utils import PathUtils 11 | from utils.treesitter_utils import TreesitterUtils 12 | from utils.logging import Logging 13 | 14 | 15 | class JpaRepositoryUtils: 16 | def __init__( 17 | self, 18 | nvim: Nvim, 19 | java_basic_types: list[tuple], 20 | common_utils: CommonUtils, 21 | treesitter_utils: TreesitterUtils, 22 | path_utils: PathUtils, 23 | logging: Logging, 24 | ): 25 | self.nvim = nvim 26 | self.java_basic_types = java_basic_types 27 | self.common_utils = common_utils 28 | self.treesitter_utils = treesitter_utils 29 | self.path_utils = path_utils 30 | self.logging = logging 31 | 32 | def get_basic_field_type_import_path( 33 | self, field_type: str, debug: bool = False 34 | ) -> Optional[str]: 35 | for type_tuple in self.java_basic_types: 36 | if field_type == type_tuple[0]: 37 | import_path: str = "" 38 | if type_tuple[1] is not None: 39 | import_path = f"{type_tuple[1]}.{type_tuple[0]}" 40 | else: 41 | # For primitive types or when value is None 42 | import_path = field_type[0] 43 | if debug: 44 | self.logging.log( 45 | [f"Field type: {field_type}", f"Import path: {import_path}"], 46 | LogLevel.DEBUG, 47 | ) 48 | return import_path 49 | if debug: 50 | self.logging.log(f"Field type {field_type} not found", LogLevel.DEBUG) 51 | 52 | def generate_jpa_repository_template( 53 | self, class_name: str, package_path: str, id_type: str, debug: bool = False 54 | ) -> Tree: 55 | id_type_import_path = self.get_basic_field_type_import_path(id_type, debug) 56 | boiler_plate = ( 57 | f"package {package_path};\n\n" 58 | f"import org.springframework.data.jpa.repository.JpaRepository;\n\n" 59 | ) 60 | if id_type_import_path: 61 | boiler_plate += f"import {id_type_import_path};\n\n" 62 | boiler_plate += ( 63 | f"public interface {class_name}Repository " 64 | f"extends JpaRepository<{class_name}, {id_type}> {{}}" 65 | ) 66 | if debug: 67 | self.logging.log( 68 | f"Boiler plate:\n{boiler_plate}", 69 | LogLevel.DEBUG, 70 | ) 71 | return self.treesitter_utils.convert_bytes_to_tree(boiler_plate.encode()) 72 | 73 | def check_if_id_field_exists(self, file_tree: Tree, debug: bool = False) -> bool: 74 | id_field_annotation_query = """ 75 | (modifiers 76 | (marker_annotation 77 | name: (identifier) @annotation_name 78 | ) 79 | ) 80 | """ 81 | query_results = self.treesitter_utils.query_match( 82 | file_tree, id_field_annotation_query 83 | ) 84 | id_annotation_found = False 85 | for result in query_results: 86 | if result.text and result.text.decode() == "Id": 87 | id_annotation_found = True 88 | if debug: 89 | self.logging.log( 90 | f"ID annotation found: {id_annotation_found}", 91 | LogLevel.DEBUG, 92 | ) 93 | return id_annotation_found 94 | 95 | def get_superclass_name( 96 | self, file_tree: Tree, debug: bool = False 97 | ) -> Optional[str]: 98 | superclass_name: Optional[str] = None 99 | class_declaration_query = "(class_declaration) @class_decl" 100 | super_class_query = """ 101 | (class_declaration 102 | superclass: (superclass 103 | (type_identifier) @superclass_type)) 104 | """ 105 | query_results = self.treesitter_utils.query_match( 106 | file_tree, class_declaration_query 107 | ) 108 | main_class_node = ( 109 | self.treesitter_utils.get_buffer_public_class_node_from_query_results( 110 | query_results, debug 111 | ) 112 | ) 113 | if main_class_node: 114 | main_class_tree = self.treesitter_utils.convert_node_to_tree( 115 | main_class_node 116 | ) 117 | query_results = self.treesitter_utils.query_match( 118 | main_class_tree, super_class_query 119 | ) 120 | if len(query_results) != 1: 121 | return None 122 | if query_results[0].text: 123 | superclass_name = query_results[0].text.decode() 124 | if debug: 125 | self.logging.log( 126 | f"Superclass name: {superclass_name}", 127 | LogLevel.DEBUG, 128 | ) 129 | return superclass_name 130 | 131 | def find_superclass_file_tree( 132 | self, superclass_name: str, debug: bool = False 133 | ) -> Optional[Tree]: 134 | class_name_query = """ 135 | (class_declaration 136 | name: (identifier) @class_name 137 | ) 138 | """ 139 | root_path = self.path_utils.get_project_root_path() 140 | super_class_tree: Optional[Tree] = None 141 | for p in root_path.rglob("*.java"): 142 | file_tree = self.treesitter_utils.convert_path_to_tree(p) 143 | query_results = self.treesitter_utils.query_match( 144 | file_tree, class_name_query 145 | ) 146 | if len(query_results) == 0: 147 | continue 148 | for result in query_results: 149 | if result.text and result.text.decode() == superclass_name: 150 | return file_tree 151 | if debug: 152 | self.logging.log( 153 | [ 154 | f"Query param: {class_name_query}", 155 | f"Found superclass tree: {super_class_tree.__repr__() if super_class_tree else None}", 156 | ], 157 | LogLevel.DEBUG, 158 | ) 159 | return super_class_tree 160 | 161 | def find_id_field_type(self, file_tree: Tree, debug: bool = False) -> Optional[str]: 162 | field_marker_name_query = """ 163 | (field_declaration 164 | (modifiers 165 | (marker_annotation 166 | name: (identifier) @annotation_name))) 167 | """ 168 | query_results = self.treesitter_utils.query_match( 169 | file_tree, field_marker_name_query 170 | ) 171 | if len(query_results) == 0: 172 | return None 173 | field_declaration: Optional[Node] = None 174 | id_field_type_node: Optional[Node] = None 175 | id_field_type: Optional[str] = None 176 | id_node: Optional[Node] = None 177 | for annotation in query_results: 178 | if annotation.text: 179 | name = self.treesitter_utils.convert_bytes_to_string(annotation.text) 180 | if name == "Id": 181 | id_node = annotation 182 | if id_node is None: 183 | return None 184 | marker_annotation = id_node.parent 185 | if marker_annotation: 186 | modifiers = marker_annotation.parent 187 | if modifiers: 188 | field_declaration = modifiers.parent 189 | if field_declaration: 190 | id_field_type_node = field_declaration.child_by_field_name("type") 191 | if id_field_type_node and id_field_type_node.text: 192 | id_field_type = self.treesitter_utils.convert_bytes_to_string( 193 | id_field_type_node.text 194 | ) 195 | if debug: 196 | self.logging.log(f"Id field type: {id_field_type}", LogLevel.DEBUG) 197 | return id_field_type 198 | 199 | def create_jpa_repository(self, buffer_path: Path, debug: bool = False) -> None: 200 | file_data = self.common_utils.get_java_file_data(buffer_path, debug) 201 | if file_data is None: 202 | error_msg = "Couldn't get file data" 203 | self.logging.log( 204 | error_msg, 205 | LogLevel.DEBUG, 206 | ) 207 | raise ValueError(error_msg) 208 | if ( 209 | not file_data.is_jpa_entity 210 | or file_data.declaration_type != DeclarationType.CLASS 211 | ): 212 | error_msg = "Invalid JPA Entity" 213 | self.logging.log(error_msg, LogLevel.ERROR) 214 | raise ValueError(error_msg) 215 | id_type = self.find_id_field_type(file_data.tree, debug=True) 216 | if not id_type: 217 | error_msg = "Unable to get superclass data" 218 | superclass_name = self.get_superclass_name(file_data.tree) 219 | if not superclass_name: 220 | self.logging.log(error_msg, LogLevel.ERROR) 221 | raise ValueError(error_msg) 222 | superclass_tree = self.find_superclass_file_tree(superclass_name, debug) 223 | if superclass_tree is None: 224 | self.logging.log( 225 | error_msg, 226 | LogLevel.ERROR, 227 | ) 228 | raise ValueError(error_msg) 229 | if not self.check_if_id_field_exists(superclass_tree, debug=debug): 230 | # TODO: Keep checking for superclasses? 231 | error_msg = "Unable to find the Id field on the superclass" 232 | self.logging.log( 233 | error_msg, 234 | LogLevel.ERROR, 235 | ) 236 | raise ValueError(error_msg) 237 | id_type = self.find_id_field_type(superclass_tree, debug=debug) 238 | if id_type is None: 239 | error_msg = "Unable to find get the Id field type" 240 | self.logging.log( 241 | error_msg, 242 | LogLevel.ERROR, 243 | ) 244 | raise ValueError(error_msg) 245 | jpa_repo_tree = self.generate_jpa_repository_template( 246 | class_name=file_data.file_name, 247 | package_path=file_data.package_path, 248 | id_type=id_type, 249 | debug=debug, 250 | ) 251 | jpa_repo_path = buffer_path.parent.joinpath( 252 | f"{file_data.file_name}Repository.java" 253 | ) 254 | self.treesitter_utils.update_buffer( 255 | tree=jpa_repo_tree, 256 | buffer_path=jpa_repo_path, 257 | save=True, 258 | debug=debug, 259 | ) 260 | if debug: 261 | self.logging.log( 262 | f"JPA Repository tree:\n{jpa_repo_tree.__repr__()}\n", 263 | LogLevel.DEBUG, 264 | ) 265 | -------------------------------------------------------------------------------- /rplugin/python3/utils/build_helper.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | from pynvim import Optional 3 | from pynvim.api import Nvim 4 | from custom_types.log_level import LogLevel 5 | from custom_types.project_properties import ProjectProperties 6 | from utils.common_utils import CommonUtils 7 | from utils.treesitter_utils import TreesitterUtils 8 | from utils.path_utils import PathUtils 9 | from pathlib import Path 10 | from platform import system 11 | import subprocess 12 | from utils.logging import Logging 13 | 14 | 15 | class BuildHelper: 16 | def __init__( 17 | self, 18 | nvim: Nvim, 19 | cwd: Path, 20 | path_utils: PathUtils, 21 | treesitter_utils: TreesitterUtils, 22 | common_utils: CommonUtils, 23 | logging: Logging, 24 | ) -> None: 25 | self.nvim = nvim 26 | self.cwd = cwd 27 | self.logging = logging 28 | self.treesitter_utils = treesitter_utils 29 | self.path_utils = path_utils 30 | self.common_utils = common_utils 31 | self.build_tool_type: Optional[Literal["maven", "gradle"]] = None 32 | self.build_tool_path: Optional[Path] = self.get_build_tool_file_path() 33 | 34 | def get_build_tool_file_path(self) -> Path: 35 | os_identifier = system() 36 | project_root_path = self.path_utils.get_project_root_path() 37 | gradlew_file: Optional[Path] = None 38 | maven_file: Optional[Path] = None 39 | gradlew_file = next( 40 | project_root_path.glob( 41 | "**/gradlew.bat" if os_identifier == "Windows" else "**/gradlew" 42 | ), 43 | None, 44 | ) 45 | if gradlew_file: 46 | self.build_tool_type = "gradle" 47 | return gradlew_file 48 | maven_file = next( 49 | project_root_path.glob( 50 | "**/mvnw.bat" if os_identifier == "Windows" else "**/mvnw" 51 | ), 52 | None, 53 | ) 54 | if maven_file: 55 | self.build_tool_type = "maven" 56 | return maven_file 57 | error_msg = "Unable to get build tool" 58 | self.logging.log(error_msg, LogLevel.ERROR) 59 | raise FileNotFoundError(error_msg) 60 | 61 | def get_maven_project_properties( 62 | self, debug: bool = False 63 | ) -> Optional[ProjectProperties]: 64 | project_properties: Optional[ProjectProperties] = None 65 | if self.build_tool_type == "maven": 66 | result = self.common_utils.run_subprocess( 67 | [ 68 | f"{str(self.build_tool_path)}", 69 | "help:evaluate", 70 | "-Dexpression=project.name", 71 | "-q", 72 | "-DforceStdout", 73 | ], 74 | debug, 75 | ) 76 | project_name = result.stdout.strip() 77 | 78 | result = self.common_utils.run_subprocess( 79 | [ 80 | f"{str(self.build_tool_path)}", 81 | "help:evaluate", 82 | "-Dexpression=project.version", 83 | "-q", 84 | "-DforceStdout", 85 | ], 86 | debug, 87 | ) 88 | project_version = result.stdout.strip() 89 | 90 | result = self.common_utils.run_subprocess( 91 | [ 92 | f"{str(self.build_tool_path)}", 93 | "help:evaluate", 94 | "-Dexpression=project.groupId", 95 | "-q", 96 | "-DforceStdout", 97 | ], 98 | debug, 99 | ) 100 | project_group = result.stdout.strip() 101 | 102 | project_build_dir = str(self.cwd / "target") 103 | project_root_dir = str(self.cwd) 104 | project_dir = str(self.cwd) 105 | 106 | if project_name and project_group and project_version: 107 | project_properties = ProjectProperties( 108 | project_name=project_name, 109 | project_group=project_group, 110 | project_version=project_version, 111 | project_build_dir=project_build_dir, 112 | project_root_dir=project_root_dir, 113 | project_dir=project_dir, 114 | ) 115 | if debug: 116 | self.logging.log(f"{project_properties}", LogLevel.DEBUG) 117 | return project_properties 118 | 119 | def get_gradle_project_properties( 120 | self, debug: bool = False 121 | ) -> Optional[ProjectProperties]: 122 | project_properties: Optional[ProjectProperties] = None 123 | if self.build_tool_path and self.build_tool_type == "gradle": 124 | result = self.common_utils.run_subprocess( 125 | [f"{str(self.build_tool_path)}", "properties"], debug 126 | ) 127 | project_name: Optional[str] = None 128 | project_version: Optional[str] = None 129 | project_group: Optional[str] = None 130 | project_build_dir: Optional[str] = None 131 | project_root_dir: Optional[str] = None 132 | project_dir: Optional[str] = None 133 | for line in result.stdout.splitlines(): 134 | if line.startswith("name:"): 135 | project_name = line.split(":")[1].strip() 136 | elif line.startswith("version:"): 137 | project_version = line.split(":")[1].strip() 138 | elif line.startswith("group:"): 139 | project_group = line.split(":")[1].strip() 140 | elif line.startswith("projectDir:"): 141 | project_dir = line.split(":")[1].strip() 142 | elif line.startswith("rootDir:"): 143 | project_root_dir = line.split(":")[1].strip() 144 | elif line.startswith("buildDir:"): 145 | project_build_dir = line.split(":")[1].strip() 146 | 147 | if ( 148 | project_name 149 | and project_group 150 | and project_version 151 | and project_build_dir 152 | and project_root_dir 153 | and project_dir 154 | ): 155 | project_properties = ProjectProperties( 156 | project_name=project_name, 157 | project_group=project_group, 158 | project_version=project_version, 159 | project_build_dir=project_build_dir, 160 | project_root_dir=project_root_dir, 161 | project_dir=project_dir, 162 | ) 163 | if debug: 164 | self.logging.log(f"{project_properties}", LogLevel.DEBUG) 165 | return project_properties 166 | 167 | def get_project_executable( 168 | self, project_properties: ProjectProperties, debug: bool = False 169 | ) -> Optional[Path]: 170 | executable_path = next( 171 | project_properties.project_build_path.glob( 172 | f"**/{project_properties.project_name}-{project_properties.project_version}.jar" 173 | ), 174 | None, 175 | ) 176 | if debug: 177 | self.logging.log(f"Executable path: {executable_path}", LogLevel.DEBUG) 178 | return executable_path 179 | 180 | def maven_build(self) -> None: 181 | self.logging.echomsg("Building") 182 | output: Optional[str] = None 183 | error: Optional[str] = None 184 | try: 185 | result = self.common_utils.run_subprocess( 186 | [f"{str(self.build_tool_path)}", "package"] 187 | ) 188 | output = " ".join(result.stdout.splitlines()) 189 | error = " ".join(result.stderr.splitlines()) 190 | if output and "BUILD SUCCESS" in output: 191 | self.logging.echomsg("Build successful") 192 | if "BUILD SUCCESS" not in output: 193 | error_msg = "Unable to build" 194 | self.logging.log(error_msg, LogLevel.ERROR) 195 | raise ValueError(error_msg) 196 | except subprocess.CalledProcessError as e: 197 | output = " ".join(e.stdout.splitlines()) if e.stdout else None 198 | error = " ".join(e.stderr.splitlines()) if e.stderr else None 199 | except Exception as e: 200 | error = str(e) 201 | error_msg = f"Unexpected error: {error}" 202 | self.logging.echomsg(error_msg) 203 | self.logging.log(error_msg, LogLevel.ERROR) 204 | raise ValueError(error_msg) 205 | 206 | def gradle_build(self) -> None: 207 | self.logging.echomsg("Building") 208 | output: Optional[str] = None 209 | error: Optional[str] = None 210 | try: 211 | result = self.common_utils.run_subprocess( 212 | [f"{str(self.build_tool_path)}", "clean", "build", "-x", "test"] 213 | ) 214 | output = " ".join(result.stdout.splitlines()) 215 | error = " ".join(result.stderr.splitlines()) 216 | if output and "BUILD SUCCESSFUL" in output: 217 | self.logging.echomsg("Build successful") 218 | if "BUILD SUCCESSFUL" not in output: 219 | error_msg = "Unable to build" 220 | self.logging.log(error_msg, LogLevel.ERROR) 221 | raise ValueError(error_msg) 222 | except subprocess.CalledProcessError as e: 223 | output = " ".join(e.stdout.splitlines()) if e.stdout else None 224 | error = " ".join(e.stderr.splitlines()) if e.stderr else None 225 | except Exception as e: 226 | error = str(e) 227 | error_msg = f"Unexpected error: {error}" 228 | self.logging.echomsg(error_msg) 229 | self.logging.log(error_msg, LogLevel.ERROR) 230 | raise ValueError(error_msg) 231 | 232 | def build(self, debug: bool = False) -> None: 233 | if self.build_tool_type == "gradle": 234 | self.gradle_build() 235 | else: 236 | self.maven_build() 237 | 238 | def run(self, debug: bool = False) -> None: 239 | if self.build_tool_type == "gradle": 240 | self.gradle_build() 241 | project_properties = self.get_gradle_project_properties(debug) 242 | if not project_properties: 243 | error_msg = "Unable to get project's properties" 244 | self.logging.echomsg(error_msg) 245 | self.logging.log(error_msg, LogLevel.ERROR) 246 | raise ValueError(error_msg) 247 | project_executable = self.get_project_executable(project_properties) 248 | if not project_executable: 249 | error_msg = "Unable to find built executable" 250 | self.logging.echomsg(error_msg) 251 | self.logging.log(error_msg, LogLevel.ERROR) 252 | raise ValueError(error_msg) 253 | else: 254 | self.maven_build() 255 | project_properties = self.get_maven_project_properties(debug) 256 | if not project_properties: 257 | error_msg = "Unable to get project's properties" 258 | self.logging.echomsg(error_msg) 259 | self.logging.log(error_msg, LogLevel.ERROR) 260 | raise ValueError(error_msg) 261 | project_executable = self.get_project_executable(project_properties) 262 | if not project_executable: 263 | error_msg = "Unable to find built executable" 264 | self.logging.echomsg(error_msg) 265 | self.logging.log(error_msg, LogLevel.ERROR) 266 | raise ValueError(error_msg) 267 | java_executable = self.path_utils.get_java_executable_path() 268 | self.nvim.command( 269 | f"split | terminal {str(java_executable)} -jar {str(project_executable)}" 270 | ) 271 | -------------------------------------------------------------------------------- /rplugin/python3/utils/treesitter_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Dict, List, Optional, Tuple 3 | 4 | from pynvim.api import Buffer 5 | import tree_sitter_java as tsjava 6 | from tree_sitter import Language, Node, Parser, Query, Tree 7 | from pynvim.api.nvim import Nvim 8 | from custom_types.log_level import LogLevel 9 | from utils.logging import Logging 10 | 11 | 12 | class TreesitterUtils: 13 | def __init__( 14 | self, 15 | nvim: Nvim, 16 | java_basic_types: list[tuple], 17 | cwd: Path, 18 | logging: Logging, 19 | ): 20 | self.nvim = nvim 21 | self.cwd: Path = cwd 22 | self.java_basic_types = java_basic_types 23 | self.logging = logging 24 | self.ts_java = Language(tsjava.language()) 25 | self.parser = Parser(self.ts_java) 26 | self.importings: List[str] = [] 27 | 28 | def convert_bytes_to_string(self, bytes_value: bytes) -> str: 29 | try: 30 | return bytes_value.decode() 31 | except (UnicodeDecodeError, AttributeError) as e: 32 | error_msg = f"Error decoding bytes: {e}" 33 | self.logging.log(error_msg, LogLevel.ERROR) 34 | raise RuntimeError(error_msg) 35 | 36 | def convert_string_to_bytes(self, string_value: str) -> bytes: 37 | try: 38 | return string_value.encode() 39 | except (UnicodeEncodeError, AttributeError) as e: 40 | error_msg = f"Error enconding string: {e}" 41 | self.logging.log(error_msg, LogLevel.ERROR) 42 | raise RuntimeError(error_msg) 43 | 44 | def convert_bytes_to_tree(self, file_bytes: bytes) -> Tree: 45 | try: 46 | if not file_bytes: 47 | raise ValueError("Input bytes are empty") 48 | buffer_tree = self.parser.parse(file_bytes) 49 | return buffer_tree 50 | except ValueError as e: 51 | error_msg = f"Error parsing bytes: {e}" 52 | self.logging.log(error_msg, LogLevel.ERROR) 53 | raise RuntimeError(error_msg) 54 | except Exception as e: 55 | error_msg = f"Unexpected error while parsing bytes: {e}" 56 | self.logging.log(error_msg, LogLevel.ERROR) 57 | raise RuntimeError(error_msg) 58 | 59 | def convert_path_to_tree(self, file_path: Path) -> Tree: 60 | buffer_bytes: bytes 61 | try: 62 | buffer_bytes = file_path.read_bytes() 63 | except (OSError, FileNotFoundError) as e: 64 | error_msg = f"Error reading from file path {str(file_path)}: {e}" 65 | self.logging.log(error_msg, LogLevel.ERROR) 66 | raise RuntimeError(error_msg) 67 | buffer_tree = self.parser.parse(buffer_bytes) 68 | return buffer_tree 69 | 70 | def convert_buffer_to_tree(self, buffer: Buffer) -> Tree: 71 | try: 72 | if not buffer: 73 | raise ValueError("Input buffer is empty") 74 | buffer_bytes = "\n".join(buffer[:]).encode("utf-8") 75 | return self.convert_bytes_to_tree(buffer_bytes) 76 | except ValueError as e: 77 | error_msg = f"Error with buffer: {e}" 78 | self.logging.log(error_msg, LogLevel.ERROR) 79 | raise RuntimeError(error_msg) 80 | except Exception as e: 81 | error_msg = f"Unexpected error while converting buffer to tree: {e}" 82 | self.logging.log(error_msg, LogLevel.ERROR) 83 | raise RuntimeError(error_msg) 84 | 85 | def convert_node_to_tree(self, node: Node) -> Tree: 86 | if node.text: 87 | return self.convert_bytes_to_tree(node.text) 88 | else: 89 | error_msg = "Unable to convert node into tree" 90 | self.logging.log(error_msg, LogLevel.ERROR) 91 | raise RuntimeError(error_msg) 92 | 93 | def query_match(self, tree: Tree, query_param: str) -> List[Node]: 94 | try: 95 | query: Query = self.ts_java.query(query_param) 96 | except Exception as e: 97 | error_msg = f"Error creating query from query_param '{query_param}': {e}" 98 | self.logging.log(error_msg, LogLevel.ERROR) 99 | raise RuntimeError(error_msg) 100 | try: 101 | query_results: List[Tuple[int, Dict[str, List[Node]]]] = query.matches( 102 | tree.root_node 103 | ) 104 | nodes: List[Node] = [] 105 | for result in query_results: 106 | for item in result[1].values(): 107 | for node in item: 108 | nodes.append(node) 109 | return nodes 110 | except Exception as e: 111 | error_msg = f"Error matching query in tree: {e}" 112 | self.logging.log(error_msg, LogLevel.ERROR) 113 | raise RuntimeError(error_msg) 114 | 115 | def get_node_text_as_string(self, node: Node, debug: bool = False) -> Optional[str]: 116 | node_text_str: Optional[str] = None 117 | if node.text: 118 | node_text_str = self.convert_bytes_to_string(node.text) 119 | if debug: 120 | self.logging.log(f"Node text: {node_text_str}", LogLevel.DEBUG) 121 | return node_text_str 122 | 123 | def get_node_by_type(self, node: Node, type_name: str) -> Optional[Node]: 124 | if node.type == type_name: 125 | return node 126 | for child in node.children: 127 | result = self.get_node_by_type(child, type_name) 128 | if result is not None: 129 | return result 130 | return None 131 | 132 | def update_buffer( 133 | self, 134 | tree: Tree, 135 | buffer_path: Path, 136 | save: bool = False, 137 | format: bool = False, 138 | organize_imports: bool = False, 139 | debug: bool = False, 140 | ): 141 | node_text = tree.root_node.text 142 | if not node_text: 143 | error_msg = "Root node doesn't have text" 144 | self.logging.log(error_msg, LogLevel.ERROR) 145 | raise ValueError(error_msg) 146 | self.nvim.command(f"e {str(buffer_path)}") 147 | if tree.root_node.text: 148 | self.nvim.current.buffer[:] = tree.root_node.text.decode().split("\n") 149 | if save: 150 | self.nvim.command(f"w {str(buffer_path)}") 151 | if format and not save: 152 | self.nvim.command("lua vim.lsp.buf.format({ async = true })") 153 | if organize_imports: 154 | self.nvim.command("lua require('jdtls').organize_imports()") 155 | if debug: 156 | self.logging.log( 157 | [ 158 | f"Updated buffer: {node_text.decode()}", 159 | f"Original buffer: {buffer_path.read_text('utf-8')}", 160 | ], 161 | LogLevel.DEBUG, 162 | ) 163 | 164 | def get_buffer_public_class_node_from_query_results( 165 | self, query_results: List[Node], debug: bool = False 166 | ) -> Optional[Node]: 167 | public_class_node: Optional[Node] = None 168 | for node in query_results: 169 | for child_node in node.children: 170 | if child_node.type == "modifiers" and child_node.text: 171 | modifiers_text_str = self.convert_bytes_to_string( 172 | child_node.text 173 | ).split("\n") 174 | if "public" in modifiers_text_str: 175 | public_class_node = node 176 | if debug: 177 | public_class_node_str: Optional[str] = None 178 | if public_class_node and public_class_node.text: 179 | self.convert_bytes_to_string(public_class_node.text) 180 | self.logging.log( 181 | f"Found public class node: {public_class_node_str}", LogLevel.DEBUG 182 | ) 183 | return public_class_node 184 | 185 | def get_buffer_public_class_name( 186 | self, tree: Tree, debug: bool = False 187 | ) -> Optional[str]: 188 | public_class_name: Optional[str] = None 189 | query_param = "(class_declaration) @class_decl" 190 | query_results: List[Node] = self.query_match(tree=tree, query_param=query_param) 191 | public_class_node = self.get_buffer_public_class_node_from_query_results( 192 | query_results=query_results, debug=debug 193 | ) 194 | if public_class_node: 195 | name_node = public_class_node.child_by_field_name("name") 196 | if name_node and name_node.text: 197 | public_class_name = self.convert_bytes_to_string(name_node.text) 198 | if debug: 199 | self.logging.log( 200 | [ 201 | f"Query param: {query_param}", 202 | f"Total query results: {len(query_results)}", 203 | f"Public class name: {public_class_name}", 204 | ], 205 | LogLevel.DEBUG, 206 | ) 207 | return public_class_name 208 | 209 | def buffer_public_class_has_annotation( 210 | self, tree: Tree, annotation_name: str, debug: bool = False 211 | ) -> bool: 212 | public_class_has_annotation: bool = False 213 | query_param = "(class_declaration) @class_decl" 214 | query_results: List[Node] = self.query_match(tree=tree, query_param=query_param) 215 | public_class_node = self.get_buffer_public_class_node_from_query_results( 216 | query_results=query_results, debug=debug 217 | ) 218 | if public_class_node: 219 | modifiers = self.get_node_by_type(public_class_node, "modifiers") 220 | if modifiers: 221 | for child in modifiers.children: 222 | if child.type in ["marker_annotation", "annotation"]: 223 | name_node = child.child_by_field_name("name") 224 | if name_node and name_node.text: 225 | if ( 226 | self.convert_bytes_to_string(name_node.text) 227 | == annotation_name 228 | ): 229 | public_class_has_annotation = True 230 | if debug: 231 | self.logging.log( 232 | f"Annotation found: {public_class_has_annotation}", LogLevel.DEBUG 233 | ) 234 | return public_class_has_annotation 235 | 236 | def buffer_public_class_has_method( 237 | self, tree: Tree, method_name: str, debug: bool = False 238 | ): 239 | public_class_has_method: bool = False 240 | query_param = "(class_declaration) @class_decl" 241 | query_results: List[Node] = self.query_match(tree=tree, query_param=query_param) 242 | public_class_node = self.get_buffer_public_class_node_from_query_results( 243 | query_results=query_results, debug=debug 244 | ) 245 | if public_class_node: 246 | body = public_class_node.child_by_field_name("body") 247 | if body: 248 | for child in body.children: 249 | if child.type == "method_declaration": 250 | node_name = child.child_by_field_name("name") 251 | if node_name and node_name.text: 252 | node_name_str = self.convert_bytes_to_string(node_name.text) 253 | if node_name_str == method_name: 254 | public_class_has_method = True 255 | if debug: 256 | self.logging.log( 257 | f"Public class has method '{method_name}': {public_class_has_method}", 258 | LogLevel.DEBUG, 259 | ) 260 | return public_class_has_method 261 | 262 | def insert_code_at_position( 263 | self, code: str, insert_position, file_tree: Tree 264 | ) -> Tree: 265 | updated_tree: Optional[Tree] = None 266 | code_bytes = code.encode("utf-8") 267 | node_text_bytes = file_tree.root_node.text 268 | if node_text_bytes: 269 | node_text_bytes = ( 270 | node_text_bytes[:insert_position] 271 | + code_bytes 272 | + node_text_bytes[insert_position:] 273 | ) 274 | updated_tree = self.convert_bytes_to_tree(node_text_bytes) 275 | if not updated_tree: 276 | error_msg = "Unable to update tree" 277 | self.logging.log(error_msg, LogLevel.ERROR) 278 | raise ValueError(error_msg) 279 | return updated_tree 280 | 281 | def add_to_importing_list( 282 | self, import_list: List[str], debug: bool = False 283 | ) -> None: 284 | imports_to_extend = [] 285 | for i in import_list: 286 | if i not in self.importings: 287 | imports_to_extend.append(i) 288 | if debug: 289 | self.logging.log( 290 | [ 291 | f"Previous import list: {str(self.importings)}", 292 | f"New imports: {str(imports_to_extend)}", 293 | f"New import list: {str(self.importings + imports_to_extend)}", 294 | ], 295 | LogLevel.DEBUG, 296 | ) 297 | self.importings.extend(imports_to_extend) 298 | 299 | def add_imports_to_file_tree(self, file_tree: Tree, debug: bool = False) -> Tree: 300 | package_query_param = "(package_declaration) @package_decl" 301 | query_results = self.query_match( 302 | tree=file_tree, query_param=package_query_param 303 | ) 304 | if len(query_results) != 1: 305 | error_msg = "File package not defined or defined incorrectly" 306 | self.logging.log(error_msg, LogLevel.ERROR) 307 | raise ValueError(error_msg) 308 | insert_byte: int = query_results[0].end_byte + 1 309 | import_list = [f"import {e};" for e in self.importings] 310 | merged_import_list = "\n".join(import_list) 311 | updated_tree = self.insert_code_at_position( 312 | merged_import_list, insert_byte, file_tree 313 | ) 314 | if debug: 315 | self.logging.log( 316 | [ 317 | f"Package query param: {package_query_param}", 318 | f"Query results len: {len(query_results)}", 319 | f"Insert byte: {insert_byte}", 320 | f"Merged import list: {merged_import_list}", 321 | f"Node before: {self.get_node_text_as_string(file_tree.root_node)}", 322 | f"Node after: {self.get_node_text_as_string(updated_tree.root_node)}", 323 | ], 324 | LogLevel.DEBUG, 325 | ) 326 | self.importings = [] 327 | return updated_tree 328 | 329 | def get_entity_field_insert_byte( 330 | self, file_tree: Tree, debug: bool = False 331 | ) -> Optional[int]: 332 | insert_byte: Optional[int] = None 333 | query_results = self.query_match(file_tree, "(class_declaration) @class_decl") 334 | main_class_node = self.get_buffer_public_class_node_from_query_results( 335 | query_results, debug 336 | ) 337 | if main_class_node: 338 | class_body = main_class_node.child_by_field_name("body") 339 | if class_body: 340 | field_declarations = class_body.children 341 | if len(field_declarations) != 0: 342 | insert_byte = field_declarations[-1].end_byte - 1 343 | else: 344 | insert_byte = class_body.start_byte 345 | if debug: 346 | self.logging.log(f"Insert byte: {insert_byte}", LogLevel.DEBUG) 347 | return insert_byte 348 | -------------------------------------------------------------------------------- /rplugin/python3/utils/entity_field_utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List, Optional 3 | 4 | from pynvim.api.nvim import Nvim 5 | from tree_sitter import Tree 6 | 7 | from custom_types.enum_type import EnumType 8 | from custom_types.other import Other 9 | from custom_types.field_time_zone_storage import FieldTimeZoneStorage 10 | from custom_types.field_temporal import FieldTemporal 11 | from custom_types.id_generation import IdGeneration 12 | from custom_types.id_generation_type import IdGenerationType 13 | from custom_types.log_level import LogLevel 14 | from custom_types.java_file_data import JavaFileData 15 | from custom_types.create_id_field_args import CreateIdEntityFieldArgs 16 | from custom_types.create_basic_field_args import CreateBasicEntityFieldArgs 17 | from custom_types.create_enum_field_args import CreateEnumEntityFieldArgs 18 | 19 | from utils.treesitter_utils import TreesitterUtils 20 | from utils.common_utils import CommonUtils 21 | from utils.logging import Logging 22 | 23 | 24 | class EntityFieldUtils: 25 | def __init__( 26 | self, 27 | nvim: Nvim, 28 | java_basic_types: list[tuple], 29 | treesitter_utils: TreesitterUtils, 30 | common_utils: CommonUtils, 31 | logging: Logging, 32 | ): 33 | self.nvim = nvim 34 | self.treesitter_utils = treesitter_utils 35 | self.logging = logging 36 | self.java_basic_types = java_basic_types 37 | self.common_utils = common_utils 38 | 39 | def merge_field_params(self, params: List[str], debug: bool = False) -> str: 40 | merged_params = ", ".join(params) 41 | if debug: 42 | self.logging.log(f"Merged params: {merged_params}", LogLevel.DEBUG) 43 | return ", ".join(params) 44 | 45 | def generate_field_column_line(self, params: List[str], debug: bool = False) -> str: 46 | merged_params = self.merge_field_params(params, debug) 47 | column_line = f"@Column({merged_params})" 48 | if debug: 49 | self.logging.log( 50 | [ 51 | f"Merged params: {merged_params}", 52 | f"Column line: {column_line}", 53 | ], 54 | LogLevel.DEBUG, 55 | ) 56 | return column_line 57 | 58 | def generate_field_body_line( 59 | self, field_type: str, field_name: str, debug: bool = False 60 | ) -> str: 61 | field_body_line = f"private {field_type} {field_name};" 62 | if debug: 63 | self.logging.log( 64 | f"Field body line: {field_body_line }", 65 | LogLevel.DEBUG, 66 | ) 67 | return field_body_line 68 | 69 | def generate_basic_field_template( 70 | self, 71 | field_package_path: str, 72 | field_type: str, 73 | field_name: str, 74 | field_length: Optional[int], 75 | field_precision: Optional[int], 76 | field_scale: Optional[int], 77 | field_time_zone_storage: Optional[FieldTimeZoneStorage], 78 | field_temporal: Optional[FieldTemporal], 79 | mandatory: bool = False, 80 | unique: bool = False, 81 | large_object: bool = False, 82 | debug: bool = False, 83 | ) -> str: 84 | imports_to_add: List[str] = [] 85 | snaked_field_name = self.common_utils.convert_to_snake_case(field_name, debug) 86 | column_params: List[str] = [f'name = "{snaked_field_name}"'] 87 | time_zone_storage_body: Optional[str] = None 88 | temporal_body: Optional[str] = None 89 | lob_body: Optional[str] = None 90 | imports_to_add.append("jakarta.persistence.Column") 91 | if "." in field_package_path: 92 | imports_to_add.append(field_package_path + "." + field_type) 93 | if ( 94 | field_package_path + "." + field_type 95 | in [ 96 | "java.lang.String", 97 | "java.net.URL", 98 | "java.util.Locale", 99 | "java.util.Currency", 100 | "java.lang.Class", 101 | "java.lang.Character[]", 102 | "char[]", 103 | "java.util.TimeZone", 104 | "java.time.ZoneOffset", 105 | ] 106 | and field_length != 255 107 | ): 108 | column_params.append(f"length = {field_length}") 109 | if ( 110 | field_package_path + "." + field_type 111 | in [ 112 | "java.time.OffsetDateTime", 113 | "java.time.OffsetTime", 114 | "java.time.ZonedDateTime", 115 | ] 116 | and field_time_zone_storage is not None 117 | ): 118 | imports_to_add.extend( 119 | [ 120 | "org.hibernate.annotations.TimeZoneStorage", 121 | "org.hibernate.annotations.TimeZoneStorageType", 122 | ] 123 | ) 124 | time_zone_storage_body = ( 125 | f"@TimeZoneStorage(TimeZoneStorageType.{field_time_zone_storage.value})" 126 | ) 127 | if ( 128 | field_package_path + "." + field_type 129 | in [ 130 | "java.util.Date", 131 | "java.util.Calendar", 132 | ] 133 | and field_temporal is not None 134 | ): 135 | imports_to_add.extend( 136 | ["jakarta.persistence.Temporal", "jakarta.persistence.TemporalType"] 137 | ) 138 | temporal_body = f"@Temporal(TemporalType.{field_temporal.value})" 139 | if ( 140 | field_package_path + "." + field_type == "java.math.BigDecimal" 141 | and field_precision is not None 142 | and field_scale is not None 143 | ): 144 | column_params.extend( 145 | [f"precision = {field_precision}", f"scale = {field_scale}"] 146 | ) 147 | if large_object: 148 | imports_to_add.append("jakarta.persistence.Lob") 149 | lob_body = "@Lob" 150 | if mandatory: 151 | column_params.append("nullable = false") 152 | if unique: 153 | column_params.append("unique = true") 154 | column_body = self.generate_field_column_line(column_params, debug) 155 | field_body = self.generate_field_body_line( 156 | field_type, self.common_utils.generate_field_name(field_name, debug), debug 157 | ) 158 | template = "" 159 | if lob_body: 160 | template += "\n\t" + lob_body 161 | if time_zone_storage_body: 162 | template += "\n\t" + time_zone_storage_body 163 | if temporal_body: 164 | template += "\n\t" + temporal_body 165 | template += "\n\t" + column_body + "\n\t" + field_body + "\n" 166 | if debug: 167 | self.logging.log( 168 | [ 169 | f"Snaked field name: {snaked_field_name}", 170 | f"Raw column params: {str(column_params)}", 171 | f"Column body: {column_body}", 172 | f"Field body: {field_body}", 173 | f"Final template: {template}", 174 | ], 175 | LogLevel.DEBUG, 176 | ) 177 | self.treesitter_utils.add_to_importing_list(imports_to_add, debug) 178 | return template 179 | 180 | def generate_id_field_template( 181 | self, 182 | field_package_path: str, 183 | field_type: str, 184 | field_name: str, 185 | id_generation: IdGeneration, 186 | id_generation_type: Optional[IdGenerationType], 187 | generator_name: Optional[str], 188 | sequence_name: Optional[str], 189 | initial_value: Optional[int], 190 | allocation_size: Optional[int], 191 | mandatory: bool = False, 192 | debug: bool = False, 193 | ) -> str: 194 | imports_to_add = [ 195 | "jakarta.persistence.Column", 196 | "jakarta.persistence.GeneratedValue", 197 | "jakarta.persistence.GenerationType", 198 | "jakarta.persistence.Id", 199 | field_package_path + "." + field_type, 200 | ] 201 | snaked_field_name = self.common_utils.convert_to_snake_case(field_name, debug) 202 | column_params: List[str] = [f'name = "{snaked_field_name}"'] 203 | generated_value_params: List[str] = [] 204 | sequence_generator_params: List[str] = [] 205 | id_field_body = "@Id" 206 | generated_value_body: Optional[str] = None 207 | sequence_generator_body: Optional[str] = None 208 | if mandatory: 209 | column_params.append("nullable = false") 210 | if id_generation != IdGeneration.NONE: 211 | generated_value_params.append( 212 | f"strategy = GenerationType.{id_generation.value.upper()}" 213 | ) 214 | if ( 215 | id_generation == IdGeneration.SEQUENCE 216 | and id_generation_type == IdGenerationType.ENTITY_EXCLUSIVE_GENERATION 217 | ): 218 | imports_to_add.append("jakarta.persistence.SequenceGenerator") 219 | generated_value_params.append(f'generator = "{generator_name}"') 220 | sequence_generator_params.extend( 221 | [ 222 | f'name = "{generator_name}"', 223 | f'sequenceName = "{sequence_name}"', 224 | ] 225 | ) 226 | if initial_value != 1 and initial_value is not None: 227 | sequence_generator_params.append( 228 | f", initialValue = {initial_value}" 229 | ) 230 | if allocation_size != 50 and allocation_size is not None: 231 | sequence_generator_params.append( 232 | f", allocationSize = {allocation_size}" 233 | ) 234 | sequence_generator_body = f"@SequenceGenerator({self.merge_field_params(sequence_generator_params, debug)})" 235 | generated_value_body = f"@GeneratedValue({self.merge_field_params(generated_value_params, debug)})" 236 | column_body = self.generate_field_column_line(column_params, debug) 237 | field_body = self.generate_field_body_line( 238 | field_type, 239 | self.common_utils.generate_field_name(field_name, False, debug), 240 | debug, 241 | ) 242 | template = "\n\t" + id_field_body 243 | if generated_value_body: 244 | template += "\n\t" + generated_value_body 245 | if sequence_generator_body: 246 | template += "\n\t" + sequence_generator_body 247 | template += "\n\t" + column_body + "\n\t" + field_body + "\n" 248 | if debug: 249 | self.logging.log( 250 | [ 251 | f"Snaked field name: {snaked_field_name}", 252 | f"Raw column params: {str(column_params)}", 253 | f"Column body: {column_body}", 254 | f"Field body: {field_body}", 255 | f"Final template: {template}", 256 | ], 257 | LogLevel.DEBUG, 258 | ) 259 | self.treesitter_utils.add_to_importing_list(imports_to_add, debug) 260 | return template 261 | 262 | def generate_enum_field_template( 263 | self, 264 | field_package_path: str, 265 | field_type: str, 266 | field_name: str, 267 | field_length: Optional[int], 268 | enum_type: EnumType = EnumType.ORDINAL, 269 | mandatory: bool = False, 270 | unique: bool = False, 271 | debug: bool = False, 272 | ) -> str: 273 | imports_to_add: List[str] = ["jakarta.persistence.Column"] 274 | snaked_field_name = self.common_utils.convert_to_snake_case(field_name, debug) 275 | column_params: List[str] = [f'name = "{snaked_field_name}"'] 276 | imports_to_add.extend( 277 | [ 278 | "jakarta.persistence.Enumerated", 279 | "jakarta.persistence.EnumType", 280 | field_package_path + "." + field_type, 281 | ] 282 | ) 283 | enumerated_body = f"@Enumerated({enum_type})" 284 | if enum_type == EnumType.STRING and field_length and field_length != 255: 285 | column_params.append(f"length = {field_length}") 286 | if mandatory: 287 | column_params.append("nullable = true") 288 | if unique: 289 | column_params.append("unique = true") 290 | column_body = self.generate_field_column_line(column_params, debug) 291 | field_body = self.generate_field_body_line( 292 | field_type, 293 | self.common_utils.generate_field_name(field_name, False, debug), 294 | debug, 295 | ) 296 | template = ( 297 | "\n\t" + enumerated_body + "\n\t" + column_body + "\n\t" + field_body + "\n" 298 | ) 299 | if debug: 300 | self.logging.log( 301 | [ 302 | f"Snaked field name: {snaked_field_name}", 303 | f"Raw column params: {str(column_params)}", 304 | f"Column body: {column_body}", 305 | f"Field body: {field_body}", 306 | f"Final template: {template}", 307 | ], 308 | LogLevel.DEBUG, 309 | ) 310 | self.treesitter_utils.add_to_importing_list(imports_to_add, debug) 311 | return template 312 | 313 | def update_buffer( 314 | self, buffer_tree: Tree, buffer_path: Path, template: str, debug: bool = False 315 | ) -> None: 316 | updated_buffer_tree = self.treesitter_utils.add_imports_to_file_tree( 317 | buffer_tree, debug 318 | ) 319 | insert_byte = self.treesitter_utils.get_entity_field_insert_byte( 320 | updated_buffer_tree, debug 321 | ) 322 | if not insert_byte: 323 | error_msg = "Unable to get field insert position" 324 | self.logging.log(error_msg, LogLevel.ERROR) 325 | raise ValueError(error_msg) 326 | updated_buffer_tree = self.treesitter_utils.insert_code_at_position( 327 | template, insert_byte, updated_buffer_tree 328 | ) 329 | self.treesitter_utils.update_buffer( 330 | tree=updated_buffer_tree, buffer_path=buffer_path, save=True, debug=debug 331 | ) 332 | if debug: 333 | self.logging.log( 334 | [ 335 | f"Template:\n{template}\n" 336 | f"Node before:\n{self.treesitter_utils.get_node_text_as_string(buffer_tree.root_node)}\n" 337 | f"Node after:\n{self.treesitter_utils.get_node_text_as_string(updated_buffer_tree.root_node)}\n" 338 | ], 339 | LogLevel.DEBUG, 340 | ) 341 | 342 | def create_basic_entity_field( 343 | self, 344 | buffer_file_data: JavaFileData, 345 | args: CreateBasicEntityFieldArgs, 346 | debug: bool = False, 347 | ) -> None: 348 | template = self.generate_basic_field_template( 349 | field_package_path=args.field_package_path, 350 | field_type=args.field_type, 351 | field_name=args.field_name, 352 | field_length=args.field_length, 353 | field_precision=args.field_precision, 354 | field_scale=args.field_scale, 355 | field_time_zone_storage=args.field_time_zone_storage_enum, 356 | field_temporal=args.field_temporal_enum, 357 | mandatory=True if Other.MANDATORY in args.other_enum else False, 358 | unique=True if Other.UNIQUE in args.other_enum else False, 359 | large_object=True if Other.LARGE_OBJECT in args.other_enum else False, 360 | debug=debug, 361 | ) 362 | self.update_buffer( 363 | buffer_file_data.tree, buffer_file_data.path, template, debug 364 | ) 365 | 366 | def create_enum_entity_field( 367 | self, 368 | buffer_file_data: JavaFileData, 369 | args: CreateEnumEntityFieldArgs, 370 | debug: bool = False, 371 | ) -> None: 372 | template = self.generate_enum_field_template( 373 | field_package_path=args.field_package_path, 374 | field_type=args.field_type, 375 | field_name=args.field_name, 376 | field_length=args.field_length, 377 | enum_type=args.enum_type_enum, 378 | mandatory=True if Other.MANDATORY in args.other_enum else False, 379 | unique=True if Other.UNIQUE in args.other_enum else False, 380 | debug=debug, 381 | ) 382 | self.update_buffer( 383 | buffer_file_data.tree, buffer_file_data.path, template, debug 384 | ) 385 | 386 | def create_id_entity_field( 387 | self, 388 | buffer_file_data: JavaFileData, 389 | args: CreateIdEntityFieldArgs, 390 | debug: bool = False, 391 | ) -> None: 392 | template = self.generate_id_field_template( 393 | field_package_path=args.field_package_path, 394 | field_type=args.field_type, 395 | field_name=args.field_name, 396 | id_generation=args.id_generation_enum, 397 | id_generation_type=args.id_generation_type_enum, 398 | generator_name=args.generator_name, 399 | sequence_name=args.sequence_name, 400 | initial_value=args.initial_value, 401 | allocation_size=args.allocation_size, 402 | mandatory=True if Other.MANDATORY in args.other_enum else False, 403 | debug=debug, 404 | ) 405 | self.update_buffer( 406 | buffer_file_data.tree, buffer_file_data.path, template, debug 407 | ) 408 | --------------------------------------------------------------------------------